Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated NGA GEOPackage core from 2.0.1 to 6.6.0 #1853

Merged
merged 11 commits into from Mar 31, 2023

Conversation

PWRxPSYCHO
Copy link
Contributor

#1707
#1684
#1706

Looking to update the NGA-Geopackage libraries within OSMDroid to the latest versions.

@eclectice
Copy link

eclectice commented Oct 13, 2022

Because GeoPackage now supports DocumentFile natively, we may be able to provide support for DocumentFile as an alternative to File (polymorphs methods to support both classes) so Geopackage files can be opened from the shared storage with respect to scoped storage introduction since Android 10+.

Even, to make it more flexible, we can simply use the GeoPackage class instead of the File class. A Geopackage file can have multiple tiles tables storing different tile maps. With a single tile source approach, we can separately handle each tile table with a separate GeoPackageMapTileModuleProvider().

Map<String, MapTileModuleProviderBase> providers = new LinkedHashMap<>();
//tiles object stores info about each geopackage file data (database, tile table, current tile table active state)
//so, we can granularly control its data that we want to manipulate
//key is database:table string format
String key = String.join(":", tiles.getDatabase(), tiles.getName());
//handle one geopackage
GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(tiles.getDatabase());
GeoPackageProvider geoPackageProvider = new GeoPackageProvider(geoPackage, activity);

if (tileOverlays.get(key) == null) {
    TilesOverlay tilesOverlay = new TilesOverlay(geoPackageProvider, activity); //with default MapTileProviderBase
    tilesOverlay.setLoadingBackgroundColor(Color.TRANSPARENT);
    tilesOverlay.setLoadingLineColor(Color.TRANSPARENT);
    tilesOverlay.setLoadingDrawable(null);

    tileOverlays.put(key, tilesOverlay);
}

//...
if (!mapView.getOverlayManager().overlays().contains(tileOverlays.get(key))) {
    mapView.getOverlayManager().add(tileOverlays.get(key));
    mapView.setDrawingCacheBackgroundColor(Color.TRANSPARENT);
    mapView.setBackgroundColor(Color.TRANSPARENT);
}

//this geopackage may have multiple tile tables
GeoPackageMapTileModuleProvider provider = geoPackageProvider.geoPackageMapTileModuleProvider();
providers.put(key, provider);
GeoPackageMapTileModuleProvider provider = (GeoPackageMapTileModuleProvider) providers.get(key);
List<GeopackageRasterTileSource> tileSources = provider
        .getTileSources(tiles.getDatabase(), tiles.getName(), tiles.isActive());
tileSources.forEach((tileSource) -> {
    if (tileSource != null) {
        String tileSourceName = ((GeopackageRasterTileSource) tileSource).name();
        //...
        final MapTileFileStorageProviderBase cacheProvider =
                MapTileProviderBasic.getMapTileFileStorageProviderBase(simpleRegisterReceiver, tileSource, tileWriter);
        providers.put(String.join("-", key, "cacheProvider"), cacheProvider);
        //...
        mapView.setTileSource(tileSource);
        mapView.zoomToBoundingBox(((GeopackageRasterTileSource) tileSource).getBounds(), true);
        mapView.getController().setZoom(((GeopackageRasterTileSource) tileSource).getMinimumZoomLevel()*1.0);
    }
});
String[] providerNames = new String[providers.size()];
int i = 0;
for (Map.Entry<String, MapTileModuleProviderBase> entry : providers.entrySet()) {
	if (entry.getKey() != null && entry.getValue() != null) {
		providerNames[i++] = String.format("%s (%s)", entry.getKey(), entry.getValue().getClass().getSimpleName());
	}
}

MapTileProviderArray obj = new MapTileProviderArray(tileSource, new SimpleRegisterReceiver(activity), providerArray);
mapView.setTileProvider(obj);

This improvement is to limit GeoPackageProvider() to a single GeoPackage file so that we may control its tile source(s) more precisely with the required tileprovider(s) to manage a separate tile cache. It will be very simple to control the tile source hide/unhide feature (which is currently missing).

From

public GeoPackageProvider(final IRegisterReceiver pRegisterReceiver,
                              final INetworkAvailablityCheck aNetworkAvailablityCheck, final ITileSource pTileSource,
                              final Context pContext, final IFilesystemCache cacheWriter, File[] databases)

to

public GeoPackageProvider(final IRegisterReceiver pRegisterReceiver,
                                 final INetworkAvailablityCheck aNetworkAvailablityCheck, final ITileSource pTileSource,
                                 final Context pContext, final IFilesystemCache cacheWriter, GeoPackage geoPackage)

From

public GeoPackageMapTileModuleProvider(File[] pFile,
                                           final Context context, IFilesystemCache cache)

to

public GeoPackageMapTileModuleProvider(final Context context, IFilesystemCache cache, GeoPackage geoPackage)

