com.mapbox.mapboxsdk.offline.OfflineMapDownloader Maven / Gradle / Ivy
package com.mapbox.mapboxsdk.offline;
import android.content.ContentValues;
import android.content.Context;
import android.content.ContextWrapper;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;
import com.mapbox.mapboxsdk.constants.MapboxConstants;
import com.mapbox.mapboxsdk.constants.MathConstants;
import com.mapbox.mapboxsdk.geometry.CoordinateRegion;
import com.mapbox.mapboxsdk.util.AppUtils;
import com.mapbox.mapboxsdk.util.DataLoadingUtils;
import com.mapbox.mapboxsdk.util.MapboxUtils;
import com.mapbox.mapboxsdk.util.NetworkUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public class OfflineMapDownloader implements MapboxConstants {
private static final String TAG = "OfflineMapDownloader";
private static OfflineMapDownloader offlineMapDownloader;
private ArrayList listeners;
private Context context;
/**
* The possible states of the offline map downloader.
*/
public enum MBXOfflineMapDownloaderState {
/**
* An offline map download job is in progress.
*/
MBXOfflineMapDownloaderStateRunning,
/**
* An offline map download job is suspended and can be either resumed or canceled.
*/
MBXOfflineMapDownloaderStateSuspended,
/**
* An offline map download job is being canceled.
*/
MBXOfflineMapDownloaderStateCanceling,
/**
* The offline map downloader is ready to begin a new offline map download job.
*/
MBXOfflineMapDownloaderStateAvailable
}
private String uniqueID;
private String mapID;
private boolean includesMetadata;
private boolean includesMarkers;
private RasterImageQuality imageQuality;
private CoordinateRegion mapRegion;
private int minimumZ;
private int maximumZ;
private MBXOfflineMapDownloaderState state;
private int totalFilesWritten;
private int totalFilesExpectedToWrite;
private ArrayList mutableOfflineMapDatabases;
/*
// Don't appear to be needed as there's one database per app for offline maps
@property (nonatomic) NSString *partialDatabasePath;
@property (nonatomic) NSURL *offlineMapDirectory;
// Don't appear to be needed as as Android and Mapbox Android SDK provide these
@property (nonatomic) NSOperationQueue *backgroundWorkQueue;
@property (nonatomic) NSOperationQueue *sqliteQueue;
@property (nonatomic) NSURLSession *dataSession;
@property (nonatomic) NSInteger activeDataSessionTasks;
*/
private OfflineMapDownloader(Context context) {
super();
this.context = context;
listeners = new ArrayList();
mutableOfflineMapDatabases = new ArrayList();
// Load OfflineMapDatabases from File System
ContextWrapper cw = new ContextWrapper(context);
for (String s : cw.databaseList()) {
if (!s.toLowerCase().contains("partial") && !s.toLowerCase().contains("journal")) {
// Setup Database Handler
OfflineDatabaseManager.getOfflineDatabaseManager(context).getOfflineDatabaseHandlerForMapId(s, true);
// Create the Database Object
OfflineMapDatabase omd = new OfflineMapDatabase(context, s);
omd.initializeDatabase();
mutableOfflineMapDatabases.add(omd);
}
}
this.state = MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateAvailable;
}
public static OfflineMapDownloader getOfflineMapDownloader(Context context) {
if (offlineMapDownloader == null) {
offlineMapDownloader = new OfflineMapDownloader(context);
}
return offlineMapDownloader;
}
public boolean addOfflineMapDownloaderListener(OfflineMapDownloaderListener listener) {
return listeners.add(listener);
}
public boolean removeOfflineMapDownloaderListener(OfflineMapDownloaderListener listener) {
return listeners.remove(listener);
}
/*
Delegate Notifications
*/
public void notifyDelegateOfStateChange() {
for (OfflineMapDownloaderListener listener : listeners) {
listener.stateChanged(this.state);
}
}
public void notifyDelegateOfInitialCount() {
for (OfflineMapDownloaderListener listener : listeners) {
listener.initialCountOfFiles(this.totalFilesExpectedToWrite);
}
}
public void notifyDelegateOfProgress() {
for (OfflineMapDownloaderListener listener : listeners) {
listener.progressUpdate(this.totalFilesWritten, this.totalFilesExpectedToWrite);
}
}
public void notifyDelegateOfNetworkConnectivityError(Throwable error) {
for (OfflineMapDownloaderListener listener : listeners) {
listener.networkConnectivityError(error);
}
}
public void notifyDelegateOfSqliteError(Throwable error) {
for (OfflineMapDownloaderListener listener : listeners) {
listener.sqlLiteError(error);
}
}
public void notifyDelegateOfHTTPStatusError(int status, String url) {
for (OfflineMapDownloaderListener listener : listeners) {
listener.httpStatusError(new Exception(String.format("HTTP Status Error %d, for url = %s", status, url)));
}
}
public void notifyDelegateOfCompletionWithOfflineMapDatabase(OfflineMapDatabase offlineMap) {
for (OfflineMapDownloaderListener listener : listeners) {
listener.completionOfOfflineDatabaseMap(offlineMap);
}
}
/*
Implementation: download urls
*/
public OfflineMapDatabase completeDatabaseAndInstantiateOfflineMapWithError() {
/*
if (AppUtils.runningOnMainThread()) {
Log.w(TAG, "completeDatabaseAndInstantiateOfflineMapWithError() running on main thread. Returning null.");
return null;
}
*/
// Rename database file (remove -PARTIAL) and update path in db object, update path in OfflineMapDatabase, create new Handler
SQLiteDatabase db = OfflineDatabaseManager.getOfflineDatabaseManager(context).getOfflineDatabaseHandlerForMapId(mapID).getReadableDatabase();
String dbPath = db.getPath();
db.close();
if (dbPath.endsWith("-PARTIAL")) {
// Rename SQLlite database file
File oldDb = new File(dbPath);
String newDb = dbPath.substring(0, dbPath.indexOf("-PARTIAL"));
boolean result = oldDb.renameTo(new File(newDb));
Log.i(TAG, "Result of rename = " + result + " for oldDb = '" + dbPath + "'; newDB = '" + newDb + "'");
}
// Update Database Handler
OfflineDatabaseManager.getOfflineDatabaseManager(context).switchHandlerFromPartialToRegular(mapID);
// Create DB object and return
OfflineMapDatabase offlineMapDatabase = new OfflineMapDatabase(context, mapID);
// Initialized with data from database
offlineMapDatabase.initializeDatabase();
return offlineMapDatabase;
// Create new OfflineMapDatabase and load with recently downloaded data
/*
// Rename the file using a unique prefix
//
CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
CFStringRef uuidString = CFUUIDCreateString(kCFAllocatorDefault, uuid);
NSString *newFilename = [NSString stringWithFormat:@"%@.complete",uuidString];
NSString *newPath = [[_offlineMapDirectory URLByAppendingPathComponent:newFilename] path];
CFRelease(uuidString);
CFRelease(uuid);
[[NSFileManager defaultManager] moveItemAtPath:_partialDatabasePath toPath:newPath error:error];
// If the move worked, instantiate and return offline map database
//
if(error && *error)
{
return nil;
}
else
{
return [[MBXOfflineMapDatabase alloc] initWithContentsOfFile:newPath];
}
*/
}
public void startDownloading() {
/*
// Shouldn't need to check as all downloading will happen in background thread
if (AppUtils.runningOnMainThread()) {
Log.w(TAG, "startDownloading() is running on main thread. Returning.");
return;
}
*/
// Update expected files numbers (totalFilesExpectedToWrite and totalFilesWritten)
sqliteQueryWrittenAndExpectedCountsWithError();
Log.d(TAG, String.format("totalFilesExpectedToWrite = %d, totalFilesWritten = %d", this.totalFilesExpectedToWrite, this.totalFilesWritten));
// [_sqliteQueue addOperationWithBlock:^{
// Get the actual URLs
ArrayList urls = sqliteReadArrayOfOfflineMapURLsToBeDownloadLimit(-1);
Log.d(TAG, String.format("number of urls to download = %d", urls.size()));
int totalDiff = this.totalFilesExpectedToWrite - this.totalFilesWritten;
if (urls.size() != totalDiff) {
// Something is off
Log.w(TAG, String.format("totalDiff %d does not equal urls size of %d. This is a problem. Returning.", totalDiff, urls.size()));
return;
} else if (urls.size() == 0 && totalDiff == 0) {
// All files are downloaded, but hasn't been persisted yet.
finishUpDownloadProcess();
return;
}
for (final String url : urls) {
/*
if (!NetworkUtils.isNetworkAvailable(context)) {
Log.w(TAG, "Network is no longer available.");
// [self notifyDelegateOfNetworkConnectivityError:error];
}
*/
AsyncTask foo = new AsyncTask() {
@Override
protected Void doInBackground(String... params) {
HttpURLConnection conn = null;
try {
conn = NetworkUtils.getHttpURLConnection(new URL(params[0]));
Log.d(TAG, "URL to download = " + conn.getURL().toString());
conn.setConnectTimeout(60000);
conn.connect();
int rc = conn.getResponseCode();
if (rc != HttpURLConnection.HTTP_OK) {
String msg = String.format("HTTP Error connection. Response Code = %d", rc);
Log.w(TAG, msg);
notifyDelegateOfHTTPStatusError(rc, params[0]);
throw new IOException(msg);
}
ByteArrayOutputStream bais = new ByteArrayOutputStream();
InputStream is = null;
try {
is = conn.getInputStream();
// Read 4K at a time
byte[] byteChunk = new byte[4096];
int n;
while ((n = is.read(byteChunk)) > 0) {
bais.write(byteChunk, 0, n);
}
} catch (IOException e) {
Log.e(TAG, String.format("Failed while reading bytes from %s: %s", conn.getURL().toString(), e.getMessage()));
e.printStackTrace();
} finally {
if (is != null) {
is.close();
}
conn.disconnect();
}
sqliteSaveDownloadedData(bais.toByteArray(), url);
} catch (IOException e) {
Log.e(TAG, e.getMessage());
e.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
}
}
return null;
}
};
foo.execute(url);
// This is the last line of the for loop
}
}
/*
Implementation: sqlite stuff
*/
public void sqliteSaveDownloadedData(byte[] data, String url) {
if (AppUtils.runningOnMainThread()) {
Log.w(TAG, "trying to run sqliteSaveDownloadedData() on main thread. Return.");
return;
}
// assert(_activeDataSessionTasks > 0);
// [_sqliteQueue addOperationWithBlock:^{
// Bail out if the state has changed to canceling, suspended, or available
//
if (this.state != MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateRunning) {
Log.w(TAG, "sqliteSaveDownloadedData() is not in a Running state so bailing. State = " + this.state);
return;
}
// Open the database read-write and multi-threaded. The slightly obscure c-style variable names here and below are
// used to stay consistent with the sqlite documentaion.
// Continue by inserting an image blob into the data table
//
SQLiteDatabase db = OfflineDatabaseManager.getOfflineDatabaseManager(context).getOfflineDatabaseHandlerForMapId(mapID).getWritableDatabase();
db.beginTransaction();
// String query2 = "INSERT INTO data(value) VALUES(?);";
ContentValues values = new ContentValues();
values.put(OfflineDatabaseHandler.FIELD_DATA_VALUE, data);
db.insert(OfflineDatabaseHandler.TABLE_DATA, null, values);
// [query appendFormat:@"UPDATE resources SET status=200,id=last_insert_rowid() WHERE url='%@';\n",[url absoluteString]];
db.execSQL(String.format("UPDATE %s SET %s=200, %s=last_insert_rowid() WHERE %s='%s';", OfflineDatabaseHandler.TABLE_RESOURCES, OfflineDatabaseHandler.FIELD_RESOURCES_STATUS, OfflineDatabaseHandler.FIELD_RESOURCES_ID, OfflineDatabaseHandler.FIELD_RESOURCES_URL, url));
db.setTransactionSuccessful();
db.endTransaction();
db.close();
/*
if(error)
{
// Oops, that didn't work. Notify the delegate.
//
[self notifyDelegateOfSqliteError:error];
}
else
{
*/
// Update the progress
//
this.totalFilesWritten += 1;
notifyDelegateOfProgress();
Log.d(TAG, "totalFilesWritten = " + this.totalFilesWritten + "; totalFilesExpectedToWrite = " + this.totalFilesExpectedToWrite);
// If all the downloads are done, clean up and notify the delegate
//
if (this.totalFilesWritten >= this.totalFilesExpectedToWrite) {
finishUpDownloadProcess();
}
/*
}
*/
// If this was the last of a batch of urls in the data session's download queue, and there are more urls
// to be downloaded, get another batch of urls from the database and keep working.
//
/*
if(activeDataSessionTasks > 0)
{
_activeDataSessionTasks -= 1;
}
if(_activeDataSessionTasks == 0 && _totalFilesWritten < _totalFilesExpectedToWrite)
{
[self startDownloading];
}
*/
}
private void finishUpDownloadProcess() {
if (this.state == MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateRunning) {
Log.i(TAG, "Just finished downloading all materials. Persist the OfflineMapDatabase, change the state, and call it a day.");
// This is what to do when we've downloaded all the files
//
// Populate OfflineMapDatabase object and persist it
OfflineMapDatabase offlineMap = completeDatabaseAndInstantiateOfflineMapWithError();
if (offlineMap != null) {
this.mutableOfflineMapDatabases.add(offlineMap);
}
notifyDelegateOfCompletionWithOfflineMapDatabase(offlineMap);
this.state = MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateAvailable;
notifyDelegateOfStateChange();
}
}
public ArrayList sqliteReadArrayOfOfflineMapURLsToBeDownloadLimit(int limit) {
ArrayList results = new ArrayList();
if (AppUtils.runningOnMainThread()) {
Log.w(TAG, "Attempting to run sqliteReadArrayOfOfflineMapURLsToBeDownloadLimit() on main thread. Returning.");
return results;
}
// Read up to limit undownloaded urls from the offline map database
//
String query = String.format("SELECT %s FROM %s WHERE %s IS NULL", OfflineDatabaseHandler.FIELD_RESOURCES_URL, OfflineDatabaseHandler.TABLE_RESOURCES, OfflineDatabaseHandler.FIELD_RESOURCES_STATUS);
if (limit > 0) {
query = query + String.format(" LIMIT %d", limit);
}
query = query + ";";
// Open the database
SQLiteDatabase db = OfflineDatabaseManager.getOfflineDatabaseManager(context).getOfflineDatabaseHandlerForMapId(mapID).getReadableDatabase();
Cursor cursor = db.rawQuery(query, null);
if (cursor.moveToFirst()) {
do {
results.add(cursor.getString(0));
} while (cursor.moveToNext());
}
cursor.close();
db.close();
return results;
}
public boolean sqliteQueryWrittenAndExpectedCountsWithError() {
// NOTE: Unlike most of the sqlite code, this method is written with the expectation that it can and will be called on the main
// thread as part of init. This is also meant to be used in other contexts throught the normal serial operation queue.
// Calculate how many files need to be written in total and how many of them have been written already
//
String query = "SELECT COUNT(url) AS totalFilesExpectedToWrite, (SELECT COUNT(url) FROM resources WHERE status IS NOT NULL) AS totalFilesWritten FROM resources;";
boolean success = false;
SQLiteDatabase db = OfflineDatabaseManager.getOfflineDatabaseManager(context).getOfflineDatabaseHandlerForMapId(mapID).getReadableDatabase();
Cursor cursor = db.rawQuery(query, null);
cursor.moveToFirst();
this.totalFilesExpectedToWrite = cursor.getInt(0);
this.totalFilesWritten = cursor.getInt(1);
cursor.close();
db.close();
success = true;
return success;
}
public boolean sqliteCreateDatabaseUsingMetadata(Hashtable metadata, List urlStrings) {
if (AppUtils.runningOnMainThread()) {
Log.w(TAG, "sqliteCreateDatabaseUsingMetadata() running on main thread. Returning.");
return false;
}
boolean success = false;
// Build a query to populate the database (map metadata and list of map resource urls)
//
/*
NSMutableString *query = [[NSMutableString alloc] init];
[query appendString:@"PRAGMA foreign_keys=ON;\n"];
[query appendString:@"BEGIN TRANSACTION;\n"];
[query appendString:@"CREATE TABLE metadata (name TEXT UNIQUE, value TEXT);\n"];
[query appendString:@"CREATE TABLE data (id INTEGER PRIMARY KEY, value BLOB);\n"];
[query appendString:@"CREATE TABLE resources (url TEXT UNIQUE, status TEXT, id INTEGER REFERENCES data);\n"];
*/
SQLiteDatabase db = OfflineDatabaseManager.getOfflineDatabaseManager(context).getOfflineDatabaseHandlerForMapId(mapID).getWritableDatabase();
db.beginTransaction();
for (String key : metadata.keySet()) {
ContentValues cv = new ContentValues();
cv.put(OfflineDatabaseHandler.FIELD_METADATA_NAME, key);
cv.put(OfflineDatabaseHandler.FIELD_METADATA_VALUE, metadata.get(key));
db.replace(OfflineDatabaseHandler.TABLE_METADATA, null, cv);
}
for (String url : urlStrings) {
ContentValues cv = new ContentValues();
cv.put(OfflineDatabaseHandler.FIELD_RESOURCES_URL, url);
db.insert(OfflineDatabaseHandler.TABLE_RESOURCES, null, cv);
}
db.setTransactionSuccessful();
db.endTransaction();
db.close();
this.totalFilesExpectedToWrite = urlStrings.size();
this.totalFilesWritten = 0;
success = true;
/*
// Open the database read-write and multi-threaded. The slightly obscure c-style variable names here and below are
// used to stay consistent with the sqlite documentaion.
sqlite3 *db;
int rc;
const char *filename = [_partialDatabasePath cStringUsingEncoding:NSUTF8StringEncoding];
rc = sqlite3_open_v2(filename, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
if (rc)
{
// Opening the database failed... something is very wrong.
//
if(error != NULL)
{
*error = [NSError mbx_errorCannotOpenOfflineMapDatabase:_partialDatabasePath sqliteError:sqlite3_errmsg(db)];
}
sqlite3_close(db);
}
else
{
// Success! Creating the database file worked, so now populate the tables we'll need to hold the offline map
//
const char *zSql = [query cStringUsingEncoding:NSUTF8StringEncoding];
char *errmsg;
sqlite3_exec(db, zSql, NULL, NULL, &errmsg);
if(error && errmsg != NULL)
{
*error = [NSError mbx_errorQueryFailedForOfflineMapDatabase:_partialDatabasePath sqliteError:errmsg];
sqlite3_free(errmsg);
}
sqlite3_close(db);
success = YES;
}
*/
return success;
}
/*
API: Begin an offline map download
*/
public void beginDownloadingMapID(String mapID, CoordinateRegion mapRegion, Integer minimumZ, Integer maximumZ) {
beginDownloadingMapID(mapID, mapRegion, minimumZ, maximumZ, true, true, RasterImageQuality.MBXRasterImageQualityFull);
}
public void beginDownloadingMapID(String mapID, CoordinateRegion mapRegion, Integer minimumZ, Integer maximumZ, boolean includeMetadata, boolean includeMarkers) {
beginDownloadingMapID(mapID, mapRegion, minimumZ, maximumZ, includeMetadata, includeMarkers, RasterImageQuality.MBXRasterImageQualityFull);
}
public void beginDownloadingMapID(String mapID, CoordinateRegion mapRegion, Integer minimumZ, Integer maximumZ,
boolean includeMetadata, boolean includeMarkers, RasterImageQuality imageQuality) {
if (state != MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateAvailable) {
Log.w(TAG, "state doesn't equal MBXOfflineMapDownloaderStateAvailable so return. state = " + state);
return;
}
// Make sure this completed map doesn't exist already
if (isMapIdAlreadyAnOfflineMapDatabase(mapID)) {
Log.w(TAG, String.format("MapId '%s' has already been downloaded. Please delete it before trying to download again.", mapID));
return;
}
// [self setUpNewDataSession];
// [_backgroundWorkQueue addOperationWithBlock:^{
// Start a download job to retrieve all the resources needed for using the specified map offline
//
this.uniqueID = UUID.randomUUID().toString();
this.mapID = mapID;
this.includesMetadata = includeMetadata;
this.includesMarkers = includeMarkers;
this.imageQuality = imageQuality;
this.mapRegion = mapRegion;
this.minimumZ = minimumZ;
this.maximumZ = maximumZ;
this.state = MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateRunning;
// [self notifyDelegateOfStateChange];
final Hashtable metadataDictionary = new Hashtable();
metadataDictionary.put("uniqueID", this.uniqueID);
metadataDictionary.put("mapID", this.mapID);
metadataDictionary.put("includesMetadata", this.includesMetadata ? "YES" : "NO");
metadataDictionary.put("includesMarkers", this.includesMarkers ? "YES" : "NO");
metadataDictionary.put("imageQuality", String.format("%d", this.imageQuality.getValue()));
metadataDictionary.put("region_latitude", String.format("%.8f", this.mapRegion.getCenter().getLatitude()));
metadataDictionary.put("region_longitude", String.format("%.8f", this.mapRegion.getCenter().getLongitude()));
metadataDictionary.put("region_latitude_delta", String.format("%.8f", this.mapRegion.getSpan().getLatitudeSpan()));
metadataDictionary.put("region_longitude_delta", String.format("%.8f", this.mapRegion.getSpan().getLongitudeSpan()));
metadataDictionary.put("minimumZ", String.format("%d", this.minimumZ));
metadataDictionary.put("maximumZ", String.format("%d", this.maximumZ));
final ArrayList urls = new ArrayList();
String version = "v3";
String dataName = "markers.geojson"; // Only using API v3 for now
// NSString *dataName = ([MBXMapKit accessToken] ? @"features.json" : @"markers.geojson");
// NSString *accessToken = ([MBXMapKit accessToken] ? [@"access_token=" stringByAppendingString:[MBXMapKit accessToken]] : nil);
// Include URLs for the metadata and markers json if applicable
//
if (includeMetadata) {
urls.add(String.format(MAPBOX_BASE_URL_V3 + "%s.json?secure%s", this.mapID, ""));
}
if (includeMarkers) {
urls.add(String.format(MAPBOX_BASE_URL_V3 + "%s/%s%s", this.mapID, dataName, ""));
}
// Loop through the zoom levels and lat/lon bounds to generate a list of urls which should be included in the offline map
//
double minLat = this.mapRegion.getCenter().getLatitude() - (this.mapRegion.getSpan().getLatitudeSpan() / 2.0);
double maxLat = minLat + this.mapRegion.getSpan().getLatitudeSpan();
double minLon = this.mapRegion.getCenter().getLongitude() - (this.mapRegion.getSpan().getLongitudeSpan() / 2.0);
double maxLon = minLon + this.mapRegion.getSpan().getLongitudeSpan();
int minX;
int maxX;
int minY;
int maxY;
int tilesPerSide;
for (int zoom = minimumZ; zoom <= maximumZ; zoom++) {
tilesPerSide = Double.valueOf(Math.pow(2.0, zoom)).intValue();
minX = Double.valueOf(Math.floor(((minLon + 180.0) / 360.0) * tilesPerSide)).intValue();
maxX = Double.valueOf(Math.floor(((maxLon + 180.0) / 360.0) * tilesPerSide)).intValue();
minY = Double.valueOf(Math.floor((1.0 - (Math.log(Math.tan(maxLat * MathConstants.PI / 180.0) + 1.0 / Math.cos(maxLat * MathConstants.PI / 180.0)) / MathConstants.PI)) / 2.0 * tilesPerSide)).intValue();
maxY = Double.valueOf(Math.floor((1.0 - (Math.log(Math.tan(minLat * MathConstants.PI / 180.0) + 1.0 / Math.cos(minLat * MathConstants.PI / 180.0)) / MathConstants.PI)) / 2.0 * tilesPerSide)).intValue();
for (int x = minX; x <= maxX; x++) {
for (int y = minY; y <= maxY; y++) {
urls.add(MapboxUtils.getMapTileURL(context, this.mapID, zoom, x, y, this.imageQuality));
}
}
}
Log.i(TAG, "Number of URLs so far: " + urls.size());
// Determine if we need to add marker icon urls (i.e. parse markers.geojson/features.json), and if so, add them
//
if (includeMarkers) {
String dName = "markers.geojson";
final String geojson = String.format(MAPBOX_BASE_URL_V3 + "%s/%s", this.mapID, dName);
if (!NetworkUtils.isNetworkAvailable(context)) {
// We got a session level error which probably indicates a connectivity problem such as airplane mode.
// Since we must fetch and parse markers.geojson/features.json in order to determine which marker icons need to be
// added to the list of urls to download, the lack of network connectivity is a non-recoverable error
// here.
//
// TODO
/*
[self notifyDelegateOfNetworkConnectivityError:error];
[self cancelImmediatelyWithError:error];
*/
return;
}
AsyncTask foo = new AsyncTask() {
@Override
protected Void doInBackground(Void... params) {
try {
HttpURLConnection conn = NetworkUtils.getHttpURLConnection(new URL(geojson));
conn.setConnectTimeout(60000);
conn.connect();
if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException();
}
BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream(), Charset.forName("UTF-8")));
String jsonText = DataLoadingUtils.readAll(rd);
// The marker geojson was successfully retrieved, so parse it for marker icons. Note that we shouldn't
// try to save it here, because it may already be in the download queue and saving it twice will mess
// up the count of urls to be downloaded!
//
Set markerIconURLStrings = new HashSet();
markerIconURLStrings.addAll(parseMarkerIconURLStringsFromGeojsonData(jsonText));
Log.i(TAG, "Number of markerIconURLs = " + markerIconURLStrings.size());
if (markerIconURLStrings.size() > 0) {
urls.addAll(markerIconURLStrings);
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
// The url for markers.geojson/features.json didn't work (some maps don't have any markers). Notify the delegate of the
// problem, and stop attempting to add marker icons, but don't bail out on whole the offline map download.
// The delegate can decide for itself whether it wants to continue or cancel.
//
// TODO
e.printStackTrace();
/*
[self notifyDelegateOfHTTPStatusError:((NSHTTPURLResponse *)response).statusCode url:response.URL];
*/
}
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
Log.i(TAG, "Done figuring out marker icons, so now start downloading everything.");
// ==========================================================================================================
// == WARNING! WARNING! WARNING! ==
// == This stuff is a duplicate of the code immediately below it, but this copy is inside of a completion ==
// == block while the other isn't. You will be sad and confused if you try to eliminate the "duplication". ==
//===========================================================================================================
startDownloadProcess(metadataDictionary, urls);
}
};
foo.execute();
} else {
Log.i(TAG, "No marker icons to worry about, so just start downloading.");
// There aren't any marker icons to worry about, so just create database and start downloading
startDownloadProcess(metadataDictionary, urls);
}
}
/**
* Private method for Starting the Whole Download Process
*
* @param metadata Metadata
* @param urls Map urls
*/
private void startDownloadProcess(final Hashtable metadata, final List urls) {
AsyncTask startDownload = new AsyncTask() {
@Override
protected Thread doInBackground(Void... params) {
// Do database creation / io on background thread
if (!sqliteCreateDatabaseUsingMetadata(metadata, urls)) {
cancelImmediatelyWithError("Map Database wasn't created");
return null;
}
notifyDelegateOfInitialCount();
startDownloading();
return null;
}
};
// Create the database and start the download
startDownload.execute();
}
public Set parseMarkerIconURLStringsFromGeojsonData(String data) {
HashSet iconURLStrings = new HashSet();
JSONObject simplestyleJSONDictionary = null;
try {
simplestyleJSONDictionary = new JSONObject(data);
// Find point features in the markers dictionary (if there are any) and add them to the map.
//
JSONArray markers = simplestyleJSONDictionary.getJSONArray("features");
if (markers != null && markers.length() > 0) {
for (int lc = 0; lc < markers.length(); lc++) {
Object value = markers.get(lc);
if (value instanceof JSONObject) {
JSONObject feature = (JSONObject) value;
String type = feature.getJSONObject("geometry").getString("type");
if ("Point".equals(type)) {
String size = feature.getJSONObject("properties").getString("marker-size");
String color = feature.getJSONObject("properties").getString("marker-color");
String symbol = feature.getJSONObject("properties").getString("marker-symbol");
if (!TextUtils.isEmpty(size) && !TextUtils.isEmpty(color) && !TextUtils.isEmpty(symbol)) {
String markerURL = MapboxUtils.markerIconURL(context, size, symbol, color);
if (!TextUtils.isEmpty(markerURL)) {
iconURLStrings.add(markerURL);
}
}
}
}
// This is the last line of the loop
}
}
} catch (JSONException e) {
e.printStackTrace();
}
// Return only the unique icon urls
//
return iconURLStrings;
}
public void cancelImmediatelyWithError(String error) {
// TODO
/*
// Creating the database failed for some reason, so clean up and change the state back to available
//
state = MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateCanceling;
[self notifyDelegateOfStateChange];
if([_delegate respondsToSelector:@selector(offlineMapDownloader:didCompleteOfflineMapDatabase:withError:)])
{
dispatch_async(dispatch_get_main_queue(), ^(void){
[_delegate offlineMapDownloader:self didCompleteOfflineMapDatabase:nil withError:error];
});
}
[_dataSession invalidateAndCancel];
[_sqliteQueue cancelAllOperations];
[_sqliteQueue addOperationWithBlock:^{
[self setUpNewDataSession];
_totalFilesWritten = 0;
_totalFilesExpectedToWrite = 0;
[[NSFileManager defaultManager] removeItemAtPath:_partialDatabasePath error:nil];
state = MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateAvailable;
[self notifyDelegateOfStateChange];
}];
*/
}
/*
API: Control an in-progress offline map download
*/
public void cancel() {
Log.d(TAG, "cancel called with state = " + state);
/*
if (state != MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateCanceling && state != MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateAvailable) {
// Stop a download job and discard the associated files
//
[_backgroundWorkQueue addOperationWithBlock:^{
_state = MBXOfflineMapDownloaderStateCanceling;
[self notifyDelegateOfStateChange];
[_dataSession invalidateAndCancel];
[_sqliteQueue cancelAllOperations];
[_sqliteQueue addOperationWithBlock:^{
[self setUpNewDataSession];
_totalFilesWritten = 0;
_totalFilesExpectedToWrite = 0;
[[NSFileManager defaultManager] removeItemAtPath:_partialDatabasePath error:nil];
if([_delegate respondsToSelector:@selector(offlineMapDownloader:didCompleteOfflineMapDatabase:withError:)])
{
NSError *canceled = [NSError mbx_errorWithCode:MBXMapKitErrorCodeDownloadingCanceled reason:@"The download job was canceled" description:@"Download canceled"];
dispatch_async(dispatch_get_main_queue(), ^(void){
[_delegate offlineMapDownloader:self didCompleteOfflineMapDatabase:nil withError:canceled];
});
}
_state = MBXOfflineMapDownloaderStateAvailable;
[self notifyDelegateOfStateChange];
}];
}
}
*/
}
public void resume() {
if (state != MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateSuspended) {
return;
}
/*
// Resume a previously suspended download job
//
[_backgroundWorkQueue addOperationWithBlock:^{
_state = MBXOfflineMapDownloaderStateRunning;
[self startDownloading];
[self notifyDelegateOfStateChange];
}];
*/
}
public void suspend() {
Log.d(TAG, "suspend called with state = " + state);
/*
if (state == MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateRunning) {
// Stop a download job, preserving the necessary state to resume later
//
[_backgroundWorkQueue addOperationWithBlock:^{
[_sqliteQueue cancelAllOperations];
_state = MBXOfflineMapDownloaderStateSuspended;
_activeDataSessionTasks = 0;
[self notifyDelegateOfStateChange];
}];
}
*/
}
/*
API: Access or delete completed offline map databases on disk
*/
public ArrayList getMutableOfflineMapDatabases() {
// Return an array with offline map database objects representing each of the *complete* map databases on disk
return mutableOfflineMapDatabases;
}
public boolean isMapIdAlreadyAnOfflineMapDatabase(String mapId) {
for (OfflineMapDatabase db : getMutableOfflineMapDatabases()) {
if (db.getMapID().equals(mapId)) {
return true;
}
}
return false;
}
public boolean removeOfflineMapDatabase(OfflineMapDatabase offlineMapDatabase) {
// Mark the offline map object as invalid in case there are any references to it still floating around
//
offlineMapDatabase.invalidate();
// Remove the offline map object from the array and delete it's backing database
//
mutableOfflineMapDatabases.remove(offlineMapDatabase);
// Remove Offline Database SQLite file
SQLiteDatabase db = OfflineDatabaseManager.getOfflineDatabaseManager(context).getOfflineDatabaseHandlerForMapId(offlineMapDatabase.getMapID()).getReadableDatabase();
String dbPath = db.getPath();
db.close();
File dbFile = new File(dbPath);
boolean result = dbFile.delete();
Log.i(TAG, String.format("Result of removing database file: %s", result));
return result;
}
public boolean removeOfflineMapDatabaseWithID(String mid) {
for (OfflineMapDatabase database : getMutableOfflineMapDatabases()) {
if (database.getMapID().equals(mid)) {
return removeOfflineMapDatabase(database);
}
}
return false;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy