Hi,
We are trying to migrate one of our applicatoin from v10.2.9 to v100.1. We need to support map formats that are currently unsupported in the new runtime (i.e. : MBTiles). In our case, the system is always offline and the map are stored locally on the Android device.
It was easy to do that in the previous version (Creating a custom TileServiceLayer in ArcGIS Android | ArcGIS Blog ), but I had no success so for on the new v100.1.
I tried to extend the ImageTiledLayer, but I get an exception when I try to create a TileInfo using the following code :
public static ImageTiledLayer createMBTilesLayer(final Uri databaseUri)
{
final SQLiteDatabase database = SQLiteDatabase.openDatabase(databaseUri.getPath(), null, SQLiteDatabase.OPEN_READONLY, new ErrorHandler());
final int maxLevel = retrieveMaxLevel(database);
final List<LevelOfDetail> levelOfDetails = new ArrayList<>();
for(int i = 0; i < maxLevel; i++)
levelOfDetails.add(new LevelOfDetail(i, 156543.032 / Math.pow(2, i), 554678932 / Math.pow(2, i)));
final TileInfo tileInfo = new TileInfo(96, TileInfo.ImageFormat.JPG, levelOfDetails, new Point(0, 0), SpatialReferences.getWebMercator(), 256, 256);
return new MBTilesLayer(tileInfo, new Envelope(-180.0, -85.0, 180, 85, SpatialReferences.getWebMercator()), database);
}
java.lang.RuntimeException: Unable to start activity ComponentInfo{}: com.esri.arcgisruntime.ArcGISRuntimeException: Invalid argument
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2456)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2539)
at android.app.ActivityThread.access$900(ActivityThread.java:159)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1384)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5507)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)Caused by: com.esri.arcgisruntime.ArcGISRuntimeException: Invalid argument
at com.esri.arcgisruntime.internal.jni.CoreTileInfo.nativeCreateWith(Native Method)
at com.esri.arcgisruntime.internal.jni.CoreTileInfo.<init>(SourceFile:72)
at com.esri.arcgisruntime.arcgisservices.TileInfo.<init>(SourceFile:121)...
Is it the right approach to support other tiled format ? Is it some obvious error in the code snippet above that I do not see ?
Regards,
Solved! Go to Solution.
I took some time today and converted the 10.2.x sample to runtime 100:
This is the MBTilesLayer.java class:
package com.arcgis.apps.mbtiles;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import com.esri.arcgisruntime.arcgisservices.LevelOfDetail;
import com.esri.arcgisruntime.arcgisservices.TileInfo;
import com.esri.arcgisruntime.data.TileKey;
import com.esri.arcgisruntime.geometry.Envelope;
import com.esri.arcgisruntime.geometry.GeometryEngine;
import com.esri.arcgisruntime.geometry.Point;
import com.esri.arcgisruntime.geometry.SpatialReferences;
import com.esri.arcgisruntime.layers.ImageTiledLayer;
import java.util.ArrayList;
import java.util.List;
/**
* The MBTilesLayer class allows you to work with a MBTiles stored in a SQLite
* database.
*
*/
public class MBTilesLayer extends ImageTiledLayer {
private SQLiteDatabase mapDb;
private int mLevels = 0;
/**
* The constructor to instantiate MBTiles from a path on device
*
* @param path
* path is expected to be of the form /sdcard/path/package.mbtiles
*/
public static MBTilesLayer init(String path) {
SQLiteDatabase mapDb;
int mLevels = 0;
try {
mapDb = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY);
} catch (SQLException ex) {
Log.e("MBTiles", ex.getMessage());
throw (ex);
}
// Default TMS bounds = bounds of Web Mercator projection
Envelope envWGS = new Envelope(-180.0, -85.0511, 180.0, 85.0511, SpatialReferences.getWgs84());
// See if the MBTiles DB defines their own Bounds in the metadata table
Cursor bounds = mapDb.rawQuery("SELECT value FROM metadata WHERE name = 'bounds'", null);
if (bounds.moveToFirst()) {
String bs = bounds.getString(0);
String[] ba = bs.split(",", 4);
if (ba.length == 4) {
double leftLon = Double.parseDouble(ba[0]);
double topLat = Double.parseDouble(ba[3]);
double rightLon = Double.parseDouble(ba[2]);
double bottomLat = Double.parseDouble(ba[1]);
envWGS = new Envelope(leftLon, bottomLat, rightLon, topLat, SpatialReferences.getWgs84());
}
}
Envelope envWeb = (Envelope) GeometryEngine.project(envWGS,
SpatialReferences.getWebMercator());
Point origin = new Point(envWeb.getXMin(), envWeb.getYMax(), envWeb.getSpatialReference());
Cursor maxLevelCur = mapDb.rawQuery("SELECT MAX(zoom_level) AS max_zoom FROM tiles", null);
if (maxLevelCur.moveToFirst()) {
mLevels = maxLevelCur.getInt(0);
}
Log.i("TAG", "Max levels = " + Integer.toString(mLevels));
double[] resolution = new double[mLevels];
double[] scale = new double[mLevels];
List<LevelOfDetail> lod = new ArrayList<>(mLevels);
for (int i = 0; i < mLevels; i++) {
// see the TMS spec for derivation of the level 0 scale and resolution
// For each level the resolution (in meters per pixel) doubles
resolution[i] = 156543.032 / Math.pow(2, i);
// Level 0 scale is 1:554,678,932. Each level doubles this.
scale[i] = 554678932 / Math.pow(2, i);
lod.add(new LevelOfDetail(i, resolution[i], scale[i]));
}
/*
* Note, the constructor must set the following values or we won't send the
* status change events to listeners and the tiles will not be fetched
*
* Origin is Top Left (web Mercator) , the rest are defined by the TMS
* Global-mercator spec (scales, resolution, 96dpi 256x256 pixel tiles) See:
* http://wiki.osgeo.org/wiki/Tile_Map_Service_Specification#global-mercator
*/
TileInfo ti = new TileInfo(96, TileInfo.ImageFormat.PNG, lod, origin, origin.getSpatialReference(), 256, 256);
return new MBTilesLayer(ti, envWeb, mapDb, mLevels);
}
private MBTilesLayer(TileInfo tileInfo, Envelope fullExtent, SQLiteDatabase mapDb, int mLevels) {
super(tileInfo, fullExtent);
this.mapDb = mapDb;
this.mLevels = mLevels;
}
@Override
protected byte[] getTile(TileKey tileKey) {
// need to flip origin
int nRows = (1 << tileKey.getLevel()); // Num rows = 2^level
int tmsRow = nRows - 1 - tileKey.getRow();
Cursor imageCur = mapDb.rawQuery("SELECT tile_data FROM tiles WHERE zoom_level = " + Integer.toString(tileKey.getLevel())
+ " AND tile_column = " + Integer.toString(tileKey.getColumn()) + " AND tile_row = " + Integer.toString(tmsRow), null);
if (imageCur.moveToFirst()) {
return imageCur.getBlob(0);
}
return null; // Alternatively we might return a "no data" tile
}
}
And here is how I called it:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mapView = (MapView) findViewById(R.id.mapView); MBTilesLayer mbTilesLayer = MBTilesLayer.init(Environment.getExternalStorageDirectory().getPath() + "/ArcGIS/mmpks/world_countries.mbtiles"); ArcGISMap arcGISMap = new ArcGISMap(new Basemap(mbTilesLayer)); mapView.setMap(arcGISMap); }
There are some things I am avoiding (like runtime permissions) but this should be a good start for you to implement this in your applications.
I hope this helps!
I took some time today and converted the 10.2.x sample to runtime 100:
This is the MBTilesLayer.java class:
package com.arcgis.apps.mbtiles;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import com.esri.arcgisruntime.arcgisservices.LevelOfDetail;
import com.esri.arcgisruntime.arcgisservices.TileInfo;
import com.esri.arcgisruntime.data.TileKey;
import com.esri.arcgisruntime.geometry.Envelope;
import com.esri.arcgisruntime.geometry.GeometryEngine;
import com.esri.arcgisruntime.geometry.Point;
import com.esri.arcgisruntime.geometry.SpatialReferences;
import com.esri.arcgisruntime.layers.ImageTiledLayer;
import java.util.ArrayList;
import java.util.List;
/**
* The MBTilesLayer class allows you to work with a MBTiles stored in a SQLite
* database.
*
*/
public class MBTilesLayer extends ImageTiledLayer {
private SQLiteDatabase mapDb;
private int mLevels = 0;
/**
* The constructor to instantiate MBTiles from a path on device
*
* @param path
* path is expected to be of the form /sdcard/path/package.mbtiles
*/
public static MBTilesLayer init(String path) {
SQLiteDatabase mapDb;
int mLevels = 0;
try {
mapDb = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY);
} catch (SQLException ex) {
Log.e("MBTiles", ex.getMessage());
throw (ex);
}
// Default TMS bounds = bounds of Web Mercator projection
Envelope envWGS = new Envelope(-180.0, -85.0511, 180.0, 85.0511, SpatialReferences.getWgs84());
// See if the MBTiles DB defines their own Bounds in the metadata table
Cursor bounds = mapDb.rawQuery("SELECT value FROM metadata WHERE name = 'bounds'", null);
if (bounds.moveToFirst()) {
String bs = bounds.getString(0);
String[] ba = bs.split(",", 4);
if (ba.length == 4) {
double leftLon = Double.parseDouble(ba[0]);
double topLat = Double.parseDouble(ba[3]);
double rightLon = Double.parseDouble(ba[2]);
double bottomLat = Double.parseDouble(ba[1]);
envWGS = new Envelope(leftLon, bottomLat, rightLon, topLat, SpatialReferences.getWgs84());
}
}
Envelope envWeb = (Envelope) GeometryEngine.project(envWGS,
SpatialReferences.getWebMercator());
Point origin = new Point(envWeb.getXMin(), envWeb.getYMax(), envWeb.getSpatialReference());
Cursor maxLevelCur = mapDb.rawQuery("SELECT MAX(zoom_level) AS max_zoom FROM tiles", null);
if (maxLevelCur.moveToFirst()) {
mLevels = maxLevelCur.getInt(0);
}
Log.i("TAG", "Max levels = " + Integer.toString(mLevels));
double[] resolution = new double[mLevels];
double[] scale = new double[mLevels];
List<LevelOfDetail> lod = new ArrayList<>(mLevels);
for (int i = 0; i < mLevels; i++) {
// see the TMS spec for derivation of the level 0 scale and resolution
// For each level the resolution (in meters per pixel) doubles
resolution[i] = 156543.032 / Math.pow(2, i);
// Level 0 scale is 1:554,678,932. Each level doubles this.
scale[i] = 554678932 / Math.pow(2, i);
lod.add(new LevelOfDetail(i, resolution[i], scale[i]));
}
/*
* Note, the constructor must set the following values or we won't send the
* status change events to listeners and the tiles will not be fetched
*
* Origin is Top Left (web Mercator) , the rest are defined by the TMS
* Global-mercator spec (scales, resolution, 96dpi 256x256 pixel tiles) See:
* http://wiki.osgeo.org/wiki/Tile_Map_Service_Specification#global-mercator
*/
TileInfo ti = new TileInfo(96, TileInfo.ImageFormat.PNG, lod, origin, origin.getSpatialReference(), 256, 256);
return new MBTilesLayer(ti, envWeb, mapDb, mLevels);
}
private MBTilesLayer(TileInfo tileInfo, Envelope fullExtent, SQLiteDatabase mapDb, int mLevels) {
super(tileInfo, fullExtent);
this.mapDb = mapDb;
this.mLevels = mLevels;
}
@Override
protected byte[] getTile(TileKey tileKey) {
// need to flip origin
int nRows = (1 << tileKey.getLevel()); // Num rows = 2^level
int tmsRow = nRows - 1 - tileKey.getRow();
Cursor imageCur = mapDb.rawQuery("SELECT tile_data FROM tiles WHERE zoom_level = " + Integer.toString(tileKey.getLevel())
+ " AND tile_column = " + Integer.toString(tileKey.getColumn()) + " AND tile_row = " + Integer.toString(tmsRow), null);
if (imageCur.moveToFirst()) {
return imageCur.getBlob(0);
}
return null; // Alternatively we might return a "no data" tile
}
}
And here is how I called it:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mapView = (MapView) findViewById(R.id.mapView); MBTilesLayer mbTilesLayer = MBTilesLayer.init(Environment.getExternalStorageDirectory().getPath() + "/ArcGIS/mmpks/world_countries.mbtiles"); ArcGISMap arcGISMap = new ArcGISMap(new Basemap(mbTilesLayer)); mapView.setMap(arcGISMap); }
There are some things I am avoiding (like runtime permissions) but this should be a good start for you to implement this in your applications.
I hope this helps!
Hi Alexander,
I changed the envelope creation code with what you provided in your reply and it fixed my issue and it works fine.
Thanks for the quick reply !
No problem!
If it helped resolve your problem, could you kindly mark the answer as correct.