de.hu_berlin.informatik.spws2014.mapever.largeimageview.CachedImage.java Source code

Java tutorial

Introduction

Here is the source code for de.hu_berlin.informatik.spws2014.mapever.largeimageview.CachedImage.java

Source

/* Copyright (C) 2014,2015  Bjrn Stelter
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 */

package de.hu_berlin.informatik.spws2014.mapever.largeimageview;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory.Options;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.support.v4.util.LruCache;
import android.util.Log;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;

// Der LruCache-bezogene Code wurde in Anlehnung an folgendes Tutorial erstellt:
// http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html

public class CachedImage extends LruCache<String, Bitmap> {

    interface CacheMissResolvedCallback {
        public void onCacheMissResolved();
    }

    // ////// CONSTANTS

    // Tilegre (Breite und Hhe, sollte Zweierpotenz sein)
    public static final int TILESIZE = 512;

    // ////// BITMAP, TILE AND CACHE STUFF

    // BitmapRegionDecoder (liest Bildausschnitte aus InputStreams)
    private BitmapRegionDecoder regionDecoder = null;

    // Liste fr Keys der Tiles, die aktuell von TileWorkerTasks generiert werden
    private ArrayList<String> workingTileTasks = new ArrayList<String>();

    // Callback, wenn nach einem Cache-Miss das gesuchte Tile erzeugt und gecachet wurde.
    private CacheMissResolvedCallback cacheMissResolvedCallback;

    // ////////////////////////////////////////////////////////////////////////
    // //////////// CONSTRUCTORS AND INITIALIZATION
    // ////////////////////////////////////////////////////////////////////////

    /**
     * Initialisiert und erzeugt einen Tile-Cache als LRU-Cache.
     * 
     * @param inputStream Stream zur Bilddatei (nur JPEG und PNG)
     * @param cacheCallback Callback, wenn ein Tile nach einem Cache-Miss generiert und im Cache gespeichert wurde.
     * @throws IOException Wird geworfen, wenn BitmapRegionDecoder nicht instanziiert werden kann (falls das Bild
     *             weder JPEG noch PNG ist, oder bei einem anderen IO-Fehler)
     */
    public CachedImage(InputStream inputStream, CachedImage.CacheMissResolvedCallback cacheCallback)
            throws IOException {
        // Tilecache erzeugen durch Aufruf des LruCache<String, Bitmap>-Konstruktors
        super(calculateCacheSize());

        // Callback setzen
        cacheMissResolvedCallback = cacheCallback;

        // BitmapRegionDecoder instanziieren. Wirft bei nicht untersttztem Format (andere als JPEG und PNG)
        // eine IOException.
        regionDecoder = BitmapRegionDecoder.newInstance(inputStream, true);

        if (regionDecoder == null) {
            throw new IOException("BitmapRegionDecoder could not create instance for unknown reasons");
        }
    }

    // ////// LRUCACHE METHODS OVERRIDES

