OSMDroid Loading Custom Offline tiles From Assets Folder
Asked Answered
B

2

9

I was wondering whether it was possible to do such a thing. I know that one would need to modify some of the existing code to pull this off but I was wondering if anyone had any direction on where to look and how to do this.

I am placing a few custom tiles on a specific area on the map as a replacement for OSM tiles providers but need them to be stored in the /assets/ folder. Any ideas?

Belkisbelknap answered 2/10, 2012 at 15:44 Comment(0)
S
9

I use the nexts classes to do that.

import java.io.InputStream;

import org.osmdroid.ResourceProxy.string;
import org.osmdroid.tileprovider.util.StreamUtils;

import android.content.res.AssetManager;
import android.graphics.drawable.Drawable;

public class AssetsTileSource extends CustomBitmapTileSourceBase {
        private final AssetManager mAssetManager;

        public AssetsTileSource(final AssetManager assetManager, final String aName, final string aResourceId,
                        final int aZoomMinLevel, final int aZoomMaxLevel, final int aTileSizePixels,
                        final String aImageFilenameEnding) {
                super(aName, aResourceId, aZoomMinLevel, aZoomMaxLevel, aTileSizePixels, aImageFilenameEnding);
                mAssetManager = assetManager;
        }

        @Override
        public Drawable getDrawable(final String aFilePath) {
                InputStream inputStream = null;
                try {
                        inputStream = mAssetManager.open(aFilePath);
                        if (inputStream != null) {
                                final Drawable drawable = getDrawable(inputStream);
                                return drawable;
                        }
                } catch (final Throwable e) {
                        // Tile does not exist in assets folder.
                        // Ignore silently
                } finally {
                        if (inputStream != null) {
                                StreamUtils.closeStream(inputStream);
                        }
                }

                return null;
        }
}

MapTileFileAssetsProvider.java

public class MapTileFileAssetsProvider extends MapTileModuleProviderBase {

            protected ITileSource mTileSource;

            public MapTileFileAssetsProvider(final ITileSource pTileSource) {
                    super(OpenStreetMapTileProviderConstants.NUMBER_OF_TILE_FILESYSTEM_THREADS, OpenStreetMapTileProviderConstants.TILE_FILESYSTEM_MAXIMUM_QUEUE_SIZE);

                    mTileSource = pTileSource;
            }

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

            @Override
            protected String getName() {
                    return "Assets Folder Provider";
            }

            @Override
            protected String getThreadGroupName() {
                    return "assetsfolder";
            }

            @Override
            protected Runnable getTileLoader() {
                    return new TileLoader();
            }

            @Override
            public int getMinimumZoomLevel() {
                    return mTileSource != null ? mTileSource.getMinimumZoomLevel() : MAXIMUM_ZOOMLEVEL;
            }

            @Override
            public int getMaximumZoomLevel() {
                    return mTileSource != null ? mTileSource.getMaximumZoomLevel() : MINIMUM_ZOOMLEVEL;
            }

            @Override
            public void setTileSource(final ITileSource pTileSource) {
                    mTileSource = pTileSource;
            }

            private class TileLoader extends MapTileModuleProviderBase.TileLoader {

                    @Override
                    public Drawable loadTile(final MapTileRequestState pState) throws CantContinueException {

                            if (mTileSource == null) {
                                    return null;
                            }

                            final MapTile pTile = pState.getMapTile();
                            String path = mTileSource.getTileRelativeFilenameString(pTile);

                            Drawable drawable;
                            try {
                                    drawable = mTileSource.getDrawable(path);
                            } catch (final LowMemoryException e) {
                                    // low memory so empty the queue
                                    throw new CantContinueException(e);
                            }

                            return drawable;
                    }
            }
    }

And

import java.io.File;
import java.io.InputStream;
import java.util.Random;

import org.osmdroid.ResourceProxy;
import org.osmdroid.ResourceProxy.string;
import org.osmdroid.tileprovider.ExpirableBitmapDrawable;
import org.osmdroid.tileprovider.MapTile;
import org.osmdroid.tileprovider.constants.OpenStreetMapTileProviderConstants;
import org.osmdroid.tileprovider.tilesource.ITileSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;