This is my version to idealize the above proposal. I've also hacked a way to control the range of zoom levels allowed on the tile source of each tile table based on the data from the GeoPackage file itself in the getMapTile() method rather than relying on the getMaximumZoomLevel() method. The latter's original method has a global effect: if there are multiple GeoPackage files loaded, the first one loaded will restrict the max zoom level allowed in the MapView which is not desired. The same global effect occurs with the min zoom level of the original getMinimumZoomLevel() method.

GeoPackageProvider

public class GeoPackageProvider extends MapTileProviderArray implements IMapTileProviderCallback {

    protected GeoPackageMapTileModuleProvider packageMapTileModuleProvider;
    protected IFilesystemCache tileWriter;
    protected INetworkAvailablityCheck mNetworkAvailabilityCheck;
    protected GeoPackage geoPackage;

    private final double mInitialZoomLevel = 5;
    private final int mLieFieLagInMillis = 1000;

    public GeoPackageProvider(GeoPackage geoPackage, Context context) {
        this(new SimpleRegisterReceiver(context), new NetworkAvailabliltyCheck(context),
                TileSourceFactory.DEFAULT_TILE_SOURCE, context, null, geoPackage);
    }

    public GeoPackageProvider(final IRegisterReceiver pRegisterReceiver,
                                 final INetworkAvailablityCheck aNetworkAvailablityCheck, final ITileSource pTileSource,
                                 final Context pContext, final IFilesystemCache cacheWriter, GeoPackage geoPackage) {


        super(pTileSource, pRegisterReceiver);
        Log.i(IMapView.LOGTAG, "Geopackage support is BETA. Please report any issues");
        Log.i(IMapView.LOGTAG, String.format("tilesUpdate.GeoPackageProvider()"));

        this.geoPackage = geoPackage;

        if (cacheWriter != null) {
            tileWriter = cacheWriter;
        } else {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD_MR1) {
                tileWriter = new TileWriter();
            } else {
                tileWriter = new SqlTileWriter();
            }
        }

        packageMapTileModuleProvider = new GeoPackageMapTileModuleProvider(pContext, tileWriter, geoPackage);
        mTileProviderList.add(packageMapTileModuleProvider);

        getTileCache().getProtectedTileContainers().add(this);

        final String[] tileProviderList = mTileProviderList.stream().map(t -> t.getTileLoader().toString()).toArray(String[]::new);
        Log.i(IMapView.LOGTAG, String.format("tilesUpdate.GeoPackageProvider(%s): tileProviders(%d) = %s", geoPackage.getName(), tileProviderList.length, Arrays.toString(tileProviderList)));
    }

    public GeoPackageMapTileModuleProvider geoPackageMapTileModuleProvider() {
        return packageMapTileModuleProvider;
    }

    @Override
    public IFilesystemCache getTileWriter() {
        return tileWriter;
    }

    @Override
    public void detach() {
        //@TODO need to understand reason why detach() is called early during app creation
        //https://github.com/osmdroid/osmdroid/issues/213
        //close the writer
        if (tileWriter != null)
            tileWriter.onDetach();
        tileWriter = null;
        packageMapTileModuleProvider.detach();
        super.detach();
    }

    @Override
    public ITileSource getTileSource() {
        return super.getTileSource();
    }

    @Override
    public void setTileSource(final ITileSource aTileSource) {
        super.setTileSource(aTileSource);
        packageMapTileModuleProvider.setTileSource(aTileSource);
        Log.i(IMapView.LOGTAG, String.format("tilesUpdate.GeoPackageProvider.setTileSource(%s) TILESOURCE UPDATED", aTileSource.name()));
    }
}

GeoPackageMapTileModuleProvider

public class GeoPackageMapTileModuleProvider extends MapTileModuleProviderBase {

    public static final String GEO_PACKAGE_EXTENSION = ".gpkg";

    private final TileSystem tileSystem = org.osmdroid.views.MapView.getTileSystem();

    GeoPackage geoPackage;

    protected GeopackageRasterTileSource currentTileSource;

    public GeoPackageMapTileModuleProvider(final Context context, IFilesystemCache cache, GeoPackage geoPackage) {
        super(Configuration.getInstance().getTileFileSystemThreads(), Configuration.getInstance().getTileFileSystemMaxQueueSize());

        Log.i(IMapView.LOGTAG, "Geopackage support is BETA. Please report any issues");

        this.geoPackage = geoPackage;

        if (geoPackage != null) {
            Log.i(IMapView.LOGTAG, String.format("GeoPackageMapTileModuleProvider(%s): current tilesource = %s", geoPackage.getName(), currentTileSource));
        } else {
            Log.i(IMapView.LOGTAG, String.format("GeoPackageMapTileModuleProvider(%s): current tilesource = %s", geoPackage, currentTileSource));
        }
    }

