/*
* Geopaparazzi - Digital field mapping on Android based devices
* Copyright (C) 2016 HydroloGIS (www.hydrologis.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.geopaparazzi.mapsforge;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import org.json.JSONException;
import org.mapsforge.android.maps.MapView;
import org.mapsforge.android.maps.mapgenerator.MapGenerator;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import eu.geopaparazzi.library.GPApplication;
import eu.geopaparazzi.library.core.ResourcesManager;
import eu.geopaparazzi.library.core.maps.BaseMap;
import eu.geopaparazzi.library.database.GPLog;
import eu.geopaparazzi.library.profiles.ProfilesHandler;
import eu.geopaparazzi.library.util.LibraryConstants;
import eu.geopaparazzi.mapsforge.databasehandlers.CustomTileDatabaseHandler;
import eu.geopaparazzi.mapsforge.databasehandlers.core.CustomTileDownloader;
import eu.geopaparazzi.mapsforge.databasehandlers.core.CustomTileTable;
import eu.geopaparazzi.mapsforge.databasehandlers.core.GeopackageTileDownloader;
import eu.geopaparazzi.mapsforge.databasehandlers.MapDatabaseHandler;
import eu.geopaparazzi.mapsforge.databasehandlers.core.MapGeneratorInternal;
import eu.geopaparazzi.mapsforge.databasehandlers.core.MapTable;
import eu.geopaparazzi.mapsforge.utils.DefaultMapurls;
import eu.geopaparazzi.spatialite.database.spatial.core.daos.SPL_Vectors;
import eu.geopaparazzi.spatialite.database.spatial.core.databasehandlers.AbstractSpatialDatabaseHandler;
import eu.geopaparazzi.spatialite.database.spatial.core.databasehandlers.MbtilesDatabaseHandler;
import eu.geopaparazzi.spatialite.database.spatial.core.databasehandlers.SpatialiteDatabaseHandler;
import eu.geopaparazzi.library.util.types.ESpatialDataSources;
import eu.geopaparazzi.spatialite.database.spatial.core.enums.VectorLayerQueryModes;
import eu.geopaparazzi.spatialite.database.spatial.core.tables.AbstractSpatialTable;
import eu.geopaparazzi.spatialite.database.spatial.core.tables.SpatialRasterTable;
import eu.geopaparazzi.spatialite.database.spatial.util.SpatialiteLibraryConstants;
import jsqlite.Exception;
/**
* The base maps sources manager.
*
* @author Andrea Antonello (www.hydrologis.com)
*/
public enum BaseMapSourcesManager {
INSTANCE;
private SharedPreferences mPreferences;
private List<BaseMap> mBaseMaps;
private String selectedTileSourceType = "";
private String selectedTableDatabasePath = "";
private String selectedTableTitle = "";
private AbstractSpatialTable selectedBaseMapTable = null;
private HashMap<BaseMap, AbstractSpatialTable> mBaseMaps2TablesMap = new HashMap<>();
private File mMapnikFile;
private boolean mReReadBasemaps = true;
BaseMapSourcesManager() {
try {
GPApplication gpApplication = GPApplication.getInstance();
mPreferences = PreferenceManager.getDefaultSharedPreferences(gpApplication);
/*
* if they do not exist add two mapurl based mapnik and opencycle
* tile sources as default ones. They will automatically
* be backed into a mbtiles db.
*/
File applicationSupporterDir = ResourcesManager.getInstance(gpApplication).getApplicationSupporterDir();
mMapnikFile = new File(applicationSupporterDir, DefaultMapurls.Mapurls.mapnik.toString() + DefaultMapurls.MAPURL_EXTENSION);
DefaultMapurls.checkAllSourcesExistence(gpApplication, applicationSupporterDir);
boolean doSpatialiteRecoveryMode = mPreferences.getBoolean(SpatialiteLibraryConstants.PREFS_KEY_SPATIALITE_RECOVERY_MODE,
false);
// doSpatialiteRecoveryMode=true;
if (doSpatialiteRecoveryMode) {
// Turn on Spatialite Recovery Modus
SPL_Vectors.VECTORLAYER_QUERYMODE = VectorLayerQueryModes.CORRECTIVEWITHINDEX;
// and reset it in the preferences
Editor editor = mPreferences.edit();
editor.putBoolean(SpatialiteLibraryConstants.PREFS_KEY_SPATIALITE_RECOVERY_MODE, false);
editor.apply();
}
selectedTileSourceType = mPreferences.getString(LibraryConstants.PREFS_KEY_TILESOURCE, ""); //$NON-NLS-1$
selectedTableDatabasePath = mPreferences.getString(LibraryConstants.PREFS_KEY_TILESOURCE_FILE, ""); //$NON-NLS-1$
selectedTableTitle = mPreferences.getString(LibraryConstants.PREFS_KEY_TILESOURCE_TITLE, ""); //$NON-NLS-1$
List<BaseMap> baseMaps = getBaseMaps();
if (selectedTableDatabasePath.length() == 0 || !new File(selectedTableDatabasePath).exists()) {
// select mapnik by default
for (BaseMap baseMap : baseMaps) {
if (baseMap.databasePath.equals(mMapnikFile.getAbsolutePath())) {
selectedTableDatabasePath = baseMap.databasePath;
selectedTableTitle = baseMap.title;
selectedTileSourceType = baseMap.mapType;
setTileSource(selectedTileSourceType, selectedTableDatabasePath, selectedTableTitle);
selectedBaseMapTable = mBaseMaps2TablesMap.get(baseMap);
break;
}
}
} else {
for (BaseMap baseMap : baseMaps) {
if (baseMap.databasePath.equals(selectedTableDatabasePath)) {
selectedBaseMapTable = mBaseMaps2TablesMap.get(baseMap);
break;
}
}
}
} catch (java.lang.Exception e) {
GPLog.error(this, null, e);
}
}
/**
* Getter for the current available basemaps.
*
* @return the list of basemaps.
*/
public List<BaseMap> getBaseMaps() {
try {
if (mBaseMaps == null || mReReadBasemaps) {
mBaseMaps = getBaseMapsFromPreferences();
if (mBaseMaps.size() == 0) {
addBaseMapsFromFile(mMapnikFile);
}
mReReadBasemaps = false;
}
return mBaseMaps;
} catch (java.lang.Exception e) {
GPLog.error(this, null, e);
}
return Collections.emptyList();
}
public void forceBasemapsreRead() {
mBaseMaps = null;
mReReadBasemaps = true;
}
/**
* Reads the maps from preferences and extracts the tables necessary.
*
* @return the list of available BaseMaps.
* @throws java.lang.Exception
*/
private List<BaseMap> getBaseMapsFromPreferences() throws java.lang.Exception {
mBaseMaps2TablesMap.clear();
List<BaseMap> baseMaps;
if (ProfilesHandler.INSTANCE.getActiveProfile() == null) {
String baseMapsJson = mPreferences.getString(BaseMap.BASEMAPS_PREF_KEY, "");
baseMaps = BaseMap.fromJsonString(baseMapsJson);
} else {
baseMaps = ProfilesHandler.INSTANCE.getBaseMaps();
}
// TODO this is ugly right now, needs to be changed
for (BaseMap baseMap : baseMaps) {
List<AbstractSpatialTable> tables = collectTablesFromFile(new File(baseMap.databasePath));
for (AbstractSpatialTable table : tables) {
BaseMap tmpBaseMap = table2BaseMap(table);
if (!mBaseMaps2TablesMap.containsKey(tmpBaseMap))
mBaseMaps2TablesMap.put(tmpBaseMap, table);
}
}
return baseMaps;
}
public void saveBaseMapsToPreferences(List<BaseMap> baseMaps) throws JSONException {
if (ProfilesHandler.INSTANCE.getActiveProfile() != null) {
// if profiles are active, the dataset configs are readonly
return;
}
String baseMapJson = BaseMap.toJsonString(baseMaps);
Editor editor = mPreferences.edit();
editor.putString(BaseMap.BASEMAPS_PREF_KEY, baseMapJson);
editor.apply();
}
public List<BaseMap> addBaseMapsFromFile(File file) {
List<BaseMap> foundBaseMaps = new ArrayList<>();
if (!file.getName().startsWith("_")) {
try {
if (mBaseMaps == null) mBaseMaps = new ArrayList<>();
List<AbstractSpatialTable> collectedTables = collectTablesFromFile(file);
saveToBaseMap(collectedTables, foundBaseMaps);
} catch (java.lang.Exception e) {
GPLog.error(this, null, e);
}
}
return foundBaseMaps;
}
public void removeBaseMap(BaseMap baseMap) throws JSONException {
mBaseMaps.remove(baseMap);
mBaseMaps2TablesMap.remove(baseMap);
saveBaseMapsToPreferences(mBaseMaps);
}
@NonNull
private List<AbstractSpatialTable> collectTablesFromFile(File file) throws IOException, Exception {
// GPLog.addLogEntry(this, "Processing file: " + file);
List<AbstractSpatialTable> collectedTables = new ArrayList<>();
/*
* add MAPURL TABLES
*/
try {
CustomTileDatabaseHandler customTileDatabaseHandler = CustomTileDatabaseHandler.getHandlerForFile(file);
if (customTileDatabaseHandler != null) {
try {
List<CustomTileTable> tables = customTileDatabaseHandler.getTables(false);
for (AbstractSpatialTable table : tables) {
collectedTables.add(table);
}
} catch (Exception e) {
e.printStackTrace();
}
customTileDatabaseHandler.close();
} else {
/*
* add MAP TABLES
*/
MapDatabaseHandler mapDatabaseHandler = MapDatabaseHandler.getHandlerForFile(file);
if (mapDatabaseHandler != null) {
try {
List<MapTable> tables = mapDatabaseHandler.getTables(false);
for (AbstractSpatialTable table : tables) {
collectedTables.add(table);
}
} catch (Exception e) {
e.printStackTrace();
}
mapDatabaseHandler.close();
} else {
/*
* add MBTILES, GEOPACKAGE, RASTERLITE TABLES
*/
AbstractSpatialDatabaseHandler sdbHandler = getRasterHandlerForFile(file);
if (sdbHandler != null) {
try {
List<SpatialRasterTable> tables = sdbHandler.getSpatialRasterTables(false);
for (AbstractSpatialTable table : tables) {
collectedTables.add(table);
}
} finally {
sdbHandler.close();
}
}
}
}
} catch (Exception e) {
GPLog.error(this, "error reading file: " + file, e);
}
return collectedTables;
}
/**
* Create a raster handler for the given file.
*
* @param file the file.
* @return the handler or null if the file didn't fit the .
*/
public static AbstractSpatialDatabaseHandler getRasterHandlerForFile(File file) throws IOException {
if (file.exists() && file.isFile()) {
String name = file.getName();
for (ESpatialDataSources spatialiteType : ESpatialDataSources.values()) {
if (!spatialiteType.isSpatialiteBased()) {
continue;
}
String extension = spatialiteType.getExtension();
if (name.endsWith(extension)) {
AbstractSpatialDatabaseHandler sdb = null;
if (name.endsWith(ESpatialDataSources.MBTILES.getExtension())) {
sdb = new MbtilesDatabaseHandler(file.getAbsolutePath(), null);
} else {
sdb = new SpatialiteDatabaseHandler(file.getAbsolutePath());
}
if (sdb.isValid()) {
return sdb;
}
}
}
}
return null;
}
private void saveToBaseMap(List<AbstractSpatialTable> tablesList, List<BaseMap> foundBaseMaps) throws JSONException {
for (AbstractSpatialTable table : tablesList) {
BaseMap newBaseMap = table2BaseMap(table);
if (mBaseMaps.contains(newBaseMap))
continue;
mBaseMaps.add(newBaseMap);
mBaseMaps2TablesMap.put(newBaseMap, table);
foundBaseMaps.add(newBaseMap);
}
saveBaseMapsToPreferences(mBaseMaps);
}
@NonNull
private BaseMap table2BaseMap(AbstractSpatialTable table) {
BaseMap newBaseMap = new BaseMap();
String databasePath = table.getDatabasePath();
File databaseFile = new File(databasePath);
newBaseMap.parentFolder = databaseFile.getParent();
newBaseMap.databasePath = table.getDatabasePath();
newBaseMap.mapType = table.getMapType();
newBaseMap.title = table.getTitle();
return newBaseMap;
}
/**
* Getter for the current selected map table.
*
* @return the current selected map table.
*/
public AbstractSpatialTable getSelectedBaseMapTable() {
if (selectedBaseMapTable == null) {
if (mBaseMaps2TablesMap.size() == 0) {
try {
getBaseMaps();
} catch (java.lang.Exception e) {
GPLog.error(this, null, e);
}
}
try {
BaseMap baseMap;
if (mBaseMaps2TablesMap.size() > 0) {
baseMap = mBaseMaps2TablesMap.keySet().iterator().next();
} else {
List<BaseMap> baseMaps = addBaseMapsFromFile(mMapnikFile);
baseMap = baseMaps.get(0);
}
setSelectedBaseMap(baseMap);
} catch (Exception e) {
GPLog.error(this, "Error on setting selected basemap", e);
}
}
return selectedBaseMapTable;
}
/**
* Selected a Map through its BaseMap.
*
* @param baseMap the base map to use..
* @throws jsqlite.Exception
*/
public void setSelectedBaseMap(BaseMap baseMap) throws Exception {
try {
selectedTileSourceType = baseMap.mapType;
selectedTableDatabasePath = baseMap.databasePath;
selectedTableTitle = baseMap.title;
selectedBaseMapTable = mBaseMaps2TablesMap.get(baseMap);
} catch (java.lang.Exception e) {
GPLog.error(this, null, e);
// fallback on mapnik
List<BaseMap> addedBaseMaps = addBaseMapsFromFile(mMapnikFile);
if (addedBaseMaps.size() > 0) {
BaseMap setBaseMap = addedBaseMaps.get(0);
selectedTileSourceType = setBaseMap.mapType;
selectedTableDatabasePath = setBaseMap.databasePath;
selectedTableTitle = setBaseMap.title;
selectedBaseMapTable = mBaseMaps2TablesMap.get(setBaseMap);
} else {
// give up
return;
}
}
setTileSource(selectedTileSourceType, selectedTableDatabasePath, selectedTableTitle);
}
/**
* Sets the tilesource for the map.
*/
private void setTileSource(String selectedTileSourceType, String selectedTileSourceFile, String selectedTableTitle) {
Editor editor = mPreferences.edit();
editor.putString(LibraryConstants.PREFS_KEY_TILESOURCE, selectedTileSourceType);
editor.putString(LibraryConstants.PREFS_KEY_TILESOURCE_FILE, selectedTileSourceFile);
editor.putString(LibraryConstants.PREFS_KEY_TILESOURCE_TITLE, selectedTableTitle);
editor.apply();
}
/**
* Load the currently selected BaseMap.
* <p/>
* <p>This method should be called from within the activity defining the
* {@link MapView}.
* <p/>
*
* @param mapView Map-View to set.
*/
public void loadSelectedBaseMap(MapView mapView) {
AbstractSpatialTable selectedSpatialTable = getSelectedBaseMapTable();
if (selectedSpatialTable != null) {
int selectedSpatialDataTypeCode = ESpatialDataSources.getCode4Name(selectedTileSourceType);
MapGenerator selectedMapGenerator = null;
try {
ESpatialDataSources selectedSpatialDataType = ESpatialDataSources.getType4Code(selectedSpatialDataTypeCode);
switch (selectedSpatialDataType) {
case MAP:
MapTable selectedMapTable = (MapTable) selectedSpatialTable;
clearTileCache(mapView);
mapView.setMapFile(selectedMapTable.getDatabaseFile());
if (selectedMapTable.getXmlFile().exists()) {
try {
mapView.setRenderTheme(selectedMapTable.getXmlFile());
} catch (java.lang.Exception e) {
// ignore the theme
GPLog.error(this, "ERROR", e);
}
}
break;
case MBTILES:
case GPKG:
case RASTERLITE2:
case SQLITE: {
// TODO check
SpatialRasterTable selectedSpatialRasterTable = (SpatialRasterTable) selectedSpatialTable;
selectedMapGenerator = new GeopackageTileDownloader(selectedSpatialRasterTable);
clearTileCache(mapView);
mapView.setMapGenerator(selectedMapGenerator);
}
break;
case MAPURL: {
selectedMapGenerator = new CustomTileDownloader(selectedSpatialTable.getDatabaseFile());
try {
clearTileCache(mapView);
mapView.setMapGenerator(selectedMapGenerator);
if (GPLog.LOG_HEAVY)
GPLog.addLogEntry(this, "MapsDirManager -I-> MAPURL setMapGenerator[" + selectedTileSourceType
+ "] selected_map[" + selectedTableDatabasePath + "]");
} catch (java.lang.NullPointerException e_mapurl) {
GPLog.error(this, "MapsDirManager setMapGenerator[" + selectedTileSourceType + "] selected_map["
+ selectedTableDatabasePath + "]", e_mapurl);
}
}
break;
default:
break;
}
} catch (java.lang.Exception e) {
selectedMapGenerator = MapGeneratorInternal.createMapGenerator(MapGeneratorInternal.mapnik);
mapView.setMapGenerator(selectedMapGenerator);
GPLog.error(this, "ERROR", e);
}
}
}
/**
* Clear MapView TileCache.
*
* @param mapView the {@link MapView}.
*/
private static void clearTileCache(MapView mapView) {
if (mapView != null) {
mapView.getInMemoryTileCache().destroy();
if (mapView.getFileSystemTileCache().isPersistent()) {
mapView.getFileSystemTileCache().setPersistent(false);
}
mapView.getFileSystemTileCache().destroy();
}
}
}