1

Goal

Download custom tiles via URLs to an Android device and view them using Google Maps. Note that I am not trying to store/cache Google's tiles. My tiles create a heat-map for conveying data and need to be available later when the device is offline.

Problem

I tried implementing 0ne_Up's OfflineTileProvider from here. I modified it slightly so that it works in my map Fragment instead of an Activity. I think it's supposed to display tiles stored on the device, or download and display them if they aren't on the device. However, I receive the following error for every tile that I don't have a URL for (my tiles don't cover the whole map):

java.io.FileNotFoundException: https://exampleDomain.com/mapData/6/32/32.png

And when it tries to load a tile that I do have a URL for, I get this error:

java.io.FileNotFoundException: data_directory/6/33/31.png: open failed: ENOENT (No such file or directory)

Which tells me that perhaps it downloaded it (or at least tried to) but just can't find it? Another weird thing happens when I try to use OpenStreetMap tiles instead (just to test if it's working). I get this error for every tile:

java.io.FileNotFoundException: https://tile.openstreetmap.org/6/34/31.png

I know the tile exists though because I can go to that URL and view it.

What I've tried

The answer linked to above was the most helpful thing I've found so far (despite it giving me an error). I've tried looking it over carefully, but I have no idea what the problem is. I've never made an app download an image before (or anything really, this is my first app). What I tried previously that works is a modified UrlTileProvider (shown below, based on user611447's answer here). It doesn't give any of the above errors and displays all of the tiles (but only when online).

Here's the Java code for my map fragment (without the imports):

/**
 * The base map without any additional content
 */
public class BaseMap extends Fragment
{
    private GoogleMap mMap;
    SupportMapFragment mapFragment;

    // restrict the zoom level to 5 and 10
    private static final int MIN_ZOOM = 5;
    private static final int MAX_ZOOM = 10;

    private static final int TILE_WIDTH = 256;
    private static final int TILE_HEIGHT = 256;


    @Override
    public View onCreateView( LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState )
    {
        // inflate the view
        View view = inflater.inflate( R.layout.fragment_base_map, container, false );

        // initialize map fragment
        mapFragment = (SupportMapFragment)
                getChildFragmentManager().findFragmentById( R.id.map );

        // Async map
        mapFragment.getMapAsync(
            new OnMapReadyCallback()
            {
                /**
                 * Manipulates the map once available.
                 * This callback is triggered when the map is ready to be used.
                 * This is where we can add markers  or lines, add listeners or move the camera.
                 * If Google Play services is not installed on the device, the user will be prompted to install
                 * it inside the SupportMapFragment. This method will only be triggered once the user has
                 * installed Google Play services and returned to the app.
                 */
                @Override
                public void onMapReady( @NonNull GoogleMap googleMap )
                {
                    setUpMap( googleMap );
                }
            }
        );

        // return view
        return view;
    }


    public void setUpMap( @NonNull GoogleMap googleMap )
    {
        // initialize map variables
        mMap = googleMap;
        //mMap.setMapType( GoogleMap.MAP_TYPE_NONE );

        boolean testingOfflineTileProvider = true;

        mMap.setMinZoomPreference(MIN_ZOOM);
        mMap.setMaxZoomPreference(MAX_ZOOM);

        // create the standard options to be used by all markers
        MarkerOptions stdMarkerOptions = new MarkerOptions()
                .icon( BitmapDescriptorFactory.defaultMarker( BitmapDescriptorFactory.HUE_GREEN ) );

        // - - - - - Map tile creation - - - - -
        TileProvider tileProvider;
        if( !testingOfflineTileProvider )
        {
            tileProvider = new UrlTileProvider( TILE_WIDTH, TILE_HEIGHT )
            {
                @Override
                public URL getTileUrl( int x, int y, int zoom )
                {
                    if( !checkTileExists( x, y, zoom ) )
                    {
                        return null;
                    }

                    // Modify the y tile coordinate to convert from TMS to XYZ tiles.
                    // This is necessary because Google Maps uses XYZ standard tiles
                    // but stored data tiles are of the TMS standard.
                    y = ( 1 << zoom ) - y - 1;

                    // Define the URL pattern for the tile images
                    String myUrlStr = String.format("https://exampleDomain.com/mapData/%d/%d/%d.png", zoom, x, y);

                    try
                    {
                        return new URL( myUrlStr );
                    }
                    catch(MalformedURLException e)
                    {
                        throw new AssertionError(e);
                    }
                }
            };
        }
        else
        {
            tileProvider = new OfflineTileProvider();
        }

        //TileOverlay tileOverlay = // not needed unless need access to a TileOverlay object
        mMap.addTileOverlay( new TileOverlayOptions()
                .tileProvider( tileProvider ) );
        // - - - - - End of tile creation - - - - -

        // Add a marker in Libreville
        LatLng libreville = new LatLng( 0.4162, 9.4673 );
        mMap.addMarker( stdMarkerOptions.position(libreville).title("Marker in Libreville") );

        // Move camera to Libreville marker
        CameraUpdate upd = CameraUpdateFactory.newLatLngZoom( libreville, 6 );
        mMap.moveCamera( upd );
    }