    /**
     * Restrict x to the range [low, high].
     */
    static private int clamp(int x, int low, int high) {
        return x < low ? low : (x > high ? high : x);
    }

    static private long clamp(long x, long low, long high) {
        return x < low ? low : (x > high ? high : x);
    }

    static private double clamp(double x, double low, double high) {
        return x < low ? low : (x > high ? high : x);
    }

    public Drawable getMapTile(final long pMapTileIndex) {

        Drawable tile = null;

        if (currentTileSource != null) {

            String database = currentTileSource.getDatabase();
            String table = currentTileSource.getTableDao();
            boolean isActive = currentTileSource.isActive();

            try {
                TileDao tileDao = geoPackage.getTileDao(table);
                TileRetriever retriever = null;



                TileMatrixSet tileMatrixSet = tileDao.getTileMatrixSet();
                mil.nga.geopackage.BoundingBox displayBoundingBox = tileMatrixSet.getBoundingBox();
                Contents contents = tileMatrixSet.getContents();
                mil.nga.geopackage.BoundingBox contentsBoundingBox = contents.getBoundingBox();

                int zoom = (int) MapTileIndex.getZoom(pMapTileIndex); //clamp(MapTileIndex.getZoom(pMapTileIndex), 0, TileSystem.primaryKeyMaxZoomLevel);
                final long MIN_ZOOM = tileDao.getTileMatrix(tileDao.getMinZoom()).getZoomLevel();
                final long MAX_ZOOM = tileDao.getTileMatrix(tileDao.getMaxZoom()).getZoomLevel();
                final long minZoomOffset = zoom - tileDao.getMinZoom();
                final long maxZoomOffset = zoom - tileDao.getMaxZoom();
                int newZoom = (int) (zoom);
                String[] zoomList = tileDao.getZoomLevels().stream().map(k -> String.valueOf(k.longValue())).toArray(String[]::new);

                int fullMapSizeInPixels = (int) (256 * Math.pow(2, zoom));
                double zoomFactor = TileSystem.getFactor(zoom);

                int x = (int) MapTileIndex.getX(pMapTileIndex); //clamp(MapTileIndex.getX(pMapTileIndex), 0, maxTileColumn);
                int y = (int) MapTileIndex.getY(pMapTileIndex); //clamp(MapTileIndex.getY(pMapTileIndex), 0, maxTileRow);

                long tileIndex = MapTileIndex.getTileIndex(zoom, x, y);
                boolean hasTile = false;

                long mapZoom = 0;
                long matrixWidth = 0;
                long matrixHeight = 0;
                long tileWidth = 0;
                long tileHeight = 0;
                double pixel_x_size = 0;
                double pixel_y_size = 0;

                mil.nga.geopackage.BoundingBox boundingBox = tileDao.getBoundingBox(); //default

                double minLonWgs84 = 0;
                double maxLonWgs84 = 0;
                double minLatWgs84 = 0;
                double maxLatWgs84 = 0;
                double minLonMerc = 0;
                double maxLonMerc = 0;
                double minLatMerc = 0;
                double maxLatMerc = 0;

                double minLon = 0;
                double maxLon = 0;
                double minLat = 0;
                double maxLat = 0;

                {

                    mil.nga.geopackage.BoundingBox boundingBox3 = tileDao.getBoundingBox();
                    double northOri = maxLatMerc = boundingBox3.getMaxLatitude(); //MaxY
                    double eastOri = maxLonMerc = boundingBox3.getMaxLongitude(); //MaxX
                    double southOri = minLatMerc = boundingBox3.getMinLatitude(); //MinY
                    double westOri = minLonMerc = boundingBox3.getMinLongitude(); //MinX

                    if (maxLatMerc > tileSystem.getMaxLatitude() || minLatMerc < tileSystem.getMinLatitude()) {
                        ProjectionTransform toWgs84 = tileDao.getProjection().getTransformation(ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM);
                        mil.nga.geopackage.BoundingBox boundingBox2 = new mil.nga.geopackage.BoundingBox(westOri, southOri, eastOri, northOri);
                        boundingBox = boundingBox2.transform(toWgs84);
                    } else {
                        boundingBox = boundingBox3;
                    }

                    double north = Math.min(tileSystem.getMaxLatitude(), boundingBox.getMaxLatitude()); //MaxY
                    double east = boundingBox.getMaxLongitude(); //MaxX
                    double south = Math.max(tileSystem.getMinLatitude(), boundingBox.getMinLatitude()); //MinY
                    double west = boundingBox.getMinLongitude(); //MinX
                    BoundingBox bounds = new BoundingBox(north, east, south, west);

                    ProjectionTransform toWgs84 = tileDao.getProjection().getTransformation(ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM);
                    mil.nga.geopackage.BoundingBox boundingBox2 = tileDao.getBoundingBox().transform(toWgs84);
                    if (boundingBox2 != null) {
                        minLon = boundingBox2.getMinLongitude();
                        maxLon = boundingBox2.getMaxLongitude();
                        minLat = boundingBox2.getMinLatitude();
                        maxLat = boundingBox2.getMaxLatitude();
                    }
                }

                final double minLonX = tileSystem.getLongitudeFromTileX(x, zoom); //minX
                final double minLatY = tileSystem.getLatitudeFromTileY(y, zoom); //minY
                final double maxLonX = tileSystem.getLongitudeFromTileX(x + 1, zoom); //maxX
                final double maxLatY = tileSystem.getLatitudeFromTileY(y + 1, zoom); //maxY

                final long pixelMinX = tileSystem.getMercatorXFromLongitude(minLonMerc, fullMapSizeInPixels, true);
                final long pixelMinY = tileSystem.getMercatorYFromLatitude(minLatMerc, fullMapSizeInPixels, true);
                final long pixelMaxX = tileSystem.getMercatorXFromLongitude(maxLonMerc, fullMapSizeInPixels, true);
                final long pixelMaxY = tileSystem.getMercatorYFromLatitude(maxLatMerc, fullMapSizeInPixels, true);

                final long tileMinX = tileSystem.getTileXFromLongitude(minLonMerc, zoom);
                final long tileMinY = tileSystem.getTileYFromLatitude(minLatMerc, zoom);
                final long tileMaxX = tileSystem.getTileXFromLongitude(maxLonMerc, zoom);
                final long tileMaxY = tileSystem.getTileYFromLatitude(maxLatMerc, zoom);

                int offsetX = (int) (x);
                int offsetY = (int) (y);

                TileMatrix tileMatrix = tileDao.getTileMatrix(newZoom);

                if (tileMatrix != null) {
                    mapZoom = tileDao.getMapZoom(tileMatrix);
                    matrixWidth = tileMatrix.getMatrixWidth();
                    matrixHeight = tileMatrix.getMatrixHeight();
                    tileWidth = tileMatrix.getTileWidth();
                    tileHeight = tileMatrix.getTileHeight();
                    pixel_x_size = tileMatrix.getPixelXSize();
                    pixel_y_size = tileMatrix.getPixelYSize();
                }

                try {
                    retriever = new GeoPackageTileRetriever(tileDao);
                } catch (Exception gpkgExp) {
                    Log.i(IMapView.LOGTAG, gpkgExp.getMessage());
                }

                if (retriever != null) {
                    hasTile = retriever.hasTile(offsetX, offsetY, newZoom);

                    if (hasTile) {

                        GeoPackageTile geoPackageTile = retriever.getTile(offsetX, offsetY, newZoom);

                        if (geoPackageTile != null && geoPackageTile.getData() != null) {
                            //don't show a tile if beyond the max zoom allowed by this raster and not active
                            if (zoom >= MIN_ZOOM && zoom <= MAX_ZOOM) {

                                Log.i(IMapView.LOGTAG, String.format("getMapTile(): Tile index %d (%s:%s:%s) => %d /%d/%d/%d => zoomFactor(%f) => mapZoom(%d) => lonWgs84(%f, %f) => latWgs84(%f, %f) => lonMerc(%f, %f) => latMerc(%f, %f) => tileX(%d, %d) => tileY(%d, %d) => tile /%d/%d/%d => tile_size(%d, %d) => pixel_size (%f, %f) => fullMapSizeInPixels(%d) => URL: %s = zoomList: %s",
                                        tileIndex,
                                        database, table, hasTile,
                                        pMapTileIndex,
                                        zoom, x, y,
                                        zoomFactor,
                                        mapZoom,
                                        minLon, maxLon,
                                        minLat, maxLat,
                                        minLonMerc, maxLonMerc,
                                        minLatMerc, maxLatMerc,
                                        tileMinX, tileMaxX,
                                        tileMinY, tileMaxY,
                                        newZoom, offsetX, offsetY,
                                        tileWidth, tileHeight,
                                        pixel_x_size, pixel_y_size, fullMapSizeInPixels,
                                        currentTileSource != null ? currentTileSource.getTileURLString(pMapTileIndex) : "null",
                                        Arrays.toString(zoomList)));

                                tile = new BitmapDrawable(BitmapConverter.toBitmap(geoPackageTile.getData()));
                            }
                        }
                    }
                }
            }
            catch (java.lang.IllegalStateException e) {
                Log.w(IMapView.LOGTAG, "IllegalStateException downloading MapTile: " + MapTileIndex.toString(pMapTileIndex) + " : " + e);
            }
        }

        return tile;

    }

