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
}