    /**
     * Gre eines Cache-Eintrags (Bitmap) in Kilobyte. Die Gre des Caches insgesamt wird also an der Menge der
     * Bitmapdaten statt an der Anzahl der Eintrge gemessen.
     */
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
        // (getByteCount() (API 12) == getRowBytes() * getHeight())
        return (bitmap.getRowBytes() * bitmap.getHeight()) / 1024;
    }

    /**
     * Berechnet die optimale Cachegre.
     */
    private static int calculateCacheSize() {
        // Get max available VM memory, exceeding this amount will throw an OutOfMemory exception.
        // Stored in kilobytes as LruCache takes an int in its constructor.
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

        Log.d("CachedImage/calculateCacheSize",
                "Memory max: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + " MB, total: "
                        + Runtime.getRuntime().totalMemory() / 1024 / 1024 + " MB, free: "
                        + Runtime.getRuntime().freeMemory() / 1024 / 1024 + " MB");

        // Use 1/8th of the available memory for this memory cache.
        // TODO Gute Wahl? Nein lieber abhngig von max-total(+free) machen, oder? (Und dann vielleicht ruhig
        // .... grer.) (Auerdem: Gertabhngig? Hm :/) (Problem, wenn Cache nicht ausreicht und noch bentigte
        // .... Tiles sofort rausgeworfen werden... Hmmmmm.)
        // (mal 1/4 nehmen und gucken, wies damit so luft)
        final int cacheSize = maxMemory / 4;

        Log.d("CachedImage/calculateCacheSize", "Max memory: " + maxMemory / 1024
                + " MB, thus creating a cache of size " + cacheSize / 1024 + " MB");

        return cacheSize;
    }

    // ////////////////////////////////////////////////////////////////////////
    // //////////// IMAGE PROPERTIES
    // ////////////////////////////////////////////////////////////////////////

    /**
     * Gibt die Breite des Bildes zurck. (Tatschliche Bildgre, auch wenn nur kleinere Teile geladen sind.)
     */
    public int getWidth() {
        return regionDecoder.getWidth();
    }

    /**
     * Gibt die Hhe des Bildes zurck. (Tatschliche Bildgre, auch wenn nur kleinere Teile geladen sind.)
     */
    public int getHeight() {
        return regionDecoder.getHeight();
    }

    // ////////////////////////////////////////////////////////////////////////
    // //////////// VERWALTUNG DES BILDAUSSCHNITT-CACHES
    // ////////////////////////////////////////////////////////////////////////

    /**
     * Generiert aus x, y, scale den Cachekey (x_y_sampleSize).
     * 
     * @param x Linke Eckkoordinate.
     * @param y Obere Eckkoordinate.
     * @param sampleSize Samplesize (n ist 1/n mal so gro wie das Original)
     * @return String x+"_"+y+"_"+sampleSize
     */
    private static String getCacheKey(int x, int y, int sampleSize) {
        return x + "_" + y + "_" + sampleSize;
    }

    /**
     * Liefert Ausschnitt ab x, y mit Samplesize scale zurck, falls im Cache vorhanden, ansonsten null.
     * 
     * @param x Linke Eckkoordinate.
     * @param y Obere Eckkoordinate.
     * @param samplingSize Samplesize (n ist 1/n mal so gro wie das Original)
     * @return Tile-Bitmap oder null
     */
    private Bitmap getCachedTileBitmap(int x, int y, int samplingSize) {
        return get(getCacheKey(x, y, samplingSize));
    }

    /**
     * Speichert gegebenen Tile im Cache.
     * 
     * @param x Linke Eckkoordinate.
     * @param y Obere Eckkoordinate.
     * @param sampleSize Samplesize (n ist 1/n mal so gro wie das Original)
     * @param tile Tile-Bitmap
     */
    private void putTileInCache(int x, int y, int sampleSize, Bitmap tile) {
        if (tile == null) {
            Log.e("CachedImage/putTileInCache", "tile == null, won't put into cache!");
            return;
        }

        // Key erzeugen
        String key = getCacheKey(x, y, sampleSize);

        Log.d("CachedImage/putTileInCache", "Putting tile " + key + " into cache.");

        // Tile im Cache speichern
        put(key, tile);
    }

    /**
     * Generiert Bildausschnitt ab Koordinaten (left, top) mit sampleSize gibt ihn als Bitmap zurck.
     * Sollte nicht direkt aufgerufen werden, sondern asynchron ber einen TileWorkerTask.
     * 
     * @param left Linke Eckkoordinate.
     * @param top Obere Eckkoordinate.
     * @param sampleSize Samplesize (n ist 1/n mal so gro wie das Original)
     * @return Bitmap des Tiles (maximal TILESIZE*TILESIZE Pixel gro)
     */
    private Bitmap generateTileBitmap(int left, int top, int sampleSize) {
        // Key erzeugen
        String key = getCacheKey(left, top, sampleSize);

        // Kein neues Tile generieren, falls es bereits vorhanden ist.
        if (get(key) != null) {
            return null;
        }

        Log.d("CachedImage/generateTileBitmap", "Generating tile " + key + " ...");
        Log.d("CachedImage/generateTileBitmap",
                "Memory max: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + " MB, total: "
                        + Runtime.getRuntime().totalMemory() / 1024 / 1024 + " MB, free: "
                        + Runtime.getRuntime().freeMemory() / 1024 / 1024 + " MB");

        // TODO OutOfMemory-Exceptions auffangen, Cachegre verkleinern? Oder so?

        // Wenn Tile komplett auerhalb des Bildbereichs liegt, gibt es kein Tile.
        // (< 0 statt < -TILESIZE reicht aus, da left,top % TILESIZE = 0 angenommen wird.)
        if (left < 0 || left >= getWidth() || top < 0 || top >= getHeight()) {
            return null;
        }

        // Berechne Mae/Eckpunkte des Tiles (gesampelte Tiles sollen dennoch TILESIZE gro sein, aber der gewnschte
        // Bildausschnitt wird dadurch natrlich grer, daher *sampleSize)
        // min(), um Tile am Rand abschneiden, wenn Bildrest nicht gro genug.
        int right = Math.min(getWidth(), left + sampleSize * TILESIZE);
        int bottom = Math.min(getHeight(), top + sampleSize * TILESIZE);

        // SampleSize festlegen, um groes Bild bei geringer Zoomstufe runterzuskalieren
        Options opts = new Options();
        opts.inSampleSize = sampleSize;

        // Tile generieren und zurckgeben
        return regionDecoder.decodeRegion(new Rect(left, top, right, bottom), opts);
    }

    /**
     * Asynchroner Task, der ein Tile generiert und es anschlieend im Cache speichert.
     */
    class TileWorkerTask extends AsyncTask<Void, Void, Bitmap> {
        private int x, y, sampleSize;

        public TileWorkerTask(int x, int y, int sampleSize) {
            this.x = x;
            this.y = y;
            this.sampleSize = sampleSize;
        }

        @Override
        protected Bitmap doInBackground(Void... params) {
            // Tile generieren
            return generateTileBitmap(x, y, sampleSize);
        }

        @Override
        protected void onPostExecute(Bitmap result) {
            if (result == null) {
                Log.e("TileWorkerTask/onPostExecute", "Generated tile, but it's == null! What?");
                return;
            }

            // Tile in Cache speichern falls ungleich null
            putTileInCache(x, y, sampleSize, result);

            // bei Fertigstellung wird der Eintrag in workingTileTasks entfernt
            workingTileTasks.remove(getCacheKey(x, y, sampleSize));

            // Callback aufrufen, das in der LargeImageView dann this.invalidated.
            if (cacheMissResolvedCallback != null) {
                cacheMissResolvedCallback.onCacheMissResolved();
            }
        }
    }

    /**
     * Gibt den Ausschnitt des Bildes zurck, der bei x,y beginnt und TILESIZE breit und hoch ist, bzw. am Rand kleiner.
     * Tiles werden mit LRU gecachet (nach x, y, scale).
     * 
     * @param x Linke Eckkoordinate.
     * @param y Obere Eckkoordinate.
     * @param sampleSize Samplesize (n ist 1/n mal so gro wie das Original)
     * @return
     */
    public Bitmap getTileBitmap(int x, int y, int sampleSize) {
        // Tile aus Cache laden, falls vorhanden, sonst null.
        Bitmap tile = getCachedTileBitmap(x, y, sampleSize);

        // Tile in asynchronen Task generieren, falls es nicht im Cache gefunden wurde.
        if (tile == null) {
            // Key erzeugen
            String key = getCacheKey(x, y, sampleSize);

            // Prfe zunchst, ob dieser Tile bereits einen laufenden TileWorkerTask hat
            if (workingTileTasks.contains(key)) {
                Log.d("CachedImage/getTileBitmap", "Tile " + key + " is already being generated...");

                // Ja, also kein Bild zurckgeben
                return null;
            } else if (!workingTileTasks.isEmpty()) {
                // Wir generieren immer nur ein Tile zur selben Zeit (siehe #234)
                Log.d("CachedImage/getTileBitmap",
                        "Tile " + key + " not found in cache, but we're already generating a tile... Wait...");
                return null;
            } else {
                Log.d("CachedImage/getTileBitmap", "Tile " + key + " not found in cache -> generating (async)...");

                // Starte Task
                TileWorkerTask task = new TileWorkerTask(x, y, sampleSize);
                task.execute();

                // Wir merken uns, dass dieses Tile jetzt generiert wird, damit bei einem nchsten Aufruf vor der
                // Fertigstellung des Tiles nicht noch ein gleicher Task erzeugt wird.
                workingTileTasks.add(key);

                // null zurckgeben um zu signalisieren, dass NOCH kein Bild vorhanden ist.
                // TileWorkerTask veranlasst nach dem Laden ein invalidate();
                return null;
            }
        }

        return tile;
    }

}