001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer.geoimage;
003
004import java.awt.Graphics2D;
005import java.awt.Image;
006import java.awt.MediaTracker;
007import java.awt.Rectangle;
008import java.awt.Toolkit;
009import java.awt.geom.AffineTransform;
010import java.awt.image.BufferedImage;
011import java.io.ByteArrayOutputStream;
012import java.io.File;
013import java.io.IOException;
014import java.util.ArrayList;
015import java.util.Collection;
016
017import javax.imageio.ImageIO;
018
019import org.apache.commons.jcs.access.behavior.ICacheAccess;
020import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
021import org.openstreetmap.josm.data.cache.JCSCacheManager;
022import org.openstreetmap.josm.gui.MainApplication;
023import org.openstreetmap.josm.gui.layer.geoimage.ImageDisplay.VisRect;
024import org.openstreetmap.josm.spi.preferences.Config;
025import org.openstreetmap.josm.tools.ExifReader;
026import org.openstreetmap.josm.tools.Logging;
027
028/**
029 * Loads thumbnail previews for a list of images from a {@link GeoImageLayer}.
030 *
031 * Thumbnails are loaded in the background and cached on disk for the next session.
032 */
033public class ThumbsLoader implements Runnable {
034    public static final int maxSize = 120;
035    public static final int minSize = 22;
036    public volatile boolean stop;
037    private final Collection<ImageEntry> data;
038    private final GeoImageLayer layer;
039    private MediaTracker tracker;
040    private ICacheAccess<String, BufferedImageCacheEntry> cache;
041    private final boolean cacheOff = Config.getPref().getBoolean("geoimage.noThumbnailCache", false);
042
043    private ThumbsLoader(Collection<ImageEntry> data, GeoImageLayer layer) {
044        this.data = data;
045        this.layer = layer;
046        initCache();
047    }
048
049    /**
050     * Constructs a new thumbnail loader that operates on a geoimage layer.
051     * @param layer geoimage layer
052     */
053    public ThumbsLoader(GeoImageLayer layer) {
054        this(new ArrayList<>(layer.data), layer);
055    }
056
057    /**
058     * Constructs a new thumbnail loader that operates on the image entries
059     * @param entries image entries
060     */
061    public ThumbsLoader(Collection<ImageEntry> entries) {
062        this(entries, null);
063    }
064
065    /**
066     * Initialize the thumbnail cache.
067     */
068    private void initCache() {
069        if (!cacheOff) {
070            cache = JCSCacheManager.getCache("geoimage-thumbnails", 0, 120,
071                    Config.getDirs().getCacheDirectory(true).getPath() + File.separator + "geoimage-thumbnails");
072        }
073    }
074
075    @Override
076    public void run() {
077        Logging.debug("Load Thumbnails");
078        tracker = new MediaTracker(MainApplication.getMap().mapView);
079        for (ImageEntry entry : data) {
080            if (stop) return;
081
082            // Do not load thumbnails that were loaded before.
083            if (!entry.hasThumbnail()) {
084                entry.setThumbnail(loadThumb(entry));
085
086                if (layer != null && MainApplication.isDisplayingMapView()) {
087                    layer.updateBufferAndRepaint();
088                }
089            }
090        }
091        if (layer != null) {
092            layer.thumbsLoaded();
093            layer.updateBufferAndRepaint();
094        }
095    }
096
097    private BufferedImage loadThumb(ImageEntry entry) {
098        final String cacheIdent = entry.getFile().toString()+':'+maxSize;
099
100        if (!cacheOff && cache != null) {
101            try {
102                BufferedImageCacheEntry cacheEntry = cache.get(cacheIdent);
103                if (cacheEntry != null && cacheEntry.getImage() != null) {
104                    Logging.debug(" from cache");
105                    return cacheEntry.getImage();
106                }
107            } catch (IOException e) {
108                Logging.warn(e);
109            }
110        }
111
112        Image img = Toolkit.getDefaultToolkit().createImage(entry.getFile().getPath());
113        tracker.addImage(img, 0);
114        try {
115            tracker.waitForID(0);
116        } catch (InterruptedException e) {
117            Logging.error(" InterruptedException while loading thumb");
118            Thread.currentThread().interrupt();
119            return null;
120        }
121        if (tracker.isErrorID(1) || img.getWidth(null) <= 0 || img.getHeight(null) <= 0) {
122            Logging.error(" Invalid image");
123            return null;
124        }
125
126        final int w = img.getWidth(null);
127        final int h = img.getHeight(null);
128        final int hh, ww;
129        final Integer exifOrientation = entry.getExifOrientation();
130        if (exifOrientation != null && ExifReader.orientationSwitchesDimensions(exifOrientation)) {
131            ww = h;
132            hh = w;
133        } else {
134            ww = w;
135            hh = h;
136        }
137
138        Rectangle targetSize = ImageDisplay.calculateDrawImageRectangle(
139                new VisRect(0, 0, ww, hh),
140                new Rectangle(0, 0, maxSize, maxSize));
141        BufferedImage scaledBI = new BufferedImage(targetSize.width, targetSize.height, BufferedImage.TYPE_INT_RGB);
142        Graphics2D g = scaledBI.createGraphics();
143
144        final AffineTransform scale = AffineTransform.getScaleInstance((double) targetSize.width / ww, (double) targetSize.height / hh);
145        if (exifOrientation != null) {
146            final AffineTransform restoreOrientation = ExifReader.getRestoreOrientationTransform(exifOrientation, w, h);
147            scale.concatenate(restoreOrientation);
148        }
149
150        while (!g.drawImage(img, scale, null)) {
151            try {
152                Thread.sleep(10);
153            } catch (InterruptedException e) {
154                Logging.warn("InterruptedException while drawing thumb");
155                Thread.currentThread().interrupt();
156            }
157        }
158        g.dispose();
159        tracker.removeImage(img);
160
161        if (scaledBI.getWidth() <= 0 || scaledBI.getHeight() <= 0) {
162            Logging.error(" Invalid image");
163            return null;
164        }
165
166        if (!cacheOff && cache != null) {
167            try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
168                ImageIO.write(scaledBI, "png", output);
169                cache.put(cacheIdent, new BufferedImageCacheEntry(output.toByteArray()));
170            } catch (IOException e) {
171                Logging.warn("Failed to save geoimage thumb to cache");
172                Logging.warn(e);
173            }
174        }
175
176        return scaledBI;
177    }
178}