    // Check that the tile server supports the requested x, y and zoom.
    private boolean checkTileExists(int x, int y, int zoom)
    {
        return ( zoom >= MIN_ZOOM && zoom <= MAX_ZOOM );
    }


    /**
     * provides tiles to the map when in offline mode, downloads them
     */
    private class OfflineTileProvider implements TileProvider
    {
        private static final String TILES_DIR = "data_directory/";

        private static final int BUFFER_SIZE_FILE = 16384;
        private static final int BUFFER_SIZE_NETWORK = 8192;

        private ConnectivityManager connectivityManager;

        @Override
        public Tile getTile( int x, int y, int zoom )
        {
            if( !checkTileExists( x, y, zoom ) )
            {
                return NO_TILE;
            }

            // Modify the y tile coordinate to convert from TMS to XYZ tiles.
            // Used only for exampleDomain.com !!!
            y = ( 1 << zoom ) - y - 1;

            // Define the URL pattern for the tile images
            String myUrlStr = String.format("https://exampleDomain.com/mapData/%d/%d/%d.png", zoom, x, y);
            String testUrlStr = String.format("https://tile.openstreetmap.org/%d/%d/%d.png", zoom, x, y);

            Log.d( TAG, "getTile( " + x + ", " + y + ", " + zoom + " )" );
            try
            {
                byte[] data;

                File file = new File(TILES_DIR + zoom + "/" + x + "/" + y + ".png");

                if( file.exists() )
                {
                    Log.w(TAG, "Tile file found!");
                    data = readTile( new FileInputStream(file), BUFFER_SIZE_FILE );
                }
                else
                {
                    if( connectivityManager == null )
                    {
                        connectivityManager = (ConnectivityManager) requireActivity()
                                .getSystemService( Context.CONNECTIVITY_SERVICE );
                    }
                    NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
                    if( activeNetworkInfo == null || !activeNetworkInfo.isConnected() )
                    {
                        Log.w(TAG, "No network");
                        return NO_TILE;
                    }

                    Log.d( TAG, "Downloading tile" );
                    data = readTile( new URL( myUrlStr ).openStream(),
                            BUFFER_SIZE_NETWORK );

                    try ( OutputStream out = new BufferedOutputStream( new FileOutputStream(file) ) )
                    {
                        Log.w(TAG, "Writing tile data to file");
                        out.write(data);
                    }
                }

                return new Tile( TILE_WIDTH, TILE_HEIGHT, data );
            }
            catch ( Exception ex )
            {
                Log.e(TAG, "Error loading tile", ex);
                return NO_TILE;
            }
        }


        private byte[] readTile( InputStream in, int bufferSize ) throws IOException
        {
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            try
            {
                int i;
                byte[] data = new byte[bufferSize];

                while ( ( i = in.read( data, 0, bufferSize ) ) != -1 )
                {
                    buffer.write(data, 0, i);
                }
                buffer.flush();

                return buffer.toByteArray();
            }
            finally
            {
                in.close();
                buffer.close();
            }
        }

    // End of OfflineTileProvider class
    }

// End of BaseMap fragment class
}


Matt N.
  • 53
  • 1
  • 10
  • Downloading and/or caching map tiles are against Google Maps API terms of service. https://cloud.google.com/maps-platform/terms 3.2.3 Restrictions Against Misusing the Services (b) No Caching. Customer will not cache Google Maps Content except as expressly permitted under the Maps Service Specific Terms. – JonR85 Jan 21 '22 at 21:14
  • 1
    @JonR85 I am **not** trying to download or cache Google Maps content. I am trying to download my own map tiles. They are entirely my own and are provided by me, not Google. – Matt N. Jan 21 '22 at 21:28
  • https://stackoverflow.com/questions/38169034/use-google-maps-offline-maps-cache-in-google-maps-android-api – JonR85 Jan 22 '22 at 02:01
  • @JonR85 Thanks for the link. Unfortunately I've already looked at all of those posts. The link in the third bullet point of that answer seems promising. However my map tiles are stored on a server and accessed via URLs, so I don't think Asset manager can help. I will consider using osmdroid instead if I have to, but I'd prefer to continue with the Google Maps approach. – Matt N. Jan 24 '22 at 09:09
  • @JonR85 As for the other links: I don't need to cache tiles. I need downloaded tiles to be kept in persistent storage. Since users need to be online to download the tiles, verifying the API key when the app is opened for the first time shouldn't be a problem. – Matt N. Jan 24 '22 at 09:18

0 Answers0