/*
* Copyright 2014 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.iosched.ui;
import com.google.android.gms.maps.model.Tile;
import com.google.android.gms.maps.model.TileProvider;
import java.io.*;
import com.jakewharton.disklrucache.DiskLruCache;
import static com.google.samples.apps.iosched.util.LogUtils.LOGD;
import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag;
/**
* Wrapper that provides a disk-based LRU cache for a TileProvider.
*
* @see com.jakewharton.disklrucache.DiskLruCache
*/
public class CachedTileProvider implements TileProvider {
private static final String TAG = makeLogTag(SVGTileProvider.class);
private static final String KEY_FORMAT = "%d_%d_%d_%s";
// Index for cache entry streams
private static final int INDEX_DATA = 0;
private static final int INDEX_HEIGHT = 1;
private static final int INDEX_WIDTH = 2;
private final String mKeyTag;
private final TileProvider mTileProvider;
private final DiskLruCache mCache;
/**
* TileProvider that wraps another TileProvider and caches all Tiles in a DiskLruCache.
*
* <p>A {@link com.jakewharton.disklrucache.DiskLruCache} can be reused across multiple instances.
* The keyTag is used to annotate entries for this TileProvider, it is recommended to use a unique
* String for each instance to prevent collisions.
*
* <p>NOTE: The supplied {@link com.jakewharton.disklrucache.DiskLruCache} requires space for
* 3 entries per cached object.
*
* @param keyTag identifier used to identify tiles for this CachedTileProvider instance
* @param tileProvider tiles from this TileProvider will be cached.
* @param cache the cache used to store tiles
*/
public CachedTileProvider(String keyTag, TileProvider tileProvider, DiskLruCache cache) {
mKeyTag = keyTag;
mTileProvider = tileProvider;
mCache = cache;
}
/**
* Load a tile.
* If cached, the data for the tile is read from the underlying cache, otherwise the tile is
* generated by the {@link com.google.android.gms.maps.model.TileProvider} and added to the
* cache.
*
* @param x
* @param y
* @param zoom
* @return
*/
@Override
public Tile getTile(int x, int y, int zoom) {
final String key = CachedTileProvider.generateKey(x, y, zoom, mKeyTag);
Tile tile = getCachedTile(key);
if (tile == null) {
// tile not cached, load from provider and then cache
tile = mTileProvider.getTile(x, y, zoom);
if (cacheTile(key, tile)) {
LOGD(TAG, "Added tile to cache " + key);
}
}
return tile;
}
/**
* Load a tile from cache.
* Returns null if there is no corresponding cache entry or it could not be loaded.
*
* @param key
* @return
*/
private Tile getCachedTile(String key) {
try {
DiskLruCache.Snapshot snapshot = mCache.get(key);
if (snapshot == null) {
// tile is not in cache
return null;
}
final byte[] data = readStreamAsByteArray(snapshot.getInputStream(INDEX_DATA));
final int height = readStreamAsInt(snapshot.getInputStream(INDEX_HEIGHT));
final int width = readStreamAsInt(snapshot.getInputStream(INDEX_WIDTH));
if (data != null) {
LOGD(TAG, "Cache hit for tile " + key);
return new Tile(width, height, data);
}
} catch (IOException e) {
// ignore error
}
return null;
}
private boolean cacheTile(String key, Tile tile) {
try {
DiskLruCache.Editor editor = mCache.edit(key);
if (editor == null) {
// editor is not available
return false;
}
writeByteArrayToStream(tile.data, editor.newOutputStream(INDEX_DATA));
writeIntToStream(tile.height, editor.newOutputStream(INDEX_HEIGHT));
writeIntToStream(tile.width, editor.newOutputStream(INDEX_WIDTH));
editor.commit();
return true;
} catch (IOException e) {
// Tile could not be cached
}
return false;
}
private static String generateKey(int x, int y, int zoom, String tag) {
return String.format(KEY_FORMAT, x, y, zoom, tag);
}
private static void writeByteArrayToStream(byte[] data, OutputStream stream) throws IOException {
try {
stream.write(data);
} finally {
stream.close();
}
}
private static void writeIntToStream(int data, OutputStream stream) throws IOException {
DataOutputStream dos = new DataOutputStream(stream);
try {
dos.writeInt(data);
} finally {
try {
dos.close();
} finally {
stream.close();
}
}
}
private static byte[] readStreamAsByteArray(InputStream inputStream) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int read = 0;
byte[] data = new byte[1024];
try {
while ((read = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, read);
}
} finally {
inputStream.close();
}
return buffer.toByteArray();
}
private static int readStreamAsInt(InputStream inputStream) throws IOException {
DataInputStream buffer = new DataInputStream(inputStream);
try {
return buffer.readInt();
} finally {
inputStream.close();
}
}
}