package mil.nga.dice.map.geopackage; import android.content.Context; import android.util.Log; import android.widget.Toast; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.MapView; import com.google.android.gms.maps.model.BitmapDescriptorFactory; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; import com.google.android.gms.maps.model.TileOverlay; import com.google.android.gms.maps.model.TileOverlayOptions; import com.google.android.gms.maps.model.TileProvider; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import mil.nga.dice.DICEConstants; import mil.nga.dice.report.Report; import mil.nga.dice.report.ReportCache; import mil.nga.geopackage.GeoPackage; import mil.nga.geopackage.GeoPackageCache; 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.FeatureCursor; import mil.nga.geopackage.features.user.FeatureDao; import mil.nga.geopackage.features.user.FeatureRow; import mil.nga.geopackage.geom.GeoPackageGeometryData; import mil.nga.geopackage.map.geom.GoogleMapShape; import mil.nga.geopackage.map.geom.GoogleMapShapeConverter; import mil.nga.geopackage.map.geom.GoogleMapShapeType; import mil.nga.geopackage.projection.Projection; import mil.nga.geopackage.tiles.features.FeatureTiles; import mil.nga.geopackage.tiles.features.DefaultFeatureTiles; import mil.nga.geopackage.tiles.features.custom.NumberFeaturesTile; import mil.nga.geopackage.map.tiles.overlay.BoundedOverlay; import mil.nga.geopackage.map.tiles.overlay.FeatureOverlay; import mil.nga.geopackage.map.tiles.overlay.FeatureOverlayQuery; import mil.nga.geopackage.map.tiles.overlay.GeoPackageOverlayFactory; import mil.nga.geopackage.tiles.user.TileDao; import mil.nga.wkb.geom.Geometry; import mil.nga.wkb.geom.GeometryType; import mil.nga.wkb.util.GeometryPrinter; /** * Manages GeoPackage feature and tile overlays, including adding to and removing from the map */ public class GeoPackageMapOverlays { /** * Context */ private final Context context; /** * Map View */ private final MapView mapView; /** * Google Map */ private final GoogleMap map; /** * GeoPackage manager */ private final GeoPackageManager manager; /** * GeoPackage cache */ private final GeoPackageCache cache; /** * Mapping between GeoPackage name and the map data */ private Map<String, GeoPackageMapData> mapData = new HashMap<>(); /** * GeoPackage list */ private List<GeoPackageMapData> mapDataList = new ArrayList<>(); /** * Current map selected report */ private Report selectedReport; /** * Synchronizing lock */ private final Lock lock = new ReentrantLock(); /** * Selected GeoPackage settings */ private GeoPackageSelected selectedSettings; /** * Marker feature */ class MarkerFeature { long featureId; String database; String tableName; } /** * Mapping between marker ids and the features */ private Map<String, MarkerFeature> markerIds = new HashMap<String, MarkerFeature>(); /** * Constructor * * @param context * @param map * @param mapView */ public GeoPackageMapOverlays(Context context, MapView mapView, GoogleMap map) { this.context = context; this.mapView = mapView; this.map = map; manager = GeoPackageFactory.getManager(context); cache = new GeoPackageCache(manager); selectedSettings = new GeoPackageSelected(context); } /** * Determine if there are any GeoPackages within DICE * * @return true if GeoPackages exist */ public boolean hasGeoPackages() { String like = DICEConstants.DICE_TEMP_CACHE_SUFFIX + "%"; List<String> geoPackages = null; try { geoPackages = manager.databasesNotLike(like); } catch (Exception e) { Log.e(GeoPackageMapOverlays.class.getSimpleName(), "Failed to find shared GeoPackage count", e); } return geoPackages != null && !geoPackages.isEmpty(); } /** * Get the map data * * @return map data */ public Map<String, GeoPackageMapData> getMapData() { return mapData; } /** * Get the map data list * * @return map data list */ public List<GeoPackageMapData> getMapDataList() { return mapDataList; } /** * Update the map with selected GeoPackages */ public void updateMap() { lock.lock(); try { updateMapSynchronized(); } catch (Exception e) { Log.e(GeoPackageMapOverlays.class.getSimpleName(), "Failed to update map with active GeoPackages", e); } finally { lock.unlock(); } } /** * Update the map while synchronized */ private void updateMapSynchronized() { Map<String, Set<String>> selectedCaches = selectedSettings.getSelectedMap(); Map<String, Set<String>> updateSelectedCaches = new HashMap<>(selectedCaches); Set<String> selectedGeoPackages = new HashSet<>(); if (selectedReport != null) { for (ReportCache reportCache : selectedReport.getCacheFiles()) { updateSelectedCaches.put(reportCache.getName(), new HashSet<String>()); selectedGeoPackages.add(reportCache.getName()); } } String like = DICEConstants.DICE_TEMP_CACHE_SUFFIX + "%"; List<String> geoPackages = null; try { geoPackages = manager.databasesLike(like); } catch (Exception e) { Log.e(GeoPackageMapOverlays.class.getSimpleName(), "Failed to find temporary GeoPackages", e); } if (geoPackages != null) { for (String geoPackage : geoPackages) { if (!selectedGeoPackages.contains(geoPackage)) { cache.close(geoPackage); try { manager.delete(geoPackage); } catch (Exception e) { Log.e(GeoPackageMapOverlays.class.getSimpleName(), "Failed to delete GeoPackage: " + geoPackage, e); } } } } Map<String, GeoPackageMapData> newMapData = new HashMap<>(); // Add the GeoPackage caches for (String name : updateSelectedCaches.keySet()) { boolean deleteFromSelected = true; // Make sure the GeoPackage exists boolean exists = false; try { exists = manager.exists(name); } catch (Exception e) { Log.e(GeoPackageMapOverlays.class.getSimpleName(), "Failed to check if GeoPackage exists: " + name, e); } if (exists) { // Make sure the GeoPackage file exists File file = null; try { file = manager.getFile(name); } catch (Exception e) { Log.e(GeoPackageMapOverlays.class.getSimpleName(), "Failed to get file for GeoPackage: " + name, e); } if (file != null) { deleteFromSelected = false; Set<String> selected = updateSelectedCaches.get(name); // Close a previously open GeoPackage connection if a new GeoPackge version boolean removeExistingFromMap = false; if (selected.isEmpty()) { cache.close(name); removeExistingFromMap = true; } GeoPackage geoPackage = cache.getOrOpen(name); GeoPackageMapData existingGeoPackageData = mapData.get(name); // If the GeoPackage is selected with no tables, select all of them as it is a new version if (selected.isEmpty()) { selected.addAll(geoPackage.getTables()); if (!selectedGeoPackages.contains(name)) { updateSelectedCaches.put(name, selected); selectedSettings.updateSelected(updateSelectedCaches); removeExistingFromMap = true; } } if(removeExistingFromMap && existingGeoPackageData != null){ existingGeoPackageData.removeFromMap(markerIds); existingGeoPackageData = null; } GeoPackageMapData geoPackageData = new GeoPackageMapData(name); newMapData.put(geoPackageData.getName(), geoPackageData); addGeoPackage(geoPackage, selected, geoPackageData, existingGeoPackageData); } else { // Delete if the file was deleted try { manager.delete(name); } catch (Exception e) { Log.e(GeoPackageMapOverlays.class.getSimpleName(), "Failed to delete GeoPackage: " + name, e); } } } // Remove the GeoPackage from the list of selected if (deleteFromSelected) { updateSelectedCaches.remove(name); selectedSettings.updateSelected(updateSelectedCaches); } } // Remove GeoPackage tables from the map that are no longer selected for (GeoPackageMapData oldGeoPackageMapData : mapDataList) { GeoPackageMapData newGeoPackageMapData = newMapData.get(oldGeoPackageMapData.getName()); if (newGeoPackageMapData == null) { oldGeoPackageMapData.removeFromMap(markerIds); cache.close(oldGeoPackageMapData.getName()); } else { for (GeoPackageTableMapData oldGeoPackageTableMapData : oldGeoPackageMapData.getTables()) { GeoPackageTableMapData newGeoPackageTableMapData = newGeoPackageMapData.getTable(oldGeoPackageTableMapData.getName()); if (newGeoPackageTableMapData == null) { oldGeoPackageTableMapData.removeFromMap(markerIds); } } } } mapData = newMapData; mapDataList = new ArrayList<>(mapData.values()); } /** * Add the GeoPackage to the map * * @param geoPackage * @param selected * @param data * @param existingData */ private void addGeoPackage(GeoPackage geoPackage, Set<String> selected, GeoPackageMapData data, GeoPackageMapData existingData) { for (String table : selected) { boolean addNew = true; if (existingData != null) { GeoPackageTableMapData tableData = existingData.getTable(table); if (tableData != null) { addNew = false; data.addTable(tableData); } } if (addNew) { if (geoPackage.isTileTable(table)) { addTileTable(geoPackage, table, data); } else if (geoPackage.isFeatureTable(table)) { addFeatureTable(geoPackage, table, data); } } } } /** * Add a tile table to the map * * @param geoPackage * @param name * @param data */ private void addTileTable(GeoPackage geoPackage, String name, GeoPackageMapData data) { GeoPackageTableMapData tableData = new GeoPackageTableMapData(name, false); data.addTable(tableData); // Create a new GeoPackage tile provider and add to the map TileDao tileDao = geoPackage.getTileDao(name); BoundedOverlay geoPackageTileOverlay = GeoPackageOverlayFactory.getBoundedOverlay(tileDao); // Check for linked feature tables FeatureTileTableLinker linker = new FeatureTileTableLinker(geoPackage); List<FeatureDao> featureDaos = linker.getFeatureDaosForTileTable(tileDao.getTableName()); for (FeatureDao featureDao : featureDaos) { // Create the feature tiles FeatureTiles featureTiles = new DefaultFeatureTiles(context, featureDao); // Create an index manager FeatureIndexManager indexer = new FeatureIndexManager(context, geoPackage, featureDao); featureTiles.setIndexManager(indexer); // Add the feature overlay query FeatureOverlayQuery featureOverlayQuery = new FeatureOverlayQuery(context, geoPackageTileOverlay, featureTiles); tableData.addFeatureOverlayQuery(featureOverlayQuery); } // Set the tiles index to be -2 of it is behind features and tiles drawn from features int zIndex = -2; // If these tiles are linked to features, set the zIndex to -1 so they are placed before imagery tiles if (!featureDaos.isEmpty()) { zIndex = -1; } TileOverlayOptions overlayOptions = createTileOverlayOptions(geoPackageTileOverlay, zIndex); TileOverlay tileOverlay = map.addTileOverlay(overlayOptions); tableData.setTileOverlay(tileOverlay); } /** * Add a feature table to the map * * @param geoPackage * @param name * @param data */ private void addFeatureTable(GeoPackage geoPackage, String name, GeoPackageMapData data) { GeoPackageTableMapData tableData = new GeoPackageTableMapData(name, true); data.addTable(tableData); // Create a new GeoPackage tile provider and add to the map FeatureDao featureDao = geoPackage.getFeatureDao(name); FeatureIndexManager indexer = new FeatureIndexManager(context, geoPackage, featureDao); if (indexer.isIndexed()) { FeatureTiles featureTiles = new DefaultFeatureTiles(context, featureDao); Integer maxFeaturesPerTile = null; if (featureDao.getGeometryType() == GeometryType.POINT) { maxFeaturesPerTile = DICEConstants.DICE_CACHE_FEATURE_TILES_MAX_POINTS_PER_TILE; } else { maxFeaturesPerTile = DICEConstants.DICE_CACHE_FEATURE_TILES_MAX_FEATURES_PER_TILE; } featureTiles.setMaxFeaturesPerTile(maxFeaturesPerTile); NumberFeaturesTile numberFeaturesTile = new NumberFeaturesTile(context); // Adjust the max features number tile draw paint attributes here as needed to // change how tiles are drawn when more than the max features exist in a tile featureTiles.setMaxFeaturesTileDraw(numberFeaturesTile); featureTiles.setIndexManager(indexer); // Adjust the feature tiles draw paint attributes here as needed to change how // features are drawn on tiles FeatureOverlay featureOverlay = new FeatureOverlay(featureTiles); featureOverlay.setMinZoom(featureDao.getZoomLevel()); FeatureTileTableLinker linker = new FeatureTileTableLinker(geoPackage); List<TileDao> tileDaos = linker.getTileDaosForFeatureTable(featureDao.getTableName()); featureOverlay.ignoreTileDaos(tileDaos); FeatureOverlayQuery featureOverlayQuery = new FeatureOverlayQuery(context, featureOverlay); tableData.addFeatureOverlayQuery(featureOverlayQuery); TileOverlayOptions overlayOptions = createTileOverlayOptions(featureOverlay, -1); TileOverlay tileOverlay = map.addTileOverlay(overlayOptions); tableData.setTileOverlay(tileOverlay); } else { indexer.close(); int maxFeaturesPerTable = 0; if (featureDao.getGeometryType() == GeometryType.POINT) { maxFeaturesPerTable = DICEConstants.DICE_CACHE_FEATURES_MAX_POINTS_PER_TABLE; } else { maxFeaturesPerTable = DICEConstants.DICE_CACHE_FEATURES_MAX_FEATURES_PER_TABLE; } Projection projection = featureDao.getProjection(); GoogleMapShapeConverter shapeConverter = new GoogleMapShapeConverter(projection); FeatureCursor featureCursor = featureDao.queryForAll(); try { final int totalCount = featureCursor.getCount(); int count = 0; while (featureCursor.moveToNext()) { FeatureRow featureRow = featureCursor.getRow(); GeoPackageGeometryData geometryData = featureRow.getGeometry(); if (geometryData != null && !geometryData.isEmpty()) { Geometry geometry = geometryData.getGeometry(); if (geometry != null) { GoogleMapShape shape = shapeConverter.toShape(geometry); if (shape.getShapeType() == GoogleMapShapeType.LAT_LNG) { LatLng latLng = (LatLng) shape.getShape(); MarkerOptions markerOptions = new MarkerOptions(); markerOptions.position(latLng); markerOptions.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE)); shape = new GoogleMapShape(GeometryType.POINT, GoogleMapShapeType.MARKER_OPTIONS, markerOptions); } GoogleMapShape mapShape = GoogleMapShapeConverter.addShapeToMap(map, shape); if (mapShape.getShapeType() == GoogleMapShapeType.MARKER) { Marker marker = (Marker) mapShape.getShape(); MarkerFeature markerFeature = new MarkerFeature(); markerFeature.database = geoPackage.getName(); markerFeature.tableName = name; markerFeature.featureId = featureRow.getId(); markerIds.put(marker.getId(), markerFeature); } tableData.addMapShape(mapShape); if (++count >= maxFeaturesPerTable) { if (count < totalCount) { Toast.makeText(context, geoPackage.getName() + "-" + name + "- added " + count + " of " + totalCount, Toast.LENGTH_LONG).show(); } break; } } } } } finally { featureCursor.close(); } } } /** * Create Tile Overlay Options for the Tile Provider using the z index * * @param tileProvider * @param zIndex * @return */ private TileOverlayOptions createTileOverlayOptions(TileProvider tileProvider, int zIndex) { TileOverlayOptions overlayOptions = new TileOverlayOptions(); overlayOptions.tileProvider(tileProvider); overlayOptions.zIndex(zIndex); return overlayOptions; } /** * Query and build a map click location message from enabled GeoPackage tables * * @param latLng click location * @return click message */ public String mapClickMessage(LatLng latLng) { StringBuilder clickMessage = new StringBuilder(); if (selectedReport == null) { for (GeoPackageMapData data : mapDataList) { String message = data.mapClickMessage(latLng, mapView, map); if (message != null) { if (clickMessage.length() > 0) { clickMessage.append("\n\n"); } clickMessage.append(message); } } } return clickMessage.length() > 0 ? clickMessage.toString() : null; } /** * Build a map click location message from the clicked marker * * @param marker clicked marker * @return click message */ public String mapClickMessage(Marker marker) { String message = null; if (selectedReport == null) { MarkerFeature markerFeature = markerIds.get(marker.getId()); if (markerFeature != null) { final GeoPackage geoPackage = manager.open(markerFeature.database); try { final FeatureDao featureDao = geoPackage .getFeatureDao(markerFeature.tableName); final FeatureRow featureRow = featureDao.queryForIdRow(markerFeature.featureId); if (featureRow != null) { GeoPackageGeometryData geomData = featureRow.getGeometry(); if (geomData != null && !geomData.isEmpty()) { Geometry geometry = geomData.getGeometry(); if (geometry != null) { StringBuilder messageBuilder = new StringBuilder(); messageBuilder.append(markerFeature.database).append(" - ").append(markerFeature.tableName).append("\n"); int geometryColumn = featureRow.getGeometryColumnIndex(); for (int i = 0; i < featureRow.columnCount(); i++) { if (i != geometryColumn) { Object value = featureRow.getValue(i); if (value != null) { messageBuilder.append("\n").append(featureRow.getColumnName(i)).append(": ").append(value); } } } if (messageBuilder.length() > 0) { messageBuilder.append("\n\n"); } messageBuilder.append(GeometryPrinter.getGeometryString(geometry)); message = messageBuilder.toString(); } } } } finally { geoPackage.close(); } } } return message; } /** * Report has been selected on the map * * @param report selected report */ public void selectedReport(Report report) { Report existingReport = selectedReport; if (existingReport == null || existingReport != report) { if (existingReport != null) { deselectedReport(); } if (!report.getCacheFiles().isEmpty()) { for (ReportCache reportCache : report.getCacheFiles()) { boolean exists = false; try { exists = manager.exists(reportCache.getName()); } catch (Exception e) { Log.e(GeoPackageMapOverlays.class.getSimpleName(), "Failed to check if GeoPackage exists" + reportCache.getName(), e); } if (!exists) { try { manager.importGeoPackageAsExternalLink(reportCache.getPath(), reportCache.getName(), true); } catch (Exception e) { Log.e(GeoPackageMapOverlays.class.getSimpleName(), "Failed to import GeoPackage " + reportCache.getName() + " at path: " + reportCache.getPath(), e); } } } selectedReport = report; updateMap(); } } } /** * Report has been deselected on the map */ public void deselectedReport() { boolean change = false; if (selectedReport != null) { selectedReport = null; String like = DICEConstants.DICE_TEMP_CACHE_SUFFIX + "%"; List<String> geoPackages = null; try { geoPackages = manager.databasesLike(like); } catch (Exception e) { Log.e(GeoPackageMapOverlays.class.getSimpleName(), "Failed to find temporary GeoPackages", e); } if (geoPackages != null) { for (String geoPackage : geoPackages) { cache.close(geoPackage); try { manager.delete(geoPackage); } catch (Exception e) { Log.e(GeoPackageMapOverlays.class.getSimpleName(), "Failed to delete GeoPackage: " + geoPackage, e); } change = true; } } if (change) { updateMap(); } } } }