public abstract class CustomBitmapTileSourceBase implements ITileSource,
                OpenStreetMapTileProviderConstants {

        private static final Logger logger = LoggerFactory.getLogger(CustomBitmapTileSourceBase.class);

        private static int globalOrdinal = 0;

        private final int mMinimumZoomLevel;
        private final int mMaximumZoomLevel;

        private final int mOrdinal;
        protected final String mName;
        protected final String mImageFilenameEnding;
        protected final Random random = new Random();

        private final int mTileSizePixels;

        private final string mResourceId;

        public CustomBitmapTileSourceBase(final String aName, final string aResourceId,
                        final int aZoomMinLevel, final int aZoomMaxLevel, final int aTileSizePixels,
                        final String aImageFilenameEnding) {
                mResourceId = aResourceId;
                mOrdinal = globalOrdinal++;
                mName = aName;
                mMinimumZoomLevel = aZoomMinLevel;
                mMaximumZoomLevel = aZoomMaxLevel;
                mTileSizePixels = aTileSizePixels;
                mImageFilenameEnding = aImageFilenameEnding;
        }

        @Override
        public int ordinal() {
                return mOrdinal;
        }

        @Override
        public String name() {
                return mName;
        }

        public String pathBase() {
                return mName;
        }

        public String imageFilenameEnding() {
                return mImageFilenameEnding;
        }

        @Override
        public int getMinimumZoomLevel() {
                return mMinimumZoomLevel;
        }

        @Override
        public int getMaximumZoomLevel() {
                return mMaximumZoomLevel;
        }

        @Override
        public int getTileSizePixels() {
                return mTileSizePixels;
        }

        @Override
        public String localizedName(final ResourceProxy proxy) {
                return proxy.getString(mResourceId);
        }

        @Override
        public Drawable getDrawable(final String aFilePath) {
                try {
                        // default implementation will load the file as a bitmap and create
                        // a BitmapDrawable from it
                        final Bitmap bitmap = BitmapFactory.decodeFile(aFilePath);
                        if (bitmap != null) {
                                return new ExpirableBitmapDrawable(bitmap);
                        } else {
                                // if we couldn't load it then it's invalid - delete it
                                try {
                                        new File(aFilePath).delete();
                                } catch (final Throwable e) {
                                        logger.error("Error deleting invalid file: " + aFilePath, e);
                                }
                        }
                } catch (final OutOfMemoryError e) {
                        logger.error("OutOfMemoryError loading bitmap: " + aFilePath);
                        System.gc();
                }
                return null;
        }

        @Override
        public String getTileRelativeFilenameString(final MapTile tile) {
                final StringBuilder sb = new StringBuilder();
                sb.append(pathBase());
                sb.append('/');
                sb.append(tile.getX());
                sb.append('_');
                sb.append(tile.getY());
                sb.append('_');
                sb.append(tile.getZoomLevel());
                sb.append(imageFilenameEnding());
                return sb.toString();
        }


        @Override
        public Drawable getDrawable(final InputStream aFileInputStream) {
                try {
                        // default implementation will load the file as a bitmap and create
                        // a BitmapDrawable from it
                        final Bitmap bitmap = BitmapFactory.decodeStream(aFileInputStream);
                        if (bitmap != null) {
                                return new ExpirableBitmapDrawable(bitmap);
                        }
                        System.gc();
                } catch (final OutOfMemoryError e) {
                        logger.error("OutOfMemoryError loading bitmap");
                        System.gc();
                        //throw new LowMemoryException(e);
                }
                return null;
        }

        public final class LowMemoryException extends Exception {
                private static final long serialVersionUID = 146526524087765134L;

                public LowMemoryException(final String pDetailMessage) {
                        super(pDetailMessage);
                }

                public LowMemoryException(final Throwable pThrowable) {
                        super(pThrowable);
                }
        }
}

Modify method getTileRelativeFilenameString() to get yout tiles (i use the next format: x_y_zoom.png)

Example:

mapView = new MapView(getApplicationContext(), 256);
mapView.setClickable(true);
mapView.setTag("Mapa");
mapView.setTileSource(TileSourceFactory.MAPNIK);
mapView.setMultiTouchControls(true);
mapView.setUseDataConnection(true);

MapTileModuleProviderBase moduleProvider = 
    new MapTileFileAssetsProvider(ASSETS_TILE_SOURCE);
SimpleRegisterReceiver simpleReceiver = 
    new SimpleRegisterReceiver(getApplicationContext());
