Offline Maps

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

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

  CartoOfflineVectorTileLayer layer = new CartoOfflineVectorTileLayer(cartoPackageManager, CartoBaseMapStyle.CARTO_BASEMAP_STYLE_VOYAGER);
  mapView.getLayers().add(layer);
  

Cached online tiled map

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 1
    String url = "http://ecn.t3.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=471&mkt=en-US";
    TileDataSource baseRasterTileDataSource = new HTTPTileDataSource(1, 19, url);

    // 2. Add persistent caching datasource, tiles will be stored locally on persistent storage
    PersistentCacheTileDataSource cachedDataSource = new PersistentCacheTileDataSource(baseRasterTileDataSource, getExternalFilesDir(null) + "/mapcache.db");

    // 3. Create layer and add to map
    TileLayer baseLayer = new RasterTileLayer(cachedDataSource);
    mapView.getLayers().add(baseLayer);

    
    // 1. Create a Bing raster data source. Note: tiles start from level 1, there is no single root tile!
    var url = "http://ecn.t3.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=471&mkt=en-US";
    var baseRasterTileDataSource = new HTTPTileDataSource(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-specific
    var cachedDataSource = new PersistentCacheTileDataSource(baseRasterTileDataSource, fileDir + "/mapcache.db");

    // 2. Create layer and add to map
    var baseLayer = new RasterTileLayer(cachedDataSource);
    MapView.Layers.Add(baseLayer);

    
    // 1. Initialize a OSM raster data source from MapQuest Open Tiles
    NSString* url = @"http://ecn.t3.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=471&mkt=en-US";
    NTHTTPTileDataSource* baseRasterTileDataSource = [[NTHTTPTileDataSource alloc] initWithMinZoom:1 maxZoom:19 baseURL: url];

    // 2. Create persistent cache for the given data source  
    NTPersistentCacheTileDataSource* cachedDataSource = [[NTPersistentCacheTileDataSource alloc] initWithDataSource:baseRasterTileDataSource databasePath:[NTAssetUtils calculateWritablePath:@"mycache.db"]];

    // 3. Initialize a raster layer with the previous data source
    NTRasterTileLayer* baseLayer = [[NTRasterTileLayer alloc] initWithDataSource:cachedDataSource];

    // 4. Add the previous raster layer to the map
    [[self.mapView getLayers] add:baseLayer];

    
    // 1. Create a Bing raster data source. Note: tiles start from level 1, there is no single root tile!
    let url = "http://ecn.t3.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=471&mkt=en-US"
    let baseRasterTileDataSource = NTHTTPTileDataSource(minZoom: 1, maxZoom: 19, baseURL: url)

    // 2. Add persistent caching datasource, tiles will be stored locally on persistent storage
    let path = NTAssetUtils.calculateWritablePath("mapcache.db")
    let cachedDataSource = NTPersistentCacheTileDataSource(dataSource: baseRasterTileDataSource, databasePath: path)

    // 3. Create layer and add to map
    let baseLayer = NTRasterTileLayer(dataSource: cachedDataSource)

    // 4. Add the previous raster layer to the map
    mapView?.getLayers()?.add(baseLayer)

    
    // 1. Create a Bing raster data source. Note: tiles start from level 1, there is no single root tile!
    val url = "http://ecn.t3.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=471&mkt=en-US"
    val baseRasterTileDataSource = HTTPTileDataSource(1, 19, url)

    // 2. Add persistent caching datasource, tiles will be stored locally on persistent storage
    val path = getExternalFilesDir(null).path + "/mapcache.db"
    val cachedDataSource = PersistentCacheTileDataSource(baseRasterTileDataSource, path)

    // 3. Create layer and add to map
    val baseLayer = RasterTileLayer(cachedDataSource)

    // 4. Add the previous raster layer to the map
    mapView?.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.

    Projection projection = contentView.mapView.getOptions().getBaseProjection();

    String url = "http://YOUR-SERVER/{z}/{x}/{y}.png";
    String path = getExternalFilesDir(null).getAbsolutePath() + "/cache.db";

    // Approximately downtown Washington DC
    MapPos min = projection.fromWgs84(new MapPos(-77.08, 38.85));
    MapPos max = projection.fromWgs84(new MapPos(-76.94, 38.93));
    MapBounds bounds = new MapBounds(min, max);

    // This source can be anything, even aero picture etc.,
    // using the most basic variant for this example
    HTTPTileDataSource source = new HTTPTileDataSource(0, 18, url);
    PersistentCacheTileDataSource cache = new PersistentCacheTileDataSource(source, path);

    // Only uses cached tiles, does not download any new tiles during zoom
    cache.setCacheOnlyMode(true);

    cache.startDownloadArea(bounds, 0, 10, new TileDownloadListener() {
        @Override
        public void onDownloadProgress(float progress) {

        }

        @Override
        public void onDownloadCompleted() {

        }
    });

    RasterTileLayer layer = new RasterTileLayer(cache);

    contentView.mapView.getLayers().add(layer);

    
      
    Projection projection = ContentView.MapView.Options.BaseProjection;

    string url = "http://YOUR-SERVER/{z}/{x}/{y}.png";
    string path = GetExternalFilesDir(null).AbsolutePath + "/cache.db";

    // Approximately downtown Washington DC
    MapPos min = projection.FromWgs84(new MapPos(-77.08, 38.85));
    MapPos max = projection.FromWgs84(new MapPos(-76.94, 38.93));
    var bounds = new MapBounds(min, max);

    // This source can be anything, even aero picture etc.,
    // using the most basic variant for this example
    var source = new HTTPTileDataSource(0, 18, url);
    var cache = new PersistentCacheTileDataSource(source, path);

    // Only uses cached tiles, does not download any new tiles during zoom
    cache.CacheOnlyMode = true;

    var listener = new DownloadListener();
    cache.StartDownloadArea(bounds, 0, 10, listener);

    var layer = new RasterTileLayer(cache);

    ContentView.MapView.Layers.Add(layer);

    /*
     * DownloadListener implementation
     */
    public class DownloadListener : TileDownloadListener
    {
        public override void OnDownloadProgress(float progress)
        {
            
        }

        public override void OnDownloadCompleted()
        {
            
        }
    }

    
    NTProjection* projection = [[mapView getOptions] getBaseProjection];
        
    NSString* url = @"http://YOUR-SERVER/{z}/{x}/{y}.png";
        
    // Create folder for the database file
    NSArray* paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask,YES);
    NSString* appSupportDir = [paths objectAtIndex: 0];
    NSString* path = [appSupportDir stringByAppendingString:@"/cache.db"];
        
    // Approximately downtown Washington DC
    NTMapPos* min = [projection fromWgs84:[[NTMapPos alloc] initWithX:-77.08 y:38.85]];
    NTMapPos* max = [projection fromWgs84:[[NTMapPos alloc] initWithX:-76.948 y:38.93]];
    NTMapBounds* bounds = [[NTMapBounds alloc] initWithMin:min max:max];
        
    // This source can be anything, even aero picture etc.,
    // using the most basic variant for this example
    NTHTTPTileDataSource* source = [[NTHTTPTileDataSource alloc] initWithMinZoom:0 maxZoom:18 baseURL:url];
    NTPersistentCacheTileDataSource* cache = [[NTPersistentCacheTileDataSource alloc] initWithDataSource:source databasePath:path];
        
    // Only uses cached tiles, does not download any new tiles during zoom
    [cache setCacheOnlyMode:YES];
        
    DownloadListener* listener = [[DownloadListener alloc] init];
    [cache startDownloadArea:bounds minZoom:0 maxZoom:10 tileDownloadListener:listener];
        
    NTRasterTileLayer* layer = [[NTRasterTileLayer alloc] initWithDataSource:cache];
    [[mapView getLayers] add:layer];

    /*
     * DownloadListener implementation
     */
    @interface DownloadListener : NTTileDownloadListener

    @end

    @implementation DownloadListener

    -(void)onDownloadProgress:(float)progress {
        
    }

    -(void)onDownloadCompleted {
        
    }

    @end

    
      
    let projection = contentView.map.getOptions().getBaseProjection()
        
    let url = "http://YOUR-SERVER{z}/{x}/{y}.png"
        
    let documentDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
    let path = documentDir + "/cache.db"
        
    // Approximately downtown Washington DC
    let min = projection?.fromWgs84(NTMapPos(x: -77.08, y: 38.85))
    let max = projection?.fromWgs84(NTMapPos(x: -76.94, y: 38.93))
    let bounds = NTMapBounds(min: min, max: max)
        
    // This source can be anything, even aero picture etc.,
    // using the most basic variant for this example
    let source = NTHTTPTileDataSource(minZoom: 0, maxZoom: 18, baseURL: url)
    let cache = NTPersistentCacheTileDataSource(dataSource: source, databasePath: path)
        
    // Only uses cached tiles, does not download any new tiles during zoom
    cache?.setCacheOnlyMode(true)
        
    let listener = DownloadListener()
    cache?.startDownloadArea(bounds, minZoom: 0, maxZoom: 10, tileDownloadListener: listener)
        
    let layer = NTRasterTileLayer(dataSource: cache)
        
    contentView.map.getLayers().add(layer)

    /*
     * DownloadListener implementation
     */
    class DownloadListener : NTTileDownloadListener {
        
        override func onDownloadProgress(_ progress: Float) {
        
        }
        
        override func onDownloadCompleted() {
            
        }
    }

    
      
    val projection = contentView!!.map.options.baseProjection

    val url = "http://YOUR-SERVER/{z}/{x}/{y}.png"
    val path = getExternalFilesDir(null).absolutePath + "/cache.db"

    // Approximately downtown Washington DC
    val min = projection.fromWgs84(MapPos(-77.08, 38.85))
    val max = projection.fromWgs84(MapPos(-76.94, 38.93))
    val bounds = MapBounds(min, max)

    // This source can be anything, even aero picture etc.,
    // using the most basic variant for this example
    val source = HTTPTileDataSource(0, 18, url)
    val cache = PersistentCacheTileDataSource(source, path)

    // Only uses cached tiles, does not download any new tiles during zoom
    cache.isCacheOnlyMode = true

    cache.startDownloadArea(bounds, 0, 10, object: TileDownloadListener() {
        override fun onDownloadProgress(progress: Float) {
            print("Download progress: " + progress.toString())
        }

        override fun onDownloadCompleted() {
            print("Download complete")
        }
    })

    val layer = 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.