    /**
     * returns ALL available raster tile sources for the specified database.
     * This will throw if the database doesn't exist or isn't registered
     *
     * @return
     */
    public List<GeopackageRasterTileSource> getTileSources(String database, String tileTable, boolean isActive) {

        List<GeopackageRasterTileSource> srcs = new ArrayList<>();

        if (geoPackage != null)
        {
            {
                TileDao tileDao = geoPackage.getTileDao(tileTable);

                final long MIN_ZOOM = tileDao.getMapZoom(tileDao.getTileMatrix(tileDao.getMinZoom()));
                final long MAX_ZOOM = tileDao.getMapZoom(tileDao.getTileMatrix(tileDao.getMaxZoom()));

                {

                    mil.nga.geopackage.BoundingBox boundingBox3 = tileDao.getBoundingBox(/*MAX_ZOOM*/ tileDao.getMaxZoom());
                    double northOri = boundingBox3.getMaxLatitude(); //MaxY
                    double eastOri = boundingBox3.getMaxLongitude(); //MaxX
                    double southOri = boundingBox3.getMinLatitude(); //MinY
                    double westOri = boundingBox3.getMinLongitude(); //MinX

                    mil.nga.geopackage.BoundingBox boundingBox = null;
                    if (northOri > tileSystem.getMaxLatitude() || southOri < tileSystem.getMinLatitude()) {
                        ProjectionTransform transform = tileDao.getProjection().getTransformation(ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM);
                        mil.nga.geopackage.BoundingBox boundingBox2 = new mil.nga.geopackage.BoundingBox(westOri, southOri, eastOri, northOri);
                        boundingBox = boundingBox2.transform(transform);
                    } else {
                        boundingBox = boundingBox3;
                    }

                    double north = Math.min(tileSystem.getMaxLatitude(), boundingBox.getMaxLatitude()); //MaxY
                    double east = boundingBox.getMaxLongitude(); //MaxX
                    double south = Math.max(tileSystem.getMinLatitude(), boundingBox.getMinLatitude()); //MinY
                    double west = boundingBox.getMinLongitude(); //MinX
                    org.osmdroid.util.BoundingBox bounds = new org.osmdroid.util.BoundingBox(north, east, south, west);

                    Log.i(IMapView.LOGTAG, String.format("GeoPackageMapTileModuleProvider.getTileSources(%s:%s) zoom(%d:%d), CRS (%s) => (%s), bounding box from (N=max_y=%f, E=max_x=%f, S=min_y=%f, W=min_x=%f) => bounding box to (N=max_y=%f, E=max_x=%f, S=min_y=%f, W=min_x=%f)",
                            database, tileTable,
                            (int) MIN_ZOOM, (int) MAX_ZOOM,
                            tileDao.getProjection().getCode(), String.valueOf(ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM),
                            northOri, eastOri, southOri, westOri,
                            north, east, south, west));

                    //must project in WGS84
                    currentTileSource = new GeopackageRasterTileSource(database, tileTable, (int) MIN_ZOOM, (int) MAX_ZOOM, bounds, isActive);
                    srcs.add(currentTileSource);
                }

                Log.i(IMapView.LOGTAG, String.format("GeoPackageMapTileModuleProvider.getTileSources(%s:%s): size = %d, current tilesource = %s", database, tileTable, srcs.size(), currentTileSource));
            }
        }

        return srcs;
    }

