package mil.nga.giat.mage.map.cache;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.preference.PreferenceManager;
import android.util.Log;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import mil.nga.geopackage.GeoPackage;
import mil.nga.geopackage.GeoPackageManager;
import mil.nga.geopackage.extension.link.FeatureTileTableLinker;
import mil.nga.geopackage.factory.GeoPackageFactory;
import mil.nga.geopackage.features.index.FeatureIndexManager;
import mil.nga.geopackage.features.user.FeatureDao;
import mil.nga.geopackage.tiles.user.TileDao;
import mil.nga.geopackage.validate.GeoPackageValidate;
import mil.nga.giat.mage.R;
import mil.nga.giat.mage.cache.CacheUtils;
import mil.nga.giat.mage.cache.GeoPackageCacheUtils;
import mil.nga.giat.mage.sdk.utils.StorageUtility;
import mil.nga.wkb.geom.GeometryType;
/**
* Created by wnewman on 2/11/16.
*/
public class CacheProvider {
private static final String LOG_NAME = CacheProvider.class.getName();
private Context context;
private static CacheProvider instance = null;
protected CacheProvider(Context context) {
this.context = context;
}
public static CacheProvider getInstance(Context context) {
if (instance == null) {
instance = new CacheProvider(context);
}
return instance;
}
public interface OnCacheOverlayListener {
void onCacheOverlay(List<CacheOverlay> cacheOverlays);
}
private List<CacheOverlay> cacheOverlays = null;
private Collection<OnCacheOverlayListener> cacheOverlayListeners = new ArrayList<>();
public void registerCacheOverlayListener(OnCacheOverlayListener listener) {
cacheOverlayListeners.add(listener);
if (cacheOverlays != null)
listener.onCacheOverlay(cacheOverlays);
}
public boolean removeCacheOverlay(String name){
boolean removed = false;
if(cacheOverlays != null){
Iterator<CacheOverlay> iterator = cacheOverlays.iterator();
while(iterator.hasNext()){
CacheOverlay cacheOverlay = iterator.next();
if(cacheOverlay.getCacheName().equalsIgnoreCase(name)){
iterator.remove();
removed = true;
break;
}
}
}
return removed;
}
public void unregisterCacheOverlayListener(OnCacheOverlayListener listener) {
cacheOverlayListeners.remove(listener);
}
public void refreshTileOverlays() {
TileOverlaysTask task = new TileOverlaysTask(null);
task.execute();
}
public void enableAndRefreshTileOverlays(String enableOverlayName) {
List<String> overlayNames = new ArrayList<>();
overlayNames.add(enableOverlayName);
enableAndRefreshTileOverlays(overlayNames);
}
public void enableAndRefreshTileOverlays(Collection<String> enableOverlayNames) {
TileOverlaysTask task = new TileOverlaysTask(enableOverlayNames);
task.execute();
}
private void setCacheOverlays(List<CacheOverlay> cacheOverlays) {
this.cacheOverlays = cacheOverlays;
for (OnCacheOverlayListener listener : cacheOverlayListeners) {
listener.onCacheOverlay(cacheOverlays);
}
}
private class TileOverlaysTask extends AsyncTask<Void, Void, List<CacheOverlay>> {
private Set<String> enable = new HashSet<>();
public TileOverlaysTask(Collection<String> enable){
if(enable != null) {
this.enable.addAll(enable);
}
}
@Override
protected List<CacheOverlay> doInBackground(Void... params) {
List<CacheOverlay> overlays = new ArrayList<>();
// Add the existing external GeoPackage databases as cache overlays
GeoPackageManager geoPackageManager = GeoPackageFactory.getManager(context);
addGeoPackageCacheOverlays(context, overlays, geoPackageManager);
// Get public external caches stored in /MapCache folder
Map<StorageUtility.StorageType, File> storageLocations = StorageUtility.getReadableStorageLocations();
for (File storageLocation : storageLocations.values()) {
File root = new File(storageLocation, context.getString(R.string.overlay_cache_directory));
if (root.exists() && root.isDirectory() && root.canRead()) {
for (File cache : root.listFiles()) {
if(cache.canRead()) {
if (cache.isDirectory()) {
// found a cache
overlays.add(new XYZDirectoryCacheOverlay(cache.getName(), cache));
} else if (GeoPackageValidate.hasGeoPackageExtension(cache)) {
GeoPackageCacheOverlay cacheOverlay = getGeoPackageCacheOverlay(context, cache, geoPackageManager);
if (cacheOverlay != null) {
overlays.add(cacheOverlay);
}
}
}
}
}
}
// Check internal/external application storage
File applicationCacheDirectory = CacheUtils.getApplicationCacheDirectory(context);
if (applicationCacheDirectory != null && applicationCacheDirectory.exists()) {
for (File cache : applicationCacheDirectory.listFiles()) {
if (GeoPackageValidate.hasGeoPackageExtension(cache)) {
GeoPackageCacheOverlay cacheOverlay = getGeoPackageCacheOverlay(context, cache, geoPackageManager);
if (cacheOverlay != null) {
overlays.add(cacheOverlay);
}
}
}
}
// Set what should be enabled based on preferences.
boolean update = false;
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
Set<String> updatedEnabledOverlays = new HashSet<>();
updatedEnabledOverlays.addAll(preferences.getStringSet(context.getString(R.string.tileOverlaysKey), Collections.<String>emptySet()));
Set<String> enabledOverlays = new HashSet<>();
enabledOverlays.addAll(updatedEnabledOverlays);
// Determine which caches are enabled
for (CacheOverlay cacheOverlay : overlays) {
// Check and enable the cache
String cacheName = cacheOverlay.getCacheName();
if (enabledOverlays.remove(cacheName)) {
cacheOverlay.setEnabled(true);
}
// Check the child caches
for (CacheOverlay childCache : cacheOverlay.getChildren()) {
if (enabledOverlays.remove(childCache.getCacheName())) {
childCache.setEnabled(true);
cacheOverlay.setEnabled(true);
}
}
// Check for new caches to enable in the overlays and preferences
if (enable.contains(cacheName)) {
update = true;
cacheOverlay.setEnabled(true);
cacheOverlay.setAdded(true);
if (cacheOverlay.isSupportsChildren()) {
for (CacheOverlay childCache : cacheOverlay.getChildren()) {
childCache.setEnabled(true);
updatedEnabledOverlays.add(childCache.getCacheName());
}
} else {
updatedEnabledOverlays.add(cacheName);
}
}
}
// Remove overlays in the preferences that no longer exist
if (!enabledOverlays.isEmpty()) {
updatedEnabledOverlays.removeAll(enabledOverlays);
update = true;
}
// If new enabled cache overlays, update them in the preferences
if (update) {
SharedPreferences.Editor editor = preferences.edit();
editor.putStringSet(context.getString(R.string.tileOverlaysKey), updatedEnabledOverlays);
editor.apply();
}
return overlays;
}
@Override
protected void onPostExecute(List<CacheOverlay> result) {
setCacheOverlays(result);
}
}
/**
* Add GeoPackage Cache Overlay for the existing databases
*
* @param context
* @param overlays
* @param geoPackageManager
*/
private void addGeoPackageCacheOverlays(Context context, List<CacheOverlay> overlays, GeoPackageManager geoPackageManager) {
// Delete any GeoPackages where the file is no longer accessible
geoPackageManager.deleteAllMissingExternal();
// Add each existing database as a cache
List<String> externalDatabases = geoPackageManager.externalDatabases();
for (String database : externalDatabases) {
GeoPackageCacheOverlay cacheOverlay = getGeoPackageCacheOverlay(context, geoPackageManager, database);
if (cacheOverlay != null) {
overlays.add(cacheOverlay);
}
}
}
/**
* Get GeoPackage Cache Overlay for the database file
*
* @param context
* @param cache
* @param geoPackageManager
* @return cache overlay
*/
private GeoPackageCacheOverlay getGeoPackageCacheOverlay(Context context, File cache, GeoPackageManager geoPackageManager) {
GeoPackageCacheOverlay cacheOverlay = null;
// Import the GeoPackage if needed
String cacheName = GeoPackageCacheUtils.importGeoPackage(geoPackageManager, cache);
if(cacheName != null){
// Get the GeoPackage overlay
cacheOverlay = getGeoPackageCacheOverlay(context, geoPackageManager, cacheName);
}
return cacheOverlay;
}
/**
* Get the GeoPackage database as a cache overlay
*
* @param context
* @param geoPackageManager
* @param database
* @return cache overlay
*/
private GeoPackageCacheOverlay getGeoPackageCacheOverlay(Context context, GeoPackageManager geoPackageManager, String database) {
GeoPackageCacheOverlay cacheOverlay = null;
GeoPackage geoPackage = null;
// Add the GeoPackage overlay
try {
geoPackage = geoPackageManager.open(database);
List<GeoPackageTableCacheOverlay> tables = new ArrayList<>();
// GeoPackage tile tables, build a mapping between table name and the created cache overlays
Map<String, GeoPackageTileTableCacheOverlay> tileCacheOverlays = new HashMap<>();
List<String> tileTables = geoPackage.getTileTables();
for (String tileTable : tileTables) {
String tableCacheName = CacheOverlay.buildChildCacheName(database, tileTable);
TileDao tileDao = geoPackage.getTileDao(tileTable);
int count = tileDao.count();
int minZoom = (int) tileDao.getMinZoom();
int maxZoom = (int) tileDao.getMaxZoom();
GeoPackageTileTableCacheOverlay tableCache = new GeoPackageTileTableCacheOverlay(tileTable, database, tableCacheName, count, minZoom, maxZoom);
tileCacheOverlays.put(tileTable, tableCache);
}
// Get a linker to find tile tables linked to features
FeatureTileTableLinker linker = new FeatureTileTableLinker(geoPackage);
Map<String, GeoPackageTileTableCacheOverlay> linkedTileCacheOverlays = new HashMap<>();
// GeoPackage feature tables
List<String> featureTables = geoPackage.getFeatureTables();
for (String featureTable : featureTables) {
String tableCacheName = CacheOverlay.buildChildCacheName(database, featureTable);
FeatureDao featureDao = geoPackage.getFeatureDao(featureTable);
int count = featureDao.count();
GeometryType geometryType = featureDao.getGeometryType();
FeatureIndexManager indexer = new FeatureIndexManager(context, geoPackage, featureDao);
boolean indexed = indexer.isIndexed();
int minZoom = 0;
if (indexed) {
minZoom = featureDao.getZoomLevel() + context.getResources().getInteger(R.integer.geopackage_feature_tiles_min_zoom_offset);
minZoom = Math.max(minZoom, 0);
minZoom = Math.min(minZoom, GeoPackageFeatureTableCacheOverlay.MAX_ZOOM);
}
GeoPackageFeatureTableCacheOverlay tableCache = new GeoPackageFeatureTableCacheOverlay(featureTable, database, tableCacheName, count, minZoom, indexed, geometryType);
// If indexed, check for linked tile tables
if(indexed){
List<String> linkedTileTables = linker.getTileTablesForFeatureTable(featureTable);
for(String linkedTileTable: linkedTileTables){
// Get the tile table cache overlay
GeoPackageTileTableCacheOverlay tileCacheOverlay = tileCacheOverlays.get(linkedTileTable);
if(tileCacheOverlay != null){
// Remove from tile cache overlays so the tile table is not added as stand alone, and add to the linked overlays
tileCacheOverlays.remove(linkedTileTable);
linkedTileCacheOverlays.put(linkedTileTable, tileCacheOverlay);
}else{
// Another feature table may already be linked to this table, so check the linked overlays
tileCacheOverlay = linkedTileCacheOverlays.get(linkedTileTable);
}
// Add the linked tile table to the feature table
if(tileCacheOverlay != null){
tableCache.addLinkedTileTable(tileCacheOverlay);
}
}
}
tables.add(tableCache);
}
// Add stand alone tile tables that were not linked to feature tables
for(GeoPackageTileTableCacheOverlay tileCacheOverlay: tileCacheOverlays.values()){
tables.add(tileCacheOverlay);
}
// Create the GeoPackage overlay with child tables
cacheOverlay = new GeoPackageCacheOverlay(database, tables);
} catch (Exception e) {
Log.e(LOG_NAME, "Could not get geopackage cache", e);
} finally {
if (geoPackage != null) {
geoPackage.close();
}
}
return cacheOverlay;
}
}