Raster MBTiles

    MBTilesTileDataSource tileDataSource = new MBTilesTileDataSource("MBTILES_FILE_FULL_PATH");
    RasterTileLayer mbTilesLayer = new RasterTileLayer(tileDataSource);
    
    mapView.getLayers().add(mbTilesLayer);

    
  
    var tileDataSource = new MBTilesTileDataSource(filePath);
    var mbTilesLayer = new RasterTileLayer(tileDataSource);

    MapView.Layers.Add(mbTilesLayer);

    
  
    NTTileDataSource* tileDataSource = [[NTMBTilesTileDataSource alloc] initWithPath: @"MBTILES_FILE_FULL_PATH"];
    NTRasterTileLayer* mbTilesLayer = [[NTRasterTileLayer alloc] initWithDataSource:tileDataSource];
    
    [[mapView getLayers] add:mbTilesLayer];

    
  
    let tileDataSource = NTMBTilesTileDataSource(path: "MBTILES_FILE_FULL_PATH")
    let mbTilesLayer = NTRasterTileLayer(dataSource: tileDataSource)
        
    mapView?.getLayers()?.add(mbTilesLayer)

    
  
    val tileDataSource = MBTilesTileDataSource("MBTILES_FILE_FULL_PATH")
    val mbTilesLayer = RasterTileLayer(tileDataSource)
    
    mapView?.layers?.add(mbTilesLayer)

    

MBTiles as VectorTileLayer

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.

    MBTilesTileDataSource tileDataSource = new MBTilesTileDataSource("estonia.mbtiles");

    // Create tile decoder based on Voyager style and VectorTileLayer
    VectorTileDecoder tileDecoder = CartoVectorTileLayer.createTileDecoder(CartoBaseMapStyle.CARTO_BASEMAP_STYLE_VOYAGER);
    VectorTileLayer offlineLayer = new VectorTileLayer(tileDataSource, tileDecoder);

    mapView.getLayers().add(offlineLayer);

    
    var tileDataSource = new MBTilesTileDataSource("estonia.mbtiles");

    // Create tile decoder based on Voyager style and VectorTileLayer
    var tileDecoder = CartoVectorTileLayer.CreateTileDecoder(CartoBaseMapStyle.CartoBasemapStyleVoyager);
    var offlineLayer = new VectorTileLayer(tileDataSource, tileDecoder);

    mapView.Layers.Add(offlineLayer);

    
  
    NSString* path = [[NSBundle mainBundle] pathForResource:@"estonia" ofType:@"mbtiles"];
    NTMBTilesTileDataSource* tileDataSource = [[NTMBTilesTileDataSource alloc] initWithPath:path];
    
    // Create tile decoder based on Voyager style and VectorTileLayer
    NTMBVectorTileDecoder* tileDecoder = [NTCartoVectorTileLayer createTileDecoder: NT_CARTO_BASEMAP_STYLE_VOYAGER];
    NTVectorTileLayer* offlineLayer = [[NTVectorTileLayer alloc] initWithDataSource:tileDataSource decoder:tileDecoder];
    
    [[mapView getLayers] add:offlineLayer];

    
    let tileDataSource = NTMBTilesTileDataSource(path: Bundle.main.path(forResource: "estonia", ofType: "mbtiles"))
    
    // Create tile decoder based on Voyager style and VectorTileLayer
    let tileDecoder = NTCartoVectorTileLayer.createTileDecoder(CartoBaseMapStyle.CARTO_BASEMAP_STYLE_VOYAGER)
    let offlineLayer = NTVectorTileLayer(tileDataSource, tileDecoder)
    
    mapView?.layers?.add(offlineLayer)

    
  
    val tileDataSource = MBTilesTileDataSource("estonia.mbtiles")
    
    // Create tile decoder based on Voyager style and VectorTileLayer
    val tileDecoder = CartoVectorTileLayer.createTileDecoder(CartoBaseMapStyle.CARTO_BASEMAP_STYLE_VOYAGER)
    val offlineLayer = 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 source
    final Projection projection = mapView.getOptions().getBaseProjection();
    final LocalVectorDataSource source = new LocalVectorDataSource(projection);
    VectorLayer layer = new VectorLayer(source);
    mapView.getLayers().add(layer);

    // As the file to load is rather large, we do not want to block our main thread
    thread = new Thread(new Runnable() {
        @Override
        public void run() {

            // Create a basic style
            MarkerStyle style = new MarkerStyleBuilder().buildStyle();

            // Read GeoJSON, parse it using SDK GeoJSON parser
            GeoJSONGeometryReader reader = new GeoJSONGeometryReader();

            // Set target projection to base (mercator)
            reader.setTargetProjection(projection);

            String fileName = "cities15000.geojson";
            String json;

            try {
                InputStream is = getAssets().open(fileName);
                int size = is.available();
                byte[] buffer = new byte[size];
                is.read(buffer);
                is.close();

                json = new String(buffer, "UTF-8");

            } catch (IOException ex) {
                return;
            }

            // Read features from local asset
            FeatureCollection features = reader.readFeatureCollection(json);

            VectorElementVector elements = new VectorElementVector();

            for (int i = 0; i < features.getFeatureCount(); i++) {
                // This data set features point geometry,
                // however, it can also be LineGeometry or PolygonGeometry
                PointGeometry geometry = (PointGeometry) features.getFeature(i).getGeometry();
                elements.add(new Marker(geometry, style));
            }

            source.addAll(elements);
        }
    });

    thread.start();

    
      
    // Read json from assets and add to map
    string json;

    using (System.IO.StreamReader sr = new System.IO.StreamReader (Assets.Open ("cities15000.geojson")))
    {
        json = sr.ReadToEnd ();
    }

    // Initialize a local vector data source
    var projection = MapView.Options.BaseProjection;
    var source = new LocalVectorDataSource(projection);

    VectorLayer layer = new VectorLayer(source);

    new System.Threading.Thread((obj) =>
    {
        // Create basic style. Use the MarkerStyleBuilder to modify your markers
        MarkerStyle style = new MarkerStyleBuilder().BuildStyle();

        // Read GeoJSON, parse it using SDK GeoJSON parser
        GeoJSONGeometryReader reader = new GeoJSONGeometryReader();

        // Set target projection to base (mercator)
        reader.TargetProjection = projection;
        
        // Read features from local asset
        FeatureCollection features = reader.ReadFeatureCollection(json);
        
        VectorElementVector elements = new VectorElementVector();

        for (int i = 0; i < features.FeatureCount; i++)
        {
            // This data set features point geometry,
            // however, it can also be LineGeometry or PolygonGeometry
            PointGeometry geometry = (PointGeometry)features.GetFeature(i).Geometry;
            elements.Add(new Marker(geometry, style));
        }

        source.AddAll(elements);
        
        // Add the clustered vector layer to the map
        MapView.Layers.Add(layer);

    }).Start();

    
    // Initialize a local vector data source
    NTProjection* projection = [[self.mapView getOptions] getBaseProjection];
    NTLocalVectorDataSource* source = [[NTLocalVectorDataSource alloc] initWithProjection:projection];
    NTVectorLayer* layer = [[NTVectorLayer alloc] initWithDataSource:source];
    [[[self mapView] getLayers] add:layer];
        
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
    dispatch_async(queue, ^{
        // Read .geojson
        NSString* fullpath = [[NSBundle mainBundle] pathForResource:@"cities15000" ofType:@"geojson"];
        NSString* json = [NSString stringWithContentsOfFile:fullpath encoding:NSUTF8StringEncoding error:nil];
        
        // .geojson parsing
        NTGeoJSONGeometryReader* geoJsonReader = [[NTGeoJSONGeometryReader alloc] init];
        [geoJsonReader setTargetProjection:projection];
        
        NTFeatureCollection* features = [geoJsonReader readFeatureCollection:json];
        
        // Initialize basic style, as it will later be overridden
        NTMarkerStyle* style = [[[NTMarkerStyleBuilder alloc] init] buildStyle];
        
        NTVectorElementVector* elements = [[NTVectorElementVector alloc] init];
        
        for (int i = 0; i < [features getFeatureCount]; i++) {
            NTPointGeometry* geometry = (NTPointGeometry *)[[features getFeature:i] getGeometry];
        
            NTMarker* marker = [[NTMarker alloc] initWithGeometry:geometry style:style];
            [elements add:marker];
        }
        
        // To avoid flickering, add all the sources
        [source addAll:elements];
    });

    
    let projection = contentView.map.getOptions().getBaseProjection()
    let source = NTLocalVectorDataSource(projection: projection)
    let layer = NTVectorLayer(dataSource: source)
        
    contentView.map.getLayers().add(layer)
        
    DispatchQueue.global().async {

        let path = Bundle.main.path(forResource: "cities15000", ofType: "geojson")
        
        guard let json = try? NSString(contentsOfFile: path!, encoding: String.Encoding.utf8.rawValue) else {
            return
        }
        
        // Create default style
        let mBuilder = NTMarkerStyleBuilder()
        let style = mBuilder?.buildStyle()
        
        // Read GeoJSON, parse it using SDK GeoJSON parser
        let reader = NTGeoJSONGeometryReader()
        reader?.setTargetProjection(self.contentView.map.getOptions().getBaseProjection())
        
        let features = reader?.readFeatureCollection(json as String!)
        
        let elements = NTVectorElementVector()
        let total = Int((features?.getFeatureCount())!)
        
        
        for i in stride(from: 0, to: total, by: 1) {
            // This data set features point geometry, however, it can also be LineGeometry or PolygonGeometry
            let geometry = features?.getFeature(Int32(i)).getGeometry() as? NTPointGeometry
            elements?.add(NTMarker(geometry: geometry, style: style))
        }
        
        DispatchQueue.main.async(execute: {
            source?.addAll(elements)
        })
    }

    
      
    val source = LocalVectorDataSource(contentView!!.map.options.baseProjection)
    val layer = VectorLayer(source)
    contentView!!.map.layers.add(layer)

    // Kotlin Anko library: https://github.com/Kotlin/anko
    doAsync {

        val filename = "cities15000.geojson"

        val stream = assets.open(filename)
        val size = stream.available()
        val buffer = ByteArray(size)

        stream.read(buffer)
        stream.close()

        val json = String(buffer, charset("UTF-8"))

        // Basic style builder
        val mBuilder = MarkerStyleBuilder()
        val style = mBuilder.buildStyle()

        // Read GeoJSON, parse it using SDK GeoJSON parser
        val reader = GeoJSONGeometryReader()
        reader.targetProjection = contentView?.projection

        val features = reader.readFeatureCollection(json)

        val elements = VectorElementVector()
        val total = features.featureCount

        for(i in 0..total - 1) {
            // This data set features point geometry, however, it can also be LineGeometry or PolygonGeometry
            val geometry = features.getFeature(i).geometry as PointGeometry
            elements.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 maps
        
        String cartoCss =
                "#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" +
                "}";

        BinaryData styleAsset = AssetUtils.loadAsset("carto-fonts.zip");
        ZippedAssetPackage assetPackage = new ZippedAssetPackage(styleAsset);

        MBVectorTileDecoder decoder = new MBVectorTileDecoder(new CartoCSSStyleSet(cartoCss, assetPackage));

        MBTilesTileDataSource mbDataSource = createMbtilesDataSourceFromAsset(mbTileFile);
        VectorTileLayer mbLayer = new VectorTileLayer(mbDataSource,decoder);
        mapView.getLayers().add(mbLayer);