    //@Override
    //public void detach() {
    //    //@TODO need to understand reason why detach() is called early during app creation
    //    super.detach();
    //    Log.i(IMapView.LOGTAG, String.format("GeoPackageMapTileModuleProvider.detach(%s): current tilesource = %s", geoPackage.getName(), currentTileSource));
    //}


    protected class TileLoader extends MapTileModuleProviderBase.TileLoader {

        @Override
        public Drawable loadTile(final long pMapTileIndex) {
            try {
                Drawable mapTile = getMapTile(pMapTileIndex);
                return mapTile;
            } catch (final Throwable e) {
                Log.e(IMapView.LOGTAG, "Error loading tile", e);
            } finally {
            }

            return null;
        }
    }

    @Override
    protected String getName() {
        return "Geopackage";
    }

    @Override
    protected String getThreadGroupName() {
        return getName();
    }

    @Override
    public GeoPackageMapTileModuleProvider.TileLoader getTileLoader() {
        return new GeoPackageMapTileModuleProvider.TileLoader();
    }

    @Override
    public boolean getUsesDataConnection() {
        return false;
    }

    @Override
    public int getMinimumZoomLevel() {
    // hackish solution to make sure we can still zoom to the OsmDroid min level
    // ignoring the tile source min zoom level; we check for this while retrieving
    // the tile at the given tile index instead
        //if (currentTileSource != null)
        //    return currentTileSource.getMinimumZoomLevel();
        return 0;
    }