MapTileProviderArray tileProviderArray = 
    new MapTileProviderArray(ASSETS_TILE_SOURCE, simpleReceiver, 
        new MapTileModuleProviderBase[] { moduleProvider });
TilesOverlay tilesOverlay = 
    new TilesOverlay(tileProviderArray, getApplicationContext());

mapView.getOverlays().add(tilesOverlay);
Sylph answered 14/1, 2013 at 15:1 Comment(4)
How to apply coordinates for navigation in tandem with zoom in/out?Nozzle
The classes are good to use, but the example is not quite complete. For the ASSETS_TILE_SOURCE, it is not a constant, but an object new from the class AssetsTileSource. With the tileProviderArray created, I use in when creating the mapView using the constructor that can take that as an argument, e.g. MapView mapView = new MapView(this, 256, resourceProxy, tileProviderArray). It is more intuitive to me to have the MapView to associate with the tile source, rather than associate it with the overlays ... maybe it's just me.Irrelevant
@Sylph thanks the sample. Won't the Bitmap.decodeStream() cause GC_FOR_ALLOC on the main UI thread?Hardnosed
@Hardnosed not sure about that, i don't experience any issues and works like i expected. Maybe it can improve this point.Sylph
U
3

Instead to read directly from assets I copy/deploy the maptiles zipped (following osmdroid map tiles directory structure format) into osmdroid maptiles directory and then declare 3 tile providers, archive, cache and online provider.

