package mil.nga.dice.report; import android.annotation.TargetApi; import android.content.Context; import android.net.Uri; import android.os.Build; import android.util.Log; import android.webkit.WebResourceRequest; import android.webkit.WebResourceResponse; import android.webkit.WebView; import com.google.android.gms.maps.model.LatLng; import java.io.ByteArrayInputStream; import java.io.File; import java.io.InputStream; import java.util.HashMap; import java.util.List; import java.util.Map; import mil.nga.dice.DICEConstants; import mil.nga.dice.io.DICEFileUtils; import mil.nga.dice.map.geopackage.GeoPackageMapData; import mil.nga.dice.map.geopackage.GeoPackageTableMapData; import mil.nga.geopackage.BoundingBox; 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.FeatureDao; import mil.nga.geopackage.tiles.features.FeatureTiles; import mil.nga.geopackage.tiles.features.DefaultFeatureTiles; 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.retriever.GeoPackageTile; import mil.nga.geopackage.tiles.retriever.GeoPackageTileRetriever; import mil.nga.geopackage.tiles.user.TileDao; import mil.nga.geopackage.validate.GeoPackageValidate; /** * GeoPackage Web View for intercepting and responding to GeoPackage tile URL requests */ public class GeoPackageWebViewClient extends WebViewJavascriptBridgeClient { /** * Table URL parameter */ private static final String TABLE_PARAM = "table"; /** * Zoom URL parameter */ private static final String ZOOM_PARAM = "z"; /** * X URL Parameter */ private static final String X_PARAM = "x"; /** * Y URL Parameter */ private static final String Y_PARAM = "y"; /** * GeoPackage manager */ private final GeoPackageManager manager; /** * GeoPackage cache */ private final GeoPackageCache cache; /** * Report Id */ private final String reportId; /** * Map Data map */ private final Map<String, GeoPackageMapData> mapData = new HashMap<>(); /** * Constructor * * @param context app context * @param reportId report id */ public GeoPackageWebViewClient(Context context, String reportId) { super(context); manager = GeoPackageFactory.getManager(context); cache = new GeoPackageCache(manager); this.reportId = reportId; } /** * Close the GeoPackage connections */ public void close() { cache.closeAll(); String like = DICEConstants.DICE_TEMP_CACHE_SUFFIX + "%"; List<String> geoPackages = null; try { geoPackages = manager.databasesLike(like); } catch (Exception e) { Log.e(GeoPackageWebViewClient.class.getSimpleName(), "Failed to find temporary GeoPackages", e); } if (geoPackages != null) { for (String geoPackage : geoPackages) { manager.delete(geoPackage); } } } /** * {@inheritDoc} * <p/> * Handle post lollipop */ @Override @TargetApi(Build.VERSION_CODES.LOLLIPOP) public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { WebResourceResponse response = null; if (request != null) { Uri url = request.getUrl(); response = handleUrl(url); } return response; } /** * {@inheritDoc} * <p/> * Handle pre lollipop // TODO remove when minimum Android version is at or above 21 */ @Override public WebResourceResponse shouldInterceptRequest(WebView view, String url) { WebResourceResponse response = null; if (url != null) { Uri uri = Uri.parse(url); response = handleUrl(uri); } return response; } /** * Handle the URL and respond with an image * * @param url potential GeoPackage URL * @return response */ private WebResourceResponse handleUrl(Uri url) { WebResourceResponse response = null; if (url != null) { String path = url.getPath(); File file = new File(path); if (GeoPackageValidate.hasGeoPackageExtension(file)) { List<String> tables = url.getQueryParameters(TABLE_PARAM); String zoom = url.getQueryParameter(ZOOM_PARAM); String x = url.getQueryParameter(X_PARAM); String y = url.getQueryParameter(Y_PARAM); // If all required parameters exist if (tables != null && !tables.isEmpty() && zoom != null && x != null && y != null) { response = handleUrl(file, tables, zoom, x, y); } else { Log.e(GeoPackageWebViewClient.class.getSimpleName(), "GeoPackage url does not contain all required parameters. Url: " + url + ", Tables: " + tables + ", zoom: " + zoom + ", x: " + x + ", y: " + y); } } } return response; } /** * Handle the URL GeoPackage tile request and respond with an image * * @param file GeoPackage file * @param tables tables to query * @param zoom zoom level * @param x x coordinate * @param y y coordinate * @return response */ private WebResourceResponse handleUrl(File file, List<String> tables, String zoom, String x, String y) { int zoomValue = Integer.parseInt(zoom); int xValue = Integer.parseInt(x); int yValue = Integer.parseInt(y); String nameWithExtension = file.getName(); String name = DICEFileUtils.removeExtension(nameWithExtension); String localPath = ReportUtils.localReportPath(file); String sharedPrefix = reportId + File.separator + DICEConstants.DICE_REPORT_SHARED_DIRECTORY; boolean shared = localPath.startsWith(sharedPrefix); name = reportId(name, reportId, shared); GeoPackage geoPackage = null; if (name != null) { if (manager.exists(name)) { try { geoPackage = cache.getOrOpen(name); } catch (Exception e) { cache.close(name); manager.delete(name); geoPackage = null; } } if (geoPackage == null) { File importFile = file; // If a shared file, check if the file exists in this report or another if (shared) { // If the file is not in this report, search other reports if (!importFile.exists()) { String sharedSearchPath = localPath.substring(reportId.length()); File[] reportDirectories = ReportUtils.getReportDirectories(context); for (File reportDirectory : reportDirectories) { File sharedLocation = new File(reportDirectory, sharedSearchPath); if (sharedLocation.exists()) { importFile = sharedLocation; break; } } } } if (importFile.exists()) { manager.importGeoPackageAsExternalLink(importFile, name); try { geoPackage = cache.getOrOpen(name); } catch (Exception e) { Log.e(GeoPackageWebViewClient.class.getSimpleName(), "Failed to open GeoPackage " + name + " at path: " + importFile.getAbsolutePath(), e); geoPackage = null; } } } } byte[] tileData = null; if (geoPackage != null) { for (String table : tables) { // Get or create the GeoPackage data GeoPackageMapData geoPackageData = mapData.get(name); if (geoPackageData == null) { geoPackageData = new GeoPackageMapData(name); mapData.put(name, geoPackageData); } // Get or create the table data GeoPackageTableMapData tableData = geoPackageData.getTable(table); if (tableData == null) { tableData = new GeoPackageTableMapData(table, geoPackage.isFeatureTable(table)); geoPackageData.addTable(tableData); } else { // Feature Overlay Queries have already been added for this table tableData = null; } if (geoPackage.isTileTable(table)) { TileDao tileDao = geoPackage.getTileDao(table); GeoPackageTileRetriever retriever = new GeoPackageTileRetriever(tileDao); if (retriever.hasTile(xValue, yValue, zoomValue)) { GeoPackageTile tile = retriever.getTile(xValue, yValue, zoomValue); if (tile != null) { tileData = tile.getData(); } } // If the first time handling this table if (tableData != null) { // Check for linked feature tables BoundedOverlay geoPackageTileOverlay = GeoPackageOverlayFactory.getBoundedOverlay(tileDao); 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); } } } else if (geoPackage.isFeatureTable(table)) { FeatureDao featureDao = geoPackage.getFeatureDao(table); FeatureTiles featureTiles = new DefaultFeatureTiles(context, featureDao); FeatureIndexManager indexer = new FeatureIndexManager(context, geoPackage, featureDao); featureTiles.setIndexManager(indexer); if (featureTiles.isIndexQuery() && featureTiles.queryIndexedFeaturesCount(xValue, yValue, zoomValue) > 0) { tileData = featureTiles.drawTileBytes(xValue, yValue, zoomValue); } if (tableData != null && featureTiles.isIndexQuery()) { featureTiles.setIndexManager(indexer); 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); }else{ featureTiles.close(); } } if (tileData != null) { break; } } } WebResourceResponse response = null; if (tileData != null) { InputStream is = new ByteArrayInputStream(tileData); response = new WebResourceResponse("text/html", "UTF-8", is); } return response; } /** * Get the report id prefix * * @param report report name * @return report id prefix */ public static String reportIdPrefix(String report) { String reportIdPrefix = report; if (reportIdPrefix != null) { reportIdPrefix = DICEConstants.DICE_TEMP_CACHE_SUFFIX + reportIdPrefix + "-"; } return reportIdPrefix; } /** * Get the report id with prefix if needed * * @param name cache name * @param report report name * @param share true if a shared cache * @return report id */ public static String reportId(String name, String report, boolean share) { String reportId = name; if (!share) { String reportIdPrefix = reportIdPrefix(report); if (reportIdPrefix != null) { reportId = reportIdPrefix + reportId; } else { reportId = null; } } return reportId; } /** * Get a message from a map click * * @param latLng click location * @param zoom zoom level * @param mapBounds map bounding box * @return click message */ public String mapClickMessage(LatLng latLng, double zoom, BoundingBox mapBounds) { StringBuilder clickMessage = new StringBuilder(); for (GeoPackageMapData geoPackageData : mapData.values()) { String message = geoPackageData.mapClickMessage(latLng, zoom, mapBounds); if (message != null) { if (clickMessage.length() > 0) { clickMessage.append("\n\n"); } clickMessage.append(message); } } return clickMessage.length() > 0 ? clickMessage.toString() : null; } /** * Get the table data from a map click * * @param latLng click location * @param zoom zoom level * @param mapBounds map bounding box * @param includePoints true to include point information * @param includeGeometries true to include all geometry information * @return map of table data */ public Map<String, Object> mapClickTableData(LatLng latLng, double zoom, BoundingBox mapBounds, boolean includePoints, boolean includeGeometries) { Map<String, Object> clickData = new HashMap<>(); for (GeoPackageMapData geoPackageData : mapData.values()) { Map<String, Object> geoPackageClickData = geoPackageData.mapClickTableData(latLng, zoom, mapBounds, includePoints, includeGeometries); if (geoPackageClickData != null) { clickData.put(geoPackageData.getName(), geoPackageClickData); } } return clickData.size() > 0 ? clickData : null; } }