    @Override
    public int getMaximumZoomLevel() {
    // hackish solution to make sure we can still zoom to the OsmDroid max level
    // ignoring the tile source max zoom level; we check for this while retrieving
    // the tile at the given tile index instead
        //if (currentTileSource != null)
        //    return currentTileSource.getMaximumZoomLevel();
        return 29;
    }

    @Override
    public void setTileSource(ITileSource tileSource) {
        if (tileSource instanceof GeopackageRasterTileSource) {
            if (currentTileSource != null && currentTileSource.isActive()) {
                currentTileSource = (GeopackageRasterTileSource) tileSource;
            }
        }
    }

}

GeopackageRasterTileSource

public class GeopackageRasterTileSource extends XYTileSource {
    private String database;
    private String tableDao;
    private BoundingBox bounds;
    private boolean isActive;

    public GeopackageRasterTileSource(String database, String table, int aZoomMinLevel, int aZoomMaxLevel, BoundingBox bbox, boolean isActive) {
        super(database + ":" + table, aZoomMinLevel, aZoomMaxLevel, 256, "png", new String[]{""});
        Log.i(IMapView.LOGTAG, "Geopackage support is BETA. Please report any issues");
        this.database = database;
        this.tableDao = table;
        this.bounds = bbox;
        this.isActive = isActive;
    }

    public BoundingBox getBounds() {
        return bounds;
    }

    public void setBounds(BoundingBox bounds) {
        this.bounds = bounds;
    }

    public String getDatabase() {
        return database;
    }

    public void setDatabase(String database) {
        this.database = database;
    }

    public String getTableDao() {
        return tableDao;
    }

    public void setTableDao(String tableDao) {
        this.tableDao = tableDao;
    }

    public boolean isActive() {
        return isActive;
    }

    public void setActive(boolean active) {
        isActive = active;
    }
}

Discussions on my approach can be found here: #1709 (comment)

Next support should be able to tell whether a table in a GeoPackage file is a features-based (vector type), a tiles-based (raster type), or feature tiles.

Copy link
Collaborator

@monsieurtanuki monsieurtanuki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me @PWRxPSYCHO!

@PWRxPSYCHO
Copy link
Contributor Author

Because GeoPackage now supports DocumentFile natively, we may be able to provide support for DocumentFile as an alternative to File (polymorphs methods to support both classes) so Geopackage files can be opened from the shared storage with respect to scoped storage introduction since Android 10+.

Even, to make it more flexible, we can simply use the GeoPackage class instead of the File class. A Geopackage file can have multiple tiles tables storing different tile maps. With a single tile source approach, we can separately handle each tile table with a separate GeoPackageMapTileModuleProvider().

Map<String, MapTileModuleProviderBase> providers = new LinkedHashMap<>();
//tiles object stores info about each geopackage file data (database, tile table, current tile table active state)
//so, we can granularly control its data that we want to manipulate
//key is database:table string format
String key = String.join(":", tiles.getDatabase(), tiles.getName());
//handle one geopackage
GeoPackage geoPackage = geoPackageViewModel.getGeoPackage(tiles.getDatabase());
GeoPackageProvider geoPackageProvider = new GeoPackageProvider(geoPackage, activity);

if (tileOverlays.get(key) == null) {
    TilesOverlay tilesOverlay = new TilesOverlay(geoPackageProvider, activity); //with default MapTileProviderBase
    tilesOverlay.setLoadingBackgroundColor(Color.TRANSPARENT);
    tilesOverlay.setLoadingLineColor(Color.TRANSPARENT);
    tilesOverlay.setLoadingDrawable(null);

    tileOverlays.put(key, tilesOverlay);
}

//...
if (!mapView.getOverlayManager().overlays().contains(tileOverlays.get(key))) {
    mapView.getOverlayManager().add(tileOverlays.get(key));
    mapView.setDrawingCacheBackgroundColor(Color.TRANSPARENT);
    mapView.setBackgroundColor(Color.TRANSPARENT);
}

//this geopackage may have multiple tile tables
GeoPackageMapTileModuleProvider provider = geoPackageProvider.geoPackageMapTileModuleProvider();
providers.put(key, provider);
GeoPackageMapTileModuleProvider provider = (GeoPackageMapTileModuleProvider) providers.get(key);
List<GeopackageRasterTileSource> tileSources = provider
        .getTileSources(tiles.getDatabase(), tiles.getName(), tiles.isActive());
