IOS: use offline maps - ios

in my app I want to use offline maps and with this maps use gpx files to have a route. I found openstreetmap to do it, but is there some better services? (the best solution with level curves)
thanks

Offline maps require a bit of experience in iOS since there aren't many projects and examples out there.
However, you got one project called Route-Me which could give you a starting point.
We used it to develop Metro Valencia Offline which successfully made it to the App Store.
I wrote a small tutorial on how to add Route-Me into your project.
Basically you can use any maps feed you may want (OpenStreetMaps is one of them and also the one we used). What you'll have to do is:
Download the map tiles ( https://metacpan.org/pod/Geo::OSM::Tiles )
Put them into a SQLite database ( http://shikii.net/blog/downloads/map2sqlite-bin.zip )
Modify Route-Me for the tiles to be fed from the database instead of from OpenStreetMaps website

You can test also: skobbler's/Telenav: http://developer.skobbler.com/support#download it's based on OpenStreetMap and they got full offline capabilities (GPX tracks navigation, map rendering, routing & rerouting)

There is also Nutiteq Maps SDK: http://developer.nutiteq.com , offline maps for iOS and Android, supports Xamarin IDE (C#) and native languages (Java, ObjectiveC, Swift). It is not so much routing and navigation (as e.g. Skobbler), but more for focused to complex maps with interactive layers (points, lines and polygons on top of maps). One advantage is that you can use your own base map sources (in-house, 3rd parties), not only OpenStreetMap what SDK itself provides.
Disclaimer: I'm developer of it.

I used default map from MapKit and a subclass of MKTileOverlay to be able to save downloaded tiles and return already cached tiles without downloading them.
1) Change source for your default map from MapKit and use a subclass of MKTileOverlay (Used "open street map" here)
- (void)viewDidLoad{
[super viewDidLoad];
static NSString * const template = #"http://tile.openstreetmap.org/{z}/{x}/{y}.png";
VHTileOverlay *overlay = [[VHTileOverlay alloc] initWithURLTemplate:template];
overlay.canReplaceMapContent = YES;
[self.mapView addOverlay:overlay level:MKOverlayLevelAboveLabels];
}
2) subclass from MKTileOverlay
#interface VHTileOverlay() // MKTileOverlay subclass
#property (nonatomic, strong) NSOperationQueue *operationQueue;
#end
#implementation VHTileOverlay
-(instancetype)initWithURLTemplate:(NSString *)URLTemplate{
self = [super initWithURLTemplate:URLTemplate];
if(self){
self.directoryPath = cachePath;
self.operationQueue = [NSOperationQueue new];
}
return self;
}
-(NSURL *)URLForTilePath:(MKTileOverlayPath)path {
return [NSURL URLWithString:[NSString stringWithFormat:#"http://tile.openstreetmap.org/%ld/%ld/%ld.png", (long)path.z, (long)path.x, (long)path.y]];
}
-(void)loadTileAtPath:(MKTileOverlayPath)path
result:(void (^)(NSData *data, NSError *error))result
{
if (!result) {
return;
}
NSString *pathToFilfe = [[self URLForTilePath:path] absoluteString];
pathToFilfe = [pathToFilfe stringByReplacingOccurrencesOfString:#"/" withString:#"|"];
// #"/" - those are not approriate for file's url...
NSData *cachedData = [self loadFileWithName:pathToFilfe];
if (cachedData) {
result(cachedData, nil);
} else {
NSURLRequest *request = [NSURLRequest requestWithURL:[self URLForTilePath:path]];
__block VHTileOverlay *weakSelf = self;
[NSURLConnection sendAsynchronousRequest:request
queue:self.operationQueue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSLog(#"%#",[weakSelf URLForTilePath:path]);
if(data){
[self saveFileWithName:[[weakSelf URLForTilePath:path] absoluteString] imageData:data];
}
result(data, connectionError);
}];
}
}
-(NSString *)pathToImageWithName:(NSString *)fileName
{
NSString *imageFilePath = [[OfflineMapCache sharedObject].cachePath stringByAppendingPathComponent:fileName];
return imageFilePath;
}
- (NSData *)loadFileWithName:(NSString *)fileName
{
NSString *imagePath = [self pathToImageWithName:fileName];
NSData *data = [[NSData alloc] initWithContentsOfFile:imagePath];
return data;
}
- (void)saveFileWithName:(NSString *)fileName imageData:(NSData *)imageData
{
// fileName = [fileName stringByReplacingOccurrencesOfString:#"/" withString:#"|"];
// NSString *imagePath = [self pathToImageWithName:fileName];
// [imageData writeToFile:imagePath atomically:YES];
}
Uncomment "saveFileWithName" and run it on simulator. You can also add NSLog(fileName) to know where to get all tiles you need.
(Simulator cache is in Users/YOU/Library/Developer/CoreSimulator/Devices/...
And Library is a hidden directory)
After you cached all you need just put in your app's bundle (just like an any other image, if you want from box cached map).
And tell your
- (void)loadTileAtPath:(MKTileOverlayPath)path
result:(void (^)(NSData *data, NSError *error))result
to get tiles from bundle.
So now I can install my app, turn wi-fi off and I'll get those maps anyway.

Related

iOS / MapKit cache management issue with MKTileOverlay and PINCache library

I am using MapKit in order to create satellite and radar animation by adding MKTileOverlay over the mapView.
With an UISlider and a PlayButton I was able to create an animation, like a GIF by playing with the alpha of the MKOverlayRenderer (setting them to 0 or 0.75 according to the position of my slider).
The animation is quite smooth, all my satellite and radar tiles are loaded properly over the mapView.
I am encountering one issue with the cache management.
I realized that MapKit didn't use cache for my tileOverlay that's why I used the library PINCache in order to save my tiles so that it doesn't request and download the images each time I'm playing the animation.
My implementation :
I override the method URLForTilePath in order to build my URL to get my tile images.
- (NSURL *)URLForTilePath:(MKTileOverlayPath)path{
double latMin, latMax, longMin, longMax;
path.contentScaleFactor = 1.0;
NSMutableArray *result = [[NSMutableArray alloc] init];
result = getBBoxForCoordinates((int)path.x, (int)path.y, (int)path.z);
longMin = [[result objectAtIndex:0] doubleValue];
latMin = [[result objectAtIndex:1] doubleValue];
longMax = [[result objectAtIndex:2] doubleValue];
latMax = [[result objectAtIndex:3] doubleValue];
NSString *finalURL = self.url;
finalURL = [finalURL stringByReplacingOccurrencesOfString:#"DATE"
withString:_date];
NSString *bbox = [NSString stringWithFormat:#"bbox=%f,%f,%f,%f", longMin, latMin, longMax, latMax];
finalURL = [finalURL stringByReplacingOccurrencesOfString:#"BBOX"
withString:bbox];
return [NSURL URLWithString:finalURL];
}
And the key method that will call URLForTilePath is my implementation of loadTileAtPath :
- (void)loadTileAtPath:(MKTileOverlayPath)path
result:(void (^)(NSData *data, NSError *error))result
{
if (!result)
{
return;
}
NSString *str = self.isRadar == true ? [NSString stringWithFormat:#"Radar%#", self.date] : [NSString stringWithFormat:#"Satellite%#", self.date];
NSData *cachedData = [[PINCache sharedCache] objectForKey:str];
if (cachedData)
{
result(cachedData, nil);
}
else
{
NSURLRequest *request = [NSURLRequest requestWithURL:[self URLForTilePath:path]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSString *str = self.isRadar == true ? [NSString stringWithFormat:#"Radar%#", self.date] : [NSString stringWithFormat:#"Satellite%#", self.date];
[[PINCache sharedCache] setObject:data forKey:str block:nil];
result(data, connectionError);
}];
}
}
Basically what I'm trying to achieve is :
Check if I have cached Data, if so then get the object.
If not, I make a request in order to download the tile with the URL given by URLForTilePath.
I then set the Object to the Cache.
The string str is my Key for the cache management
I have 2 important values in order to sort and differentiate the tiles, the type (Radar or Satellite, different image, different URL), and the Date.
I can see that the cache management is working, the Overlays are rendering way faster but the main issue I'm encountering is that it doesn't load and build the tiles at the according coordinate.
My mapView is like a puzzle of the world wrongly built.
With my piece of code, can you see something wrong I made ?
I couldn't find the reason to this memory/cache management issues with mapKit.
I decided to try with GoogleMap, it tooks me less than 2 hours to implement it, and the results are amazing.
I wanted to respect the Apple community by using Apple solution, but Google Map provided me so much more performances.

iOS web app as native app

I'm primarily a web developer and I've been given the last minute task of throwing together an iOS application for a conference we're hosting.
We have the web app for the conference built and it is hosted on our web server. Following the tutorial here I've managed to make a very simple application which will display display the content in a UIWebView.
Our other developer has built something for android which will cache the pages and will check a .txt file on the server to see if the content has changed. If the content has changed, new content will be fetched. (conference schedule is very subject to change).
I would like to implement a similar functionality in my iOS application. Is there a simple way to go about doing this?
This is what my ViewController.m looks like:
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
#synthesize appView;
- (void)viewDidLoad
{
[super viewDidLoad];
NSURL *path = [NSURL URLWithString:[NSString stringWithFormat:#"http://ict.uiuc.edu/railroad/GLXS/app/index.html"]];
NSURLRequest *request = [NSURLRequest requestWithURL:path];
[appView loadRequest:request];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
If i am not wrong the only thing u need to perform is to get web view refreshed when an edit is perform on remote .txt file. in my view You can download the file whenever your app is run for the first time and later on set timer that will check remote file attributes in an interval(say 1min or one sec).
Here is sample code that will trigger and return true if file is modified.
//here is sample code that will return true if file is modified
//url:your remote file url. filePathe: local file that you downloade when you app is run for first time
+ (BOOL)isServerURL:(NSString *)url NewerThanLocalFile:(NSString *)filePath {
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
NSDate *date = [attributes fileModificationDate];
NSString *lastModifiedString = nil;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString: url] cachePolicy:NSURLCacheStorageAllowed timeoutInterval:2];
[request setHTTPMethod:#"HEAD"];
NSHTTPURLResponse *response;
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error: NULL];
if ([response respondsToSelector:#selector(allHeaderFields)]) {
lastModifiedString = [[response allHeaderFields] objectForKey:#"Last-Modified"];
}
NSDate *lastModifiedServer = nil;
#try {
//set time format as required
NSDateFormatter *df = [[NSDateFormatter alloc] init];
df.dateFormat = #"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
df.locale = [[NSLocale alloc] initWithLocaleIdentifier:#"en_US"];
df.timeZone = [NSTimeZone timeZoneWithAbbreviation:#"GMT"];
lastModifiedServer = [df dateFromString:lastModifiedString];
}
#catch (NSException * e) {
NSLog(#"Error parsing last modified date: %# - %#", lastModifiedString, [e description]);
}
if ([date laterDate:lastModifiedServer] == lastModifiedServer)
{
return YES;
} else {
return NO;
}
} else {
return YES;
}
}
If timer returns YES refresh your web-view.Hope i am clear.
You've essentially created a very simple hybrid application, i.e. one that's part native code and part web content. There are a number of frameworks out there that make this kind of thing a lot easier. The best known is PhoneGap, the open source version of which is Apache Cordova. You can use Cordova as it comes to build your app, or you can look at the source and use the same techniques in your own code.
Since your question seems to relate to caching content, you might look into HTML5's application cache, which should work fine with or without PhoneGap.
Have a look at How to cache images and html files in PhoneGap and Download images and save locally on iPhone Phonegap app for more information.
You can also check periodically if .txt file on server is changed if so then reload the website using following code
[appView reload];
or
you can put a refresh button on top of the webview in a corner and you can check it on demand

Is there any way to bundle map data with an iOS app?

I've got a client interested in an iOS app, but one of the requirements is an offline map. Unfortunately, both Google and iOS maps require an active internet connection to download data. Once you've opened the app up once, I think you can trigger a cache for an area, but you still require that connection.
Is there any way to bundle the actual map data with the app, or would I need to reinvent the wheel by writing my own map code?
If you app is iOS 7+, you have some flexibility by using MKTileOverlay. It allows you to put your own tile set on top of a normal Apple map.
By creating a MKTileOverlay subclass and overload loadTileAtPath: you could check your bundle for the initial state, then maybe a separate cache for tiles loaded when there was an internet connection, and finally use the URL to load the tile when all else fails.
- (void)loadTileAtPath:(MKTileOverlayPath)path
result:(void (^)(NSData *data, NSError *error))result
{
if (!result) {
return;
}
NSString *filePath = ...; // Predetermined filename based off of MKTileOverlayPath
NSData *initialData = [NSData dataWithContentsOfFile:filePath];
NSData *cachedData = [self.cache objectForKey:[self URLForTilePath:path]];
if (initialData) {
result(initialData, nil);
} else if (cachedData) {
result(cachedData, nil);
} else {
NSURLRequest *request = [NSURLRequest requestWithURL:[self URLForTilePath:path]];
[NSURLConnection sendAsynchronousRequest:request queue:self.operationQueue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
result(data, connectionError);
}];
}
}
More info and some examples here
I got this issue before and the only way I found was by using a third party map tiles provider called MapBox. They provide an API for iOS and other platforms as well. You can even customize your own maps by using TileMill. The former even allows you to export the map tiles as an MBTiles file which provides a way of storing millions of tiles in a single SQLite database making it possible to store and transfer web maps in a single file.

Offline MapKit solution for iOS

Quick question for you. I have an app that I'm working on that will require the use of maps, but will not have a network connection. I have seen others ask similar questions about this, but my requirements are a slight bit different, so I wanted to post a new question.
Here are my requirements for the app.
Allow pinch/zoom of a map with definable annotations
Map must be available offline
Map should be restricted to a certain geo area
App does not have to pass Apple inspection. This is an enterprise app that will serve as a kiosk.
So, is there any framework or method of caching map tiles that anyone can suggest?
Thanks go out in advance.
I used default map from MapKit and a subclass of MKTileOverlay to be able to save downloaded tiles and return already cached tiles without downloading them.
1) Change source for your default map from MapKit and use a subclass of MKTileOverlay (Used "open street map" here)
- (void)viewDidLoad{
[super viewDidLoad];
static NSString * const template = #"http://tile.openstreetmap.org/{z}/{x}/{y}.png";
VHTileOverlay *overlay = [[VHTileOverlay alloc] initWithURLTemplate:template];
overlay.canReplaceMapContent = YES;
[self.mapView addOverlay:overlay level:MKOverlayLevelAboveLabels];
}
2) subclass from MKTileOverlay
#interface VHTileOverlay() // MKTileOverlay subclass
#property (nonatomic, strong) NSOperationQueue *operationQueue;
#end
#implementation VHTileOverlay
-(instancetype)initWithURLTemplate:(NSString *)URLTemplate{
self = [super initWithURLTemplate:URLTemplate];
if(self){
self.directoryPath = cachePath;
self.operationQueue = [NSOperationQueue new];
}
return self;
}
-(NSURL *)URLForTilePath:(MKTileOverlayPath)path {
return [NSURL URLWithString:[NSString stringWithFormat:#"http://tile.openstreetmap.org/%ld/%ld/%ld.png", (long)path.z, (long)path.x, (long)path.y]];
}
-(void)loadTileAtPath:(MKTileOverlayPath)path
result:(void (^)(NSData *data, NSError *error))result
{
if (!result) {
return;
}
NSString *pathToFilfe = [[self URLForTilePath:path] absoluteString];
pathToFilfe = [pathToFilfe stringByReplacingOccurrencesOfString:#"/" withString:#"|"];
// #"/" - those are not approriate for file's url...
NSData *cachedData = [self loadFileWithName:pathToFilfe];
if (cachedData) {
result(cachedData, nil);
} else {
NSURLRequest *request = [NSURLRequest requestWithURL:[self URLForTilePath:path]];
__block VHTileOverlay *weakSelf = self;
[NSURLConnection sendAsynchronousRequest:request
queue:self.operationQueue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSLog(#"%#",[weakSelf URLForTilePath:path]);
if(data){
[self saveFileWithName:[[weakSelf URLForTilePath:path] absoluteString] imageData:data];
}
result(data, connectionError);
}];
}
}
-(NSString *)pathToImageWithName:(NSString *)fileName
{
NSString *imageFilePath = [[OfflineMapCache sharedObject].cachePath stringByAppendingPathComponent:fileName];
return imageFilePath;
}
- (NSData *)loadFileWithName:(NSString *)fileName
{
NSString *imagePath = [self pathToImageWithName:fileName];
NSData *data = [[NSData alloc] initWithContentsOfFile:imagePath];
return data;
}
- (void)saveFileWithName:(NSString *)fileName imageData:(NSData *)imageData
{
// fileName = [fileName stringByReplacingOccurrencesOfString:#"/" withString:#"|"];
// NSString *imagePath = [self pathToImageWithName:fileName];
// [imageData writeToFile:imagePath atomically:YES];
}
Uncomment "saveFileWithName" and run it on simulator. You can also add NSLog(fileName) to know where to get all tiles you need. (Simulator cache is in Users/YOU/Library/Developer/CoreSimulator/Devices/... And Library is a hidden directory)
After you cached all you need just put in your app's bundle (just like an any other image, if you want from box cached map). And tell your
- (void)loadTileAtPath:(MKTileOverlayPath)path
result:(void (^)(NSData *data, NSError *error))result
to get tiles from bundle.
So now I can install my app, turn wi-fi off and I'll get those maps anyway.
Try using http://mapbox.com/ which has both an iOS SDK and an app for constructing offline maps.
Check section 10.3 of the Google Maps Terms of Service and you'll find that you're not allowed to store any Google Maps content. That means that you'll need to provide not only your own map, but also replace the handy MapKit functionality. You really can't use MapKit at all for offline applications, as far as I can tell.
Unfortunately MapKit Framework doesn't support Offline Map Access. You Should prefer MapBox iOS
SDK (MapBox iOS SDK is a toolset for building maps applications which support offline caching policy, zooming limits, retina display behavior, starting coordinate, and map view dragging deceleration etc....
Find the example link
Happy Coding
See the skobbler/Telenav SDK - it's based on OpenStreetMap and is an (almost) full stack replacement to mapkit (map rendering, routing & turn by turn navigation) with integrated support for offline maps

Table view with image caching and "pull to update" option

Almost every ios app now has something like "Feed" option.
Programming that usually includes fetching images from the web, caching them, handling pages, "pull to update option", etc. - all the standart stuff.
But looks like there is no standart solution for that?
I tried "three 20" - really big, complicated library with many modules. It really lacks good documentation! And it also had "slowdowns" while fetching images from cache.
Maybe I should use different small libraries for each small task separately? Like HJCache, EGO, etc.
Or is it better to write everything from scratch without any libraries?
Please give me advice on best practices here, i am really stuck now.
This one is very easy to drop in for pull to refresh.
For image loading, I wrote the following category for UIImageView:
// .h
#interface UIImageView (UIImageView_Load)
- (void)loadFrom:(NSURL *)url completion:(void (^)(UIImage *))completion;
#end
// .m
#import "UIImageView+Load.h"
#import <QuartzCore/QuartzCore.h>
#implementation UIImageView (UIImageView_Load)
- (void)loadFrom:(NSURL *)url completion:(void (^)(UIImage *))completion {
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (data) {
self.image = [UIImage imageWithData:data];
if (completion) completion(self.image);
}
}];
}
#end
// To use it when building a cell
//...
MyModelObject *myModelObject = [self.myModel objectAtIndex:indexPath.row];
if (myModelObject.image) {
cell.imageView.image = myModelObject.image;
} else {
NSURL *url = [NSURL urlWithString:myModelObject.imageUrl];
[cell.imageView loadFrom:url completion:^(UIImage *image) {
// cache it in the model
myModelObject.image = image;
cell.imageView.image = image;
}];
}
// ...
I'm a fan of Leah Culver's Pull to Refresh library, or this STableViewController which handles pull-to-refresh as well as endless scrolling downward.
For image loading, try SDWebImage from the DailyMotion app.

Resources