add MBTiles support
This should be straightforward based on RMMBTilesSource. However, linking to SQLite is a problem if we want to keep things simple for installation. Two thoughts right now:
- For CocoaPods, make MBTiles suppert a subspec. Linking isn't much of a big deal here anyway since it's managed.
- For manual install, just make a macro at the top of the file to
#ifdefout the SQLite code by default. Then, if the dev wants, they can flip the macro bit and link it themselves. This gets around people having to link before use.
Being able to have offline overlays that come built into an app is essential to some of the apps we are working on. I was hoping to use MBTiles for this, and this seems like a great feature to integrate in.
This would indeed be a major breakthrough... having the comfort and slickness of MapKit, combined with the flexibility of offline mbtiles sources. Great!
MBTiles support would be great for my project ! MBXMapKit is a nice approach combining the SDK map kit and Mapbox. Good job team !
Still not able to get to this in the near term. But to lay out the strategy I was planning:
- Have a look inside
MBXMapKit.mat https://github.com/mapbox/mbxmapkit/blob/a9bc909dc52d76dd61839fc40e62d3efbd1dc189/MBXMapKit.m#L83-L103 for where the layer's min/max zooms and bounds are defined. This also relies on theregionthat is set in https://github.com/mapbox/mbxmapkit/blob/a9bc909dc52d76dd61839fc40e62d3efbd1dc189/MBXMapKit.m#L115-L130 - The actual tile URL formulation and fetching is done in https://github.com/mapbox/mbxmapkit/blob/a9bc909dc52d76dd61839fc40e62d3efbd1dc189/MBXMapKit.m#L155-L240 per the new MapKit API. For MBTiles,
URLForTilePath:would not be used and insteadloadTileAtPath:result:would just hit the SQLite directly for the tile defined bypath(i.e. the tilez/x/y). - Over in the SDK, you can see in https://github.com/mapbox/mapbox-ios-sdk/blob/1517baf35c30c2a64252a834e662d00e5e7158f9/MapView/Map/RMMBTilesSource.m all of the SQLite code (called via FMDB) that is used to open the MBTiles, get the min/max zoom and bounds, and pull out tile data. In this case, the class forms whole
UIImageobjects from it, whereasloadTileAtPath:result:in MapKit would just act on theNSDatadirectly. - There is probably some work to be done around the threading model based on how MapKit works vs. how the iOS SDK (via
CATiledLayer) does, but since the existing SQLite code uses theFMDatabaseQueueserial queue API for thread safety, a first cut should work pretty well.
I've had some success playing with offline tiles on the MapKit side (i.e. not involving MBXMapKit) by subclassing MKTileOverlay. Here are the main things you'll need to do to make the API work:
Get an MKMapView set up in a view controller that implements MKMapViewDelegate protocol. The view controller should be set as the MKMapView's delegate.
In the view controller, implement - (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay so that it will return a [[MKTileOverlayRenderer alloc] initWithTileOverlay:overlay] when it gets called with your subclassed MKTileOverlay as the overlay.
Subclass MKTileOverlay with the following important bits:
- (MKMapRect)boundingMapRect
{
// This works to test with, but you should figure out the actual bounding box
return MKMapRectWorld;
}
- (BOOL)isGeometryFlipped
{
// Default coordinate system is upside down relative to an MBTiles from TileMill, so flip it
return YES;
}
- (void)loadTileAtPath:(MKTileOverlayPath)path result:(void (^)(NSData *tileData, NSError *error))result
{
// Find your MBTiles file and open it with sqlite, FMDB, or whatever
// Figure out if path represents a valid tile in your MBTiles file
// Assuming it does, make this query to get the tile...
NSString *query = [NSString stringWithFormat:@"select tile_data from tiles where zoom_level = %ld and tile_column = %ld and tile_row = %ld",(long)path.z,(long)path.x,(long)path.y];
// Based on what comes back from your query, initialize these objects appropriately...
NSData *myTileData;
NSError *myErrorStatus;
// Invoke the argument block to complete the asynchronous load...
result(myTileData, myErrorStatus);
}
Back in your view controller, in viewDidLoad or someplace, do something kinda like this to add your overlay to the MKMapView:
_tileOverlay = [[MBTilesOverlay alloc] initWithURLTemplate:nil];
//_tileOverlay.canReplaceMapContent = YES;
[_mapView addOverlay:_tileOverlay];
Hi Justin,
I have some free time again, so I'm gonna take a swing at this. No promises, but I'll send you a pull request if things go well.
I like the approach over in https://github.com/mapbox/mbxmapkit/issues/20 of doing this with defines for the folks that really need it. Leaving this open until this is addressed and integrated either in #20 directly or the aggregate #38 pull.
The ability to use vector-mbtiles would be awesome, though I am not sure how easy this would be to implement within this confined project. As far as I understand, there would have to be some mechanism to render those on-the-fly then. Would something like this be feasible?
Would something like this be feasible?
Yes, but this is a separate from MBTiles proper -- MBTiles is more the transport medium than the rendering. We're looking into vector rendering now.
This can wait [edit: ... until after the 0.3.0 release]
Is "this can wait" referring to vector rendering, or MBTiles overall?
The ability to have offline maps bundled with an app seems to be a pretty central feature to MBXMapKit, at least how I envision using it.
Thoughts? Is there another more preferred way than MBTiles suggested?
@radven Ah, sorry for the overly terse comment there. What I had in mind was in the context of the huge pile of stuff that we're trying to merge into master from the issue79 branch (see PR #81).
MBTiles support is currently tagged as part of the 0.3.0 milestone, but it's totally not ready yet. There's some code sitting around in one of my old branches from February or so, but it needs a bunch of work. On the other hand, there's lots of stuff which is totally ready to go, including a brand new offline map downloader which I'd encourage you to take a look at.
@incanus and I are thinking that we can probably get a good 0.3.0 release out some time this week or next, but that wouldn't include MBTiles. So "this can wait" = "let's not hold up the 0.3.0 release for this feature".
@tkrajacic This is the MBTiles issue that I mentioned over in #104
I'll have a shot at it!
Just to see if I am going in a direction you like:
I think there should be a wrapper for the mbtiles file (just as there is for the MBXOfflineDatabase) for two reasons:
- Well, MBXOfflineDatabase already nicely does this and staying with good patterns is great.
- The proposed MBXMBTilesOverlay should not deal with sqlite internals imho. (Although one could argue, that this is its express purpose.
Further:
- The MBXMBTilesDatabase should be initialized with an
NSURLto the file (and so should the other already present classes as well) as this is the preferred way to refer to resources according to the Apple Developer Library
@tkrajacic I just committed a change (https://github.com/mapbox/mbxmapkit/commit/fa58498faf92ef773f8b5e4ca661f936be32c6ed) to the issue3-mbtiles branch which reflects my understanding of how you are suggesting mbtiles ought to work. If that's what you have in mind, I like it.
Absolutely! I was already implementing it that way. (Which now poses the problem: Do I now rebase from your branch to mine, or merge?)
Okay, cool. For branches, I would suggest that you do whatever is most convenient for you, so that when you're done you can submit a pull request against the issue3-mbtiles branch. I'm about out of time to spend thinking about mbtiles until probably the end of this week, or maybe next week, so there isn't any hurry.
We originally had the mbtiles feature planned as part of the 0.3.0 milestone, and that release will hopefully happen this month. I'm thinking now that mbtiles will probably end up in the 0.3.1 release, perhaps in June.
Mhm, do you want to keep the (NSData *)dataForURL:(NSURL *)url withError:(NSError **)error signature?
Wouldn't it make more sense, if the MBXMBTileOverlay would call something like (NSData *)dataForPath:(MKTileOverlayPath)path on its MBXMBTileDatabase as loadTileAtPath:... can then just forward the path?
~~I can't imagine a scenario where using a URL would make sense.~~
Maybe for switching out an mbtiles database with an MBXOfflineDatabase it could be useful to have this as an optional way of getting tiles?
Should the Project contain a demo file (.mbtiles)? I have added a small (~1.5MB) file to my Project for testing (quick export from Tilemill).
MBTiles support is working fine so far on my branch but there is a lot of duplication, as I have implemented a separate overlay because mbtiles files and mapbox maps do not share that much features, and most of the stuff in MBXRasterOverlay just makes no sense for an MBTiles file as far as I can tell.
Then again, @wsnook and @incanus you know how this should fit in, so I will gladly change it to your liking.
Should the Project contain a demo file (.mbtiles)?
Yes. There are also a couple of small mbtiles files in this folder of my old issue3+7 branch which I created for testing with.
@tkrajacic Taking a look at your code so far, it looks like you're making good progress.
I'm not sure I understand what the advantage is of having MBXMBTilesDatabase split out into a separate class from MBXMBTilesOverlay. The reason we followed that pattern with MBXOfflineMapDatabase is that the MBXRasterTileOverlay class needed to be separate from the MBXOfflineMapDownloader class, and they both needed to work with MBXOfflineMapDatabase objects.
The reason for having those three classes cooperate for offline maps is that it enables MBXMapKit to hide the complicated implementation details of the offline map downloader so developers don't have to worry about their own offline maps implementation. With MBTiles, TileMill does all the work of creating the .mbtiles files, and it's not much work for the application to manage those files on its own, so the situation is really different compared to offline maps.
It seems like you could just have something like - (id)initWithMBTilesURL:(NSURL *)mbtilesURL in MBXMBTilesOverlay and keep the header files simpler. The reason I mention this is that when we put something in a header file, it becomes a part of the public API, and then we have to support it. There are big advantages in terms of support and maintenance when we avoid creating any non-essential classes and when we avoid putting anything in the headers which could be put into the implementation files instead.
Thanks for taking the time to work on this.
Excellent points, I will refactor to your suggestions.
I like @property (nonatomic) BOOL shouldOverzoom;. Do you know about the overzoom code that I have in my old issue35 branch?
Sure, that's where I stole it from iirc. ;) For now, I mostly copied some old stuff in and started massaging the code.
General status update for anyone watching this issue: I haven't forgotten about MBTiles, but it's also behind a couple other things in my queue.
I don't think anyone would have thought that you'd have forgotten ;) We have seen all the progress on other projects which you probably had your part in!
I have also further enhanced my implementation of MBTiles support (not on GH yet) with asynchronous sqlite access and shared connection per overlay.
@tkrajacic It makes me a little nervous when you mention "asynchronous sqlite access and shared connection per overlay". Could you say a little more about that? I'm wondering if you've done any profiling to see if there is a performance gain? Also, do you feel like the new things you're working on help the code to be more readable?
As far as I could tell from the offline caching stuff, opening lots of sqlite connections and reading synchronously works very fast, and it doesn't create any noteworthy memory management issues.
I've deliberately tried to do sqlite code using a simplistic brute force approach to cut down on complexity of the code (sqlite c calls are challenging enough to read already) and to reduce the opportunities for threading related problems.
I know that the specific way I was calling libsqlite3 was thread safe, but from reading the sqlite3 documentation, I got the idea that other ways to call it might require a lot of additional safeguards of the sort included in FMDB.
hehe, no need to get nervous @wsnook ;)
Basically, I thought it made more sense to have every MBXMBTilesOverlay have it's own database connection that its methods would use. I'm not sure, that there is much performance gain from it but I think it is just simpler and easier to read. (Sadly I also lack Instruments experience to test this seriously)
I have modeled the loading of the tiles in the same way you implemented with offline databases in a method
- (void)asyncLoadMBTilesDataForPath:(MKTileOverlayPath)path
workerBlock:(void(^)(NSData *, NSError **))workerBlock
completionHandler:(void(^)(NSData *, NSError *))completionHandler
and the asynchronous loading is done on a dispatch queue that is also a property of every MBXMBTilesOverlay instance. Therefor reading is serialized but not on the main queue.
I'll try to update my fork tomorrow so you can have a look. Maybe you can think of a better implementation, then I'll gladly change/refactor it! I have tested my implementation with quite a lot of overlays in my own app and it worked nicely so far.
@tkrajacic Okay, cool. I'm looking forward to seeing what you come up with.