public class MapTileProviderAssets extends MapTileProviderArray 
        implements IMapTileProviderCallback {

    private static final String LOG_TAG = "MapTileProviderAssets";

    private static final String ASSETS_MAP_DIRECTORY = "map";
    private static final String SDCARD_PATH = Environment.getExternalStorageDirectory().getPath();
    private static final String OSMDROID_MAP_FILE_SOURCE_DIRECTORY = "osmdroid";
    private static final String OSMDROID_MAP_FILE_SOURCE_DIRECTORY_PATH = 
            SDCARD_PATH + "/" + OSMDROID_MAP_FILE_SOURCE_DIRECTORY;

    public MapTileProviderAssets(final Context pContext) {
        this(pContext, TileSourceFactory.DEFAULT_TILE_SOURCE);
    }

    public MapTileProviderAssets(final Context pContext, final ITileSource pTileSource) {
        this(pContext, new SimpleRegisterReceiver(pContext), 
                new NetworkAvailabliltyCheck(pContext), pTileSource);

    }

    public MapTileProviderAssets(final Context pContext, final IRegisterReceiver pRegisterReceiver,
                                 final INetworkAvailablityCheck aNetworkAvailablityCheck, 
                                 final ITileSource pTileSource) {
        super(pTileSource, pRegisterReceiver);

        final TileWriter tileWriter = new TileWriter();

        // copy assets delivered in apk into osmdroid map source dir
        // load zip archive first, then cache, then online
        final List<String> zipArchivesRelativePathInAssets = 
                listArchives(pContext.getAssets(), ASSETS_MAP_DIRECTORY);
        for (final String zipFileRelativePathInAssets : zipArchivesRelativePathInAssets) {
            final String copiedFilePath = copyAssetFile(
                    pContext.getAssets(), zipFileRelativePathInAssets, 
                    OSMDROID_MAP_FILE_SOURCE_DIRECTORY);
            Log.d(LOG_TAG, String.format(
                    "Archive zip file copied into map source directory %s", copiedFilePath));
        }
        // list zip files in map archive directory
        final Set<String> setZipFileArchivesPath = new HashSet<String>();
        FileTools.listFiles(setZipFileArchivesPath, new File(
                OSMDROID_MAP_FILE_SOURCE_DIRECTORY_PATH), ".zip", true);
        final Set<IArchiveFile> setZipFileArchives = new HashSet<IArchiveFile>();
        for (final String zipFileArchivesPath : setZipFileArchivesPath) {
            final File zipfile = new File(zipFileArchivesPath);
            final IArchiveFile archiveFile = ArchiveFileFactory.getArchiveFile(zipfile);
            if (archiveFile != null) {
                setZipFileArchives.add(archiveFile);
            }
            setZipFileArchives.add(archiveFile);
            Log.d(LOG_TAG, String.format(
                    "Archive zip file %s added to map source ", zipFileArchivesPath));
        }

        final MapTileFileArchiveProvider archiveProvider;
        Log.d(LOG_TAG, String.format(
                "%s archive zip files will be used as source", setZipFileArchives.size()));
        if (setZipFileArchives.size() > 0) {
            final IArchiveFile[] pArchives = 
                    setZipFileArchives.toArray(new IArchiveFile[setZipFileArchives.size()]);
            archiveProvider = new MapTileFileArchiveProvider(
                    pRegisterReceiver, pTileSource, pArchives);
        } else {
            archiveProvider = new MapTileFileArchiveProvider(
                    pRegisterReceiver, pTileSource);
        }
        mTileProviderList.add(archiveProvider);

        // cache
        final MapTileFilesystemProvider fileSystemProvider = 
                new MapTileFilesystemProvider(pRegisterReceiver, pTileSource);
        mTileProviderList.add(fileSystemProvider);

        // online tiles
        final MapTileDownloader downloaderProvider = 
                new MapTileDownloader(pTileSource, tileWriter, aNetworkAvailablityCheck);
        mTileProviderList.add(downloaderProvider);
    }

    public static List<String> listArchives(final AssetManager assetManager, 
                                            final String subDirectory) {
        final List<String> listArchives = new ArrayList<String>();
        try {
            final String[] lstFiles = assetManager.list(subDirectory);
            if (lstFiles != null && lstFiles.length > 0) {
                for (final String file : lstFiles) {
                    if (isZip(file)) {
                        listArchives.add(subDirectory + "/" + file);
                    }
                    // filter files (xxxxx.xxx format) and parse only directories, 
                    // with out this all files are parsed and
                    // the process is VERY slow
                    // WARNNING: we could have directories with dot for versioning
                    else if (isDirectory(file)) {// (file.lastIndexOf(".") != (file.length() - 4)) {
                        listArchives(assetManager, subDirectory + "/" + file);
                    }
                }
            }
        } catch (final IOException e) {
            Log.w(LOG_TAG, String.format("List error: can't list %s, exception %s", 
                    subDirectory, Log.getStackTraceString(e)));
        } catch (final Exception e) {
            Log.w(LOG_TAG, String.format("List error: can't list %s, exception %s", 
                    subDirectory, Log.getStackTraceString(e)));
        }
        return listArchives;
    }

    private static boolean isZip(final String file) {
        return file.endsWith(".zip");
    }

    private static boolean isDirectory(final String file) {
        return file.lastIndexOf(".") != (file.length() - 4);
    }

    private static String copyAssetFile(final AssetManager assetManager, 
                                        final String assetRelativePath,
                                        final String destinationDirectoryOnSdcard) {
        InputStream in = null;
        OutputStream out = null;
        final String newfilePath = SDCARD_PATH + "/" + 
                destinationDirectoryOnSdcard + "/" + assetRelativePath;
        final File newFile = new File(newfilePath);
        // copy file only if it doesn't exist yet
        if (!newFile.exists()) {
            Log.d(LOG_TAG, String.format(
                    "Copy %s map archive in assets into %s", assetRelativePath, newfilePath));
            try {
                final File directory = newFile.getParentFile();
                if (!directory.exists()) {
                    if (directory.mkdirs()) {
                        // Log.d(LOG_TAG, "Directory created: " + directory.getAbsolutePath());
                    }
                }
                in = assetManager.open(assetRelativePath);
                out = new FileOutputStream(newfilePath);
                copyFile(in, out);
                in.close();
                in = null;
                out.flush();
                out.close();
                out = null;
            } catch (final Exception e) {
                Log.e(LOG_TAG, "Exception during copyAssetFile: " + Log.getStackTraceString(e));
            }
        }
        return newfilePath;
    }

    private static void copyFile(final InputStream in, final OutputStream out) throws IOException {
        final byte[] buffer = new byte[1024];
        int read;
        while ((read = in.read(buffer)) != -1) {
            out.write(buffer, 0, read);
        }
    }

}
Unbelieving answered 12/2, 2013 at 12:38 Comment(2)
Your code is incomplete. There is one method not accesible: it is FileTools.listFiles. I've comment it and I added this line to replace it: setZipFileArchivesPath.add("name_of_your_map_file.zip"); .... I am not sure still if i have to include the string .zip or not as a name of the file. I will check for myself.Spear
Yes the code of FileTools.listFiles is not present because it is easy to implement. This method simply list all the zip files of the inputed map archive directory.Unbelieving

© 2022 - 2024 — McMap. All rights reserved.