tileSources.forEach((tileSource) -> {
    if (tileSource != null) {
        String tileSourceName = ((GeopackageRasterTileSource) tileSource).name();
        //...
        final MapTileFileStorageProviderBase cacheProvider =
                MapTileProviderBasic.getMapTileFileStorageProviderBase(simpleRegisterReceiver, tileSource, tileWriter);
        providers.put(String.join("-", key, "cacheProvider"), cacheProvider);
        //...
        mapView.setTileSource(tileSource);
        mapView.zoomToBoundingBox(((GeopackageRasterTileSource) tileSource).getBounds(), true);
        mapView.getController().setZoom(((GeopackageRasterTileSource) tileSource).getMinimumZoomLevel()*1.0);
    }
});
String[] providerNames = new String[providers.size()];
int i = 0;
for (Map.Entry<String, MapTileModuleProviderBase> entry : providers.entrySet()) {
	if (entry.getKey() != null && entry.getValue() != null) {
		providerNames[i++] = String.format("%s (%s)", entry.getKey(), entry.getValue().getClass().getSimpleName());
	}
}

MapTileProviderArray obj = new MapTileProviderArray(tileSource, new SimpleRegisterReceiver(activity), providerArray);
mapView.setTileProvider(obj);

This improvement is to limit GeoPackageProvider() to a single GeoPackage file so that we may control its tile source(s) more precisely with the required tileprovider(s) to manage a separate tile cache. It will be very simple to control the tile source hide/unhide feature (which is currently missing).

From

public GeoPackageProvider(final IRegisterReceiver pRegisterReceiver,
                              final INetworkAvailablityCheck aNetworkAvailablityCheck, final ITileSource pTileSource,
                              final Context pContext, final IFilesystemCache cacheWriter, File[] databases)

to

public GeoPackageProvider(final IRegisterReceiver pRegisterReceiver,
                                 final INetworkAvailablityCheck aNetworkAvailablityCheck, final ITileSource pTileSource,
                                 final Context pContext, final IFilesystemCache cacheWriter, GeoPackage geoPackage)

From

public GeoPackageMapTileModuleProvider(File[] pFile,
                                           final Context context, IFilesystemCache cache)

to

public GeoPackageMapTileModuleProvider(final Context context, IFilesystemCache cache, GeoPackage geoPackage)

This is my version to idealize the above proposal. I've also hacked a way to control the range of zoom levels allowed on the tile source of each tile table based on the data from the GeoPackage file itself in the getMapTile() method rather than relying on the getMaximumZoomLevel() method. The latter's original method has a global effect: if there are multiple GeoPackage files loaded, the first one loaded will restrict the max zoom level allowed in the MapView which is not desired. The same global effect occurs with the min zoom level of the original getMinimumZoomLevel() method.
GeoPackageProvider
GeoPackageMapTileModuleProvider
GeopackageRasterTileSource

Discussions on my approach can be found here: #1709 (comment)

Next support should be able to tell whether a table in a GeoPackage file is a features-based (vector type), a tiles-based (raster type), or feature tiles.

@eclectice Are these changes something that will be added to OSMDroid in the next release? Or is this a suggestion for developers who wish to use this feature going forward?

@PWRxPSYCHO
Copy link
Contributor Author

@monsieurtanuki The CI looks like it is failing for an API version. Should I address that? Or could that be a breaking change for the application as a whole?

@monsieurtanuki
Copy link
Collaborator

The CI looks like it is failing for an API version. Should I address that? Or could that be a breaking change for the application as a whole?

That does look like a breaking change.
Given that this could impact only the geopackage package (in its own build.gradle), and assuming that your PR is really worth it, we could upgrade only the geopackage package to sdk 32.
Or, maybe you're able to identify what is causing this "min sdk 32" error - if it's something unimportant we can get rid of it.
Actually, I don't know very much about those things. @spyhunter99?

@eclectice
Copy link

@PWRxPSYCHO I suggest it as a proposal to be implemented once your change has been successfully committed, as I was unable to push my change owing to an unknown OsmDroid build issue with Gradle-Fury last time. I'm also unfamiliar with the Git tool.

On minimum SDK version, there is a note regarding Android platform support

Starting with AGP 7.3.0-beta05, the highest supported minimum SDK version is 33 (you can use minSdk = 33). The minimum SDK represents the minimum version of Android that your app can run on and is set in the app-level build.gradle file.

https://developer.android.com/studio/releases/gradle-plugin#android-platform-support

@monsieurtanuki
Copy link
Collaborator

Starting with AGP 7.3.0-beta05

We don't need to go that high, do we?

@eclectice
Copy link

eclectice commented Oct 13, 2022

Starting with AGP 7.3.0-beta05

We don't need to go that high, do we?

My current AGP 7.3.0 build (which I will update this info soon) generated this lint error demanding minSDKversion 24. https://issuetracker.google.com/issues/219091668?hl=id

@monsieurtanuki
Copy link
Collaborator

My current AGP 7.3.0 build (which I will update this info soon) generated this lint error demanding minSDKversion 24.

That would be problematic for my Lollipop smartphone (21).

@eclectice
Copy link

My current AGP 7.3.0 build (which I will update this info soon) generated this lint error demanding minSDKversion 24.

That would be problematic for my Lollipop smartphone (21).

