CARTO Mobile SDK supports several different types and levels of offline maps
Offline country-wide basemap packages managed by CARTO, consumed using PackageManager
Offline area basemap packages (e.g. cities or other custom areas), also using PackageManager
Persistently cached online map - connect to online map and SDK keeps offline cache
Tile pre-download - download online tiles for given area and zoom range, and cache on device
MBtiles file with your own data. You can bundle it with app, or download from your server
Bundled GeoJson with your own data
Offline package from CARTO account data
An offline map app setup could be composed as this for example:
Offline global basemaps
Creation and management of PackageManager, which handles offline package discovery, updates and downloads is covered in detail the next chapter. Once you have it, you should add CartoOfflineVectorTileLayer to the MapView, using PackageManager and map style for offline map layer as constructor parameters.
Note: If map package is not yet downloaded, then this layer will have no map. So you may want to add another online tiled layer with same style, which will be replaced once offline map is downloaded
PersistentCacheTileDataSource caches HTTP tiles to a persistent sqlite database file. If the tile already exists in the database, the request to the original DataSource is ignored. This can be applied for both raster and vector tiles. The original DataSource’s expired headers are taken into account as part of the processing.
This DataSource is used as “virtual datasource” - it you create it on top of another datasource, normally HTTPTileDataSource.
// 1. Create a Bing raster data source. Note: tiles start from level 1Stringurl="http://ecn.t3.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=471&mkt=en-US";TileDataSourcebaseRasterTileDataSource=newHTTPTileDataSource(1,19,url);// 2. Add persistent caching datasource, tiles will be stored locally on persistent storagePersistentCacheTileDataSourcecachedDataSource=newPersistentCacheTileDataSource(baseRasterTileDataSource,getExternalFilesDir(null)+"/mapcache.db");// 3. Create layer and add to mapTileLayerbaseLayer=newRasterTileLayer(cachedDataSource);mapView.getLayers().add(baseLayer);
1
2
3
4
5
6
7
8
9
10
11
12
13
// 1. Create a Bing raster data source. Note: tiles start from level 1, there is no single root tile!varurl="http://ecn.t3.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=471&mkt=en-US";varbaseRasterTileDataSource=newHTTPTileDataSource(1,19,url);// Add persistent caching datasource, tiles will be stored locally on persistent storage// fileDir must be a directory where files can be written - this is platform-specificvarcachedDataSource=newPersistentCacheTileDataSource(baseRasterTileDataSource,fileDir+"/mapcache.db");// 2. Create layer and add to mapvarbaseLayer=newRasterTileLayer(cachedDataSource);MapView.Layers.Add(baseLayer);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. Initialize a OSM raster data source from MapQuest Open TilesNSString*url=@"http://ecn.t3.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=471&mkt=en-US";NTHTTPTileDataSource*baseRasterTileDataSource=[[NTHTTPTileDataSourcealloc]initWithMinZoom:1maxZoom:19baseURL:url];// 2. Create persistent cache for the given data source NTPersistentCacheTileDataSource*cachedDataSource=[[NTPersistentCacheTileDataSourcealloc]initWithDataSource:baseRasterTileDataSourcedatabasePath:[NTAssetUtilscalculateWritablePath:@"mycache.db"]];// 3. Initialize a raster layer with the previous data sourceNTRasterTileLayer*baseLayer=[[NTRasterTileLayeralloc]initWithDataSource:cachedDataSource];// 4. Add the previous raster layer to the map[[self.mapViewgetLayers]add:baseLayer];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. Create a Bing raster data source. Note: tiles start from level 1, there is no single root tile!leturl="http://ecn.t3.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=471&mkt=en-US"letbaseRasterTileDataSource=NTHTTPTileDataSource(minZoom:1,maxZoom:19,baseURL:url)// 2. Add persistent caching datasource, tiles will be stored locally on persistent storageletpath=NTAssetUtils.calculateWritablePath("mapcache.db")letcachedDataSource=NTPersistentCacheTileDataSource(dataSource:baseRasterTileDataSource,databasePath:path)// 3. Create layer and add to mapletbaseLayer=NTRasterTileLayer(dataSource:cachedDataSource)// 4. Add the previous raster layer to the mapmapView?.getLayers()?.add(baseLayer)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. Create a Bing raster data source. Note: tiles start from level 1, there is no single root tile!valurl="http://ecn.t3.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=471&mkt=en-US"valbaseRasterTileDataSource=HTTPTileDataSource(1,19,url)// 2. Add persistent caching datasource, tiles will be stored locally on persistent storagevalpath=getExternalFilesDir(null).path+"/mapcache.db"valcachedDataSource=PersistentCacheTileDataSource(baseRasterTileDataSource,path)// 3. Create layer and add to mapvalbaseLayer=RasterTileLayer(cachedDataSource)// 4. Add the previous raster layer to the mapmapView?.layers?.add(baseLayer)
Tile pre-downloading
CARTO Mobile SDK also enables you to download needed map tiles from online to your device and keep them in your cache, indefinitely. The specified download location will always be available to you offline. Download is using persistent map cache PersistentCacheTileDataSource’s function startDownloadArea
Note: Most public map tile servers do not allow such bulk tile downloading, so this feature is meant to be used with your own server. Also check that the download would not be too big. You can check number of downloaded tiles with tile count estimator. We do not suggest let users to download larger areas than few thousand tiles.
Projectionprojection=contentView.mapView.getOptions().getBaseProjection();Stringurl="http://YOUR-SERVER/{z}/{x}/{y}.png";Stringpath=getExternalFilesDir(null).getAbsolutePath()+"/cache.db";// Approximately downtown Washington DCMapPosmin=projection.fromWgs84(newMapPos(-77.08,38.85));MapPosmax=projection.fromWgs84(newMapPos(-76.94,38.93));MapBoundsbounds=newMapBounds(min,max);// This source can be anything, even aero picture etc.,// using the most basic variant for this exampleHTTPTileDataSourcesource=newHTTPTileDataSource(0,18,url);PersistentCacheTileDataSourcecache=newPersistentCacheTileDataSource(source,path);// Only uses cached tiles, does not download any new tiles during zoomcache.setCacheOnlyMode(true);cache.startDownloadArea(bounds,0,10,newTileDownloadListener(){@OverridepublicvoidonDownloadProgress(floatprogress){}@OverridepublicvoidonDownloadCompleted(){}});RasterTileLayerlayer=newRasterTileLayer(cache);contentView.mapView.getLayers().add(layer);
Projectionprojection=ContentView.MapView.Options.BaseProjection;stringurl="http://YOUR-SERVER/{z}/{x}/{y}.png";stringpath=GetExternalFilesDir(null).AbsolutePath+"/cache.db";// Approximately downtown Washington DCMapPosmin=projection.FromWgs84(newMapPos(-77.08,38.85));MapPosmax=projection.FromWgs84(newMapPos(-76.94,38.93));varbounds=newMapBounds(min,max);// This source can be anything, even aero picture etc.,// using the most basic variant for this examplevarsource=newHTTPTileDataSource(0,18,url);varcache=newPersistentCacheTileDataSource(source,path);// Only uses cached tiles, does not download any new tiles during zoomcache.CacheOnlyMode=true;varlistener=newDownloadListener();cache.StartDownloadArea(bounds,0,10,listener);varlayer=newRasterTileLayer(cache);ContentView.MapView.Layers.Add(layer);/*
* DownloadListener implementation
*/publicclassDownloadListener:TileDownloadListener{publicoverridevoidOnDownloadProgress(floatprogress){}publicoverridevoidOnDownloadCompleted(){}}
NTProjection*projection=[[mapViewgetOptions]getBaseProjection];NSString*url=@"http://YOUR-SERVER/{z}/{x}/{y}.png";// Create folder for the database fileNSArray*paths=NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,NSUserDomainMask,YES);NSString*appSupportDir=[pathsobjectAtIndex:0];NSString*path=[appSupportDirstringByAppendingString:@"/cache.db"];// Approximately downtown Washington DCNTMapPos*min=[projectionfromWgs84:[[NTMapPosalloc]initWithX:-77.08y:38.85]];NTMapPos*max=[projectionfromWgs84:[[NTMapPosalloc]initWithX:-76.948y:38.93]];NTMapBounds*bounds=[[NTMapBoundsalloc]initWithMin:minmax:max];// This source can be anything, even aero picture etc.,// using the most basic variant for this exampleNTHTTPTileDataSource*source=[[NTHTTPTileDataSourcealloc]initWithMinZoom:0maxZoom:18baseURL:url];NTPersistentCacheTileDataSource*cache=[[NTPersistentCacheTileDataSourcealloc]initWithDataSource:sourcedatabasePath:path];// Only uses cached tiles, does not download any new tiles during zoom[cachesetCacheOnlyMode:YES];DownloadListener*listener=[[DownloadListeneralloc]init];[cachestartDownloadArea:boundsminZoom:0maxZoom:10tileDownloadListener:listener];NTRasterTileLayer*layer=[[NTRasterTileLayeralloc]initWithDataSource:cache];[[mapViewgetLayers]add:layer];/*
* DownloadListener implementation
*/@interfaceDownloadListener:NTTileDownloadListener@end@implementationDownloadListener-(void)onDownloadProgress:(float)progress{}-(void)onDownloadCompleted{}@end
letprojection=contentView.map.getOptions().getBaseProjection()leturl="http://YOUR-SERVER{z}/{x}/{y}.png"letdocumentDir=NSSearchPathForDirectoriesInDomains(.documentDirectory,.userDomainMask,true)[0]letpath=documentDir+"/cache.db"// Approximately downtown Washington DCletmin=projection?.fromWgs84(NTMapPos(x:-77.08,y:38.85))letmax=projection?.fromWgs84(NTMapPos(x:-76.94,y:38.93))letbounds=NTMapBounds(min:min,max:max)// This source can be anything, even aero picture etc.,// using the most basic variant for this exampleletsource=NTHTTPTileDataSource(minZoom:0,maxZoom:18,baseURL:url)letcache=NTPersistentCacheTileDataSource(dataSource:source,databasePath:path)// Only uses cached tiles, does not download any new tiles during zoomcache?.setCacheOnlyMode(true)letlistener=DownloadListener()cache?.startDownloadArea(bounds,minZoom:0,maxZoom:10,tileDownloadListener:listener)letlayer=NTRasterTileLayer(dataSource:cache)contentView.map.getLayers().add(layer)/*
* DownloadListener implementation
*/classDownloadListener:NTTileDownloadListener{overridefunconDownloadProgress(_progress:Float){}overridefunconDownloadCompleted(){}}
valprojection=contentView!!.map.options.baseProjectionvalurl="http://YOUR-SERVER/{z}/{x}/{y}.png"valpath=getExternalFilesDir(null).absolutePath+"/cache.db"// Approximately downtown Washington DCvalmin=projection.fromWgs84(MapPos(-77.08,38.85))valmax=projection.fromWgs84(MapPos(-76.94,38.93))valbounds=MapBounds(min,max)// This source can be anything, even aero picture etc.,// using the most basic variant for this examplevalsource=HTTPTileDataSource(0,18,url)valcache=PersistentCacheTileDataSource(source,path)// Only uses cached tiles, does not download any new tiles during zoomcache.isCacheOnlyMode=truecache.startDownloadArea(bounds,0,10,object:TileDownloadListener(){overridefunonDownloadProgress(progress:Float){print("Download progress: "+progress.toString())}overridefunonDownloadCompleted(){print("Download complete")}})vallayer=RasterTileLayer(cache)contentView!!.map.layers.add(layer)
MBTiles files
Following code samples assume that you have already mbtiles file in device file system, and know the file full path. How you exactly get it, from app bundle or download it from your server, depends on app.
You can use your favorite MBTiles file creator go convert your data to MBTiles, e.g. MBTiler.
Note: You cannot open mbtiles file directly from Android assets (i.e. APK file), instead you need to copy the file to file storage, and open it from there. On iOS this is not needed.
Vector maps always need proper style definitions. Style definition must match with specific data schema (i.e. layers and tags there), and if you have your custom data inside vector tiles, then the style must be compatible with that. Building suitable style asset is described for the case of customized basemap styles in SDK wiki “Creating custom basemap styles”. Basemap styling and overlay styling with vector tiles is technically same, so same style package format is used there.
You can also use CARTO Builder to define map styling using web interface , see chapter Offline CARTO map below how to get data to mobile offline from this.
In this sample we use mbtiles downloaded from openmaptiles.org, these are in openmaptiles schema. As also CARTO bundled vector maps use same schema, then the built-in CARTO_BASEMAP_STYLE_VOYAGER and other styles happen to be compatible with this, and can be used to render the data.
MBTilesTileDataSourcetileDataSource=newMBTilesTileDataSource("estonia.mbtiles");// Create tile decoder based on Voyager style and VectorTileLayerVectorTileDecodertileDecoder=CartoVectorTileLayer.createTileDecoder(CartoBaseMapStyle.CARTO_BASEMAP_STYLE_VOYAGER);VectorTileLayerofflineLayer=newVectorTileLayer(tileDataSource,tileDecoder);mapView.getLayers().add(offlineLayer);
1
2
3
4
5
6
7
8
9
vartileDataSource=newMBTilesTileDataSource("estonia.mbtiles");// Create tile decoder based on Voyager style and VectorTileLayervartileDecoder=CartoVectorTileLayer.CreateTileDecoder(CartoBaseMapStyle.CartoBasemapStyleVoyager);varofflineLayer=newVectorTileLayer(tileDataSource,tileDecoder);mapView.Layers.Add(offlineLayer);
1
2
3
4
5
6
7
8
9
10
11
NSString*path=[[NSBundlemainBundle]pathForResource:@"estonia"ofType:@"mbtiles"];NTMBTilesTileDataSource*tileDataSource=[[NTMBTilesTileDataSourcealloc]initWithPath:path];// Create tile decoder based on Voyager style and VectorTileLayerNTMBVectorTileDecoder*tileDecoder=[NTCartoVectorTileLayercreateTileDecoder:NT_CARTO_BASEMAP_STYLE_VOYAGER];NTVectorTileLayer*offlineLayer=[[NTVectorTileLayeralloc]initWithDataSource:tileDataSourcedecoder:tileDecoder];[[mapViewgetLayers]add:offlineLayer];
1
2
3
4
5
6
7
8
9
lettileDataSource=NTMBTilesTileDataSource(path:Bundle.main.path(forResource:"estonia",ofType:"mbtiles"))// Create tile decoder based on Voyager style and VectorTileLayerlettileDecoder=NTCartoVectorTileLayer.createTileDecoder(CartoBaseMapStyle.CARTO_BASEMAP_STYLE_VOYAGER)letofflineLayer=NTVectorTileLayer(tileDataSource,tileDecoder)mapView?.layers?.add(offlineLayer)
1
2
3
4
5
6
7
8
9
10
valtileDataSource=MBTilesTileDataSource("estonia.mbtiles")// Create tile decoder based on Voyager style and VectorTileLayervaltileDecoder=CartoVectorTileLayer.createTileDecoder(CartoBaseMapStyle.CARTO_BASEMAP_STYLE_VOYAGER)valofflineLayer=VectorTileLayer(tileDataSource,tileDecoder)mapView?.layers?.add(offlineLayer)
GeoJson
GeoJSON is a format for encoding a variety of geographic data structures. GeoJSON supports the following geometry types: Point, LineString, Polygon, MultiPoint, MultiLineString, and MultiPolygon. Geometric objects with additional properties are Feature objects. Sets of features are contained by FeatureCollection objects.
The following example displays how you can load GeoJSON from bundled assets and render it on the map. Make sure you have the loaded .geojson file as a bundled Asset (Android) or Resource (iOS), you can get a copy from here
// Initialize a local vector data sourcefinalProjectionprojection=mapView.getOptions().getBaseProjection();finalLocalVectorDataSourcesource=newLocalVectorDataSource(projection);VectorLayerlayer=newVectorLayer(source);mapView.getLayers().add(layer);// As the file to load is rather large, we do not want to block our main threadthread=newThread(newRunnable(){@Overridepublicvoidrun(){// Create a basic styleMarkerStylestyle=newMarkerStyleBuilder().buildStyle();// Read GeoJSON, parse it using SDK GeoJSON parserGeoJSONGeometryReaderreader=newGeoJSONGeometryReader();// Set target projection to base (mercator)reader.setTargetProjection(projection);StringfileName="cities15000.geojson";Stringjson;try{InputStreamis=getAssets().open(fileName);intsize=is.available();byte[]buffer=newbyte[size];is.read(buffer);is.close();json=newString(buffer,"UTF-8");}catch(IOExceptionex){return;}// Read features from local assetFeatureCollectionfeatures=reader.readFeatureCollection(json);VectorElementVectorelements=newVectorElementVector();for(inti=0;i<features.getFeatureCount();i++){// This data set features point geometry,// however, it can also be LineGeometry or PolygonGeometryPointGeometrygeometry=(PointGeometry)features.getFeature(i).getGeometry();elements.add(newMarker(geometry,style));}source.addAll(elements);}});thread.start();
// Read json from assets and add to mapstringjson;using(System.IO.StreamReadersr=newSystem.IO.StreamReader(Assets.Open("cities15000.geojson"))){json=sr.ReadToEnd();}// Initialize a local vector data sourcevarprojection=MapView.Options.BaseProjection;varsource=newLocalVectorDataSource(projection);VectorLayerlayer=newVectorLayer(source);newSystem.Threading.Thread((obj)=>{// Create basic style. Use the MarkerStyleBuilder to modify your markersMarkerStylestyle=newMarkerStyleBuilder().BuildStyle();// Read GeoJSON, parse it using SDK GeoJSON parserGeoJSONGeometryReaderreader=newGeoJSONGeometryReader();// Set target projection to base (mercator)reader.TargetProjection=projection;// Read features from local assetFeatureCollectionfeatures=reader.ReadFeatureCollection(json);VectorElementVectorelements=newVectorElementVector();for(inti=0;i<features.FeatureCount;i++){// This data set features point geometry,// however, it can also be LineGeometry or PolygonGeometryPointGeometrygeometry=(PointGeometry)features.GetFeature(i).Geometry;elements.Add(newMarker(geometry,style));}source.AddAll(elements);// Add the clustered vector layer to the mapMapView.Layers.Add(layer);}).Start();
// Initialize a local vector data sourceNTProjection*projection=[[self.mapViewgetOptions]getBaseProjection];NTLocalVectorDataSource*source=[[NTLocalVectorDataSourcealloc]initWithProjection:projection];NTVectorLayer*layer=[[NTVectorLayeralloc]initWithDataSource:source];[[[selfmapView]getLayers]add:layer];dispatch_queue_tqueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0ul);dispatch_async(queue,^{// Read .geojsonNSString*fullpath=[[NSBundlemainBundle]pathForResource:@"cities15000"ofType:@"geojson"];NSString*json=[NSStringstringWithContentsOfFile:fullpathencoding:NSUTF8StringEncodingerror:nil];// .geojson parsingNTGeoJSONGeometryReader*geoJsonReader=[[NTGeoJSONGeometryReaderalloc]init];[geoJsonReadersetTargetProjection:projection];NTFeatureCollection*features=[geoJsonReaderreadFeatureCollection:json];// Initialize basic style, as it will later be overriddenNTMarkerStyle*style=[[[NTMarkerStyleBuilderalloc]init]buildStyle];NTVectorElementVector*elements=[[NTVectorElementVectoralloc]init];for(inti=0;i<[featuresgetFeatureCount];i++){NTPointGeometry*geometry=(NTPointGeometry*)[[featuresgetFeature:i]getGeometry];NTMarker*marker=[[NTMarkeralloc]initWithGeometry:geometrystyle:style];[elementsadd:marker];}// To avoid flickering, add all the sources[sourceaddAll:elements];});
letprojection=contentView.map.getOptions().getBaseProjection()letsource=NTLocalVectorDataSource(projection:projection)letlayer=NTVectorLayer(dataSource:source)contentView.map.getLayers().add(layer)DispatchQueue.global().async{letpath=Bundle.main.path(forResource:"cities15000",ofType:"geojson")guardletjson=try?NSString(contentsOfFile:path!,encoding:String.Encoding.utf8.rawValue)else{return}// Create default styleletmBuilder=NTMarkerStyleBuilder()letstyle=mBuilder?.buildStyle()// Read GeoJSON, parse it using SDK GeoJSON parserletreader=NTGeoJSONGeometryReader()reader?.setTargetProjection(self.contentView.map.getOptions().getBaseProjection())letfeatures=reader?.readFeatureCollection(jsonasString!)letelements=NTVectorElementVector()lettotal=Int((features?.getFeatureCount())!)foriinstride(from:0,to:total,by:1){// This data set features point geometry, however, it can also be LineGeometry or PolygonGeometryletgeometry=features?.getFeature(Int32(i)).getGeometry()as?NTPointGeometryelements?.add(NTMarker(geometry:geometry,style:style))}DispatchQueue.main.async(execute:{source?.addAll(elements)})}
valsource=LocalVectorDataSource(contentView!!.map.options.baseProjection)vallayer=VectorLayer(source)contentView!!.map.layers.add(layer)// Kotlin Anko library: https://github.com/Kotlin/ankodoAsync{valfilename="cities15000.geojson"valstream=assets.open(filename)valsize=stream.available()valbuffer=ByteArray(size)stream.read(buffer)stream.close()valjson=String(buffer,charset("UTF-8"))// Basic style buildervalmBuilder=MarkerStyleBuilder()valstyle=mBuilder.buildStyle()// Read GeoJSON, parse it using SDK GeoJSON parservalreader=GeoJSONGeometryReader()reader.targetProjection=contentView?.projectionvalfeatures=reader.readFeatureCollection(json)valelements=VectorElementVector()valtotal=features.featureCountfor(iin0..total-1){// This data set features point geometry, however, it can also be LineGeometry or PolygonGeometryvalgeometry=features.getFeature(i).geometryasPointGeometryelements.add(Marker(geometry,style))}source.addAll(elements)}
Offline CARTO map
You can create offline map package is via CARTO platform, from a dataset in BUILDER:
Upload your data to CARTO, create a new Map with CARTO Builder, define map layer styling
Use the Mobile Tile Packager tool to create the offline map data package from the map, as MBTiles file.
Add the package file to the mobile app - you can have your app download it from your server, or add it as bundled asset to your app.
Add the map to MapView, as a VectorTileLayer, from MBTilesTileDataSource and apply CartoCSS as String for styling. You can take and copy-paste CartoCSS from BUILDER’s Layer Styling tab and “CARTOCSS” view: check the toggle in bottom of the screen.
This method enables you to create two in one: optimized vector tiles and suitable CartoCSS styling for your map.
Note: If you use text labels, then you need to bundle and add Font package .zip file to the app and TileDecoder, as in sample below. You can get one from here, copy it to your app bundled assets.
// Below "text-placement: nutibillboard;" does not work in BUILDER web, it is special placement for 3D mobile mapsStringcartoCss="#offlinepackages {\n"+" polygon-fill: #374C70;\n"+" polygon-opacity: 0.9;\n"+" polygon-gamma: 0.5;\n"+" ::outline {"+" line-color: #FFF;\n"+"}"+"#offlinepackages::labels {\n"+" text-name: [package_id];\n"+" text-face-name: 'DejaVu Sans Book';\n"+" text-size: 10;\n"+" text-fill: #130505;\n"+" text-label-position-tolerance: 0;\n"+" text-halo-radius: 1;\n"+" text-halo-fill: #dee3e7;\n"+" text-dy: -10;\n"+" text-allow-overlap: false;\n"+" text-placement: nutibillboard;\n"+" text-placement-type: dummy;\n"+"}";BinaryDatastyleAsset=AssetUtils.loadAsset("carto-fonts.zip");ZippedAssetPackageassetPackage=newZippedAssetPackage(styleAsset);MBVectorTileDecoderdecoder=newMBVectorTileDecoder(newCartoCSSStyleSet(cartoCss,assetPackage));MBTilesTileDataSourcembDataSource=createMbtilesDataSourceFromAsset(mbTileFile);VectorTileLayermbLayer=newVectorTileLayer(mbDataSource,decoder);mapView.getLayers().add(mbLayer);