This is my current project build.gradle that is causing the lint error (I upgraded to Android Studio Dolphin 2021.3.1 last month);

buildscript {
    repositories {
        mavenCentral()
        google()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:7.3.0"
        classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.19"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10"
        classpath 'com.google.gradle:osdetector-gradle-plugin:1.7.1'
    }
}

ext {
    android_build_sdk_version = "33"
    android_compile_sdk_version = "android-33"
    if (USE_LOCAL_USB_LIB) {
        android_build_min_sdk_version = "24"
    } else {
        android_build_min_sdk_version = "24"
    }
    android_build_target_sdk_version = "33"
    android_build_tools_version = "30.0.3"

    buildToolsVersion = "$android_build_tools_version"
    compileSdkVersion = android_compile_sdk_version
    minSdkVersion = android_build_min_sdk_version
    targetSdkVersion = android_build_target_sdk_version
}

wrapper {
    gradleVersion = "7.5.1"
    //noinspection UnnecessaryQualifiedReference
    distributionType = Wrapper.DistributionType.ALL
}

@PWRxPSYCHO
Copy link
Contributor Author

PWRxPSYCHO commented Oct 13, 2022

Latest CI build issue is related to this: j256/ormlite-android#141

The compileSDK bump is no longer required

@eclectice
Copy link

Latest CI build issue is related to this: j256/ormlite-android#141

The compileSDK bump is no longer required

It is just a variable but it is not applied in the android {} entry

@eclectice
Copy link

eclectice commented Oct 13, 2022

Latest CI build issue is related to this: j256/ormlite-android#141

The compileSDK bump is no longer required

This is how I solved dependency duplicates between libraries that use different versions, and how I implemented my local version of OsmDroid Geopackage API support without using the same class package names. For example, I created SafGeoPackageProvider as a replacement for OsmDroid GeoPackageProvider version so that I could use the most recent GeoPackage API version.

dependencies {
    api 'mil.nga.geopackage:geopackage-android:6.6.0'
    implementation('org.osmdroid:osmdroid-geopackage:6.1.14') {
        exclude group: 'org.osmdroid.gpkg'
        exclude module: 'ormlite-core'
        exclude group: 'com.j256.ormlite'
    }
    implementation('com.github.MKergall:osmbonuspack:6.9.0') {
        exclude group: 'org.osmdroid.gpkg'
    }
}

android {
    packagingOptions {
        excludes += "DebugProbesKt.bin"
        excludes += "com/j256/ormlite/core/LICENSE.txt"
    }
}

@PWRxPSYCHO
Copy link
Contributor Author

Last issue seems to be on the DocumentFile

There are 2 different versions being used. Android.x 1.0.1 & android support: 28.0.0

@eclectice
Copy link

Latest CI build issue is related to this: j256/ormlite-android#141
The compileSDK bump is no longer required

It is just a variable but it is not applied in the android {} entry

Hmm, compileSdkVersion is still required in the android {} section since AGP complains about it if missing during the build.

@PWRxPSYCHO
Copy link
Contributor Author

PWRxPSYCHO commented Oct 14, 2022

Latest CI build issue is related to this: j256/ormlite-android#141
The compileSDK bump is no longer required

It is just a variable but it is not applied in the android {} entry

Hmm, compileSdkVersion is still required in the android {} section since AGP complains about it if missing during the build.

The original compileSdkVersion issue we ran into was resolved. I was using version 6.5.0 of Geopackage-Android which bumped the version up, but in 6.6.0 the version was bumped back down so that got rid of that issue.

If you checkout the CI run here: https://github.com/ScriptTactics/osmdroid/actions/runs/3244600273/jobs/5320984386#step:5:327

You'll see that it's clashing with the document file issue. Android-Support brings in version 28.0.0 while the NGA-Geopackage-Android brings in Androidx which uses version 1.0.1.

I am also running CI on my newest PR: #1856 ->
https://github.com/ScriptTactics/osmdroid/actions/runs/3245757323/jobs/5323696698

This has all the changes in from this PR and the other PR(#1856). (Removing all android-support libraries and updating to Androidx) and there does not seem to be any issues about dependencies or duplicates

@eclectice
Copy link

Because of changes in the GeoPackage API since v2.0.1, this WIKI must be updated.
https://github.com/osmdroid/osmdroid/wiki/Geopackage-Support

@spyhunter99 spyhunter99 merged commit ddfef5e into osmdroid:master Mar 31, 2023
@spyhunter99
Copy link
Collaborator

merged, feel free to update the wiki

@PWRxPSYCHO PWRxPSYCHO deleted the feature/#1707 branch April 1, 2023 20:42
@PWRxPSYCHO PWRxPSYCHO changed the title Updated NGA GEOPackage core from 2.0.1 to 6.5.0 Updated NGA GEOPackage core from 2.0.1 to 6.6.0 Apr 18, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants