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            try {
071                cache = JCSCacheManager.getCache("geoimage-thumbnails", 0, 120,
072                        Config.getDirs().getCacheDirectory(true).getPath() + File.separator + "geoimage-thumbnails");
073            } catch (IOException e) {
074                Logging.warn("Failed to initialize cache for geoimage-thumbnails");
075                Logging.warn(e);
076            }
077        }
078    }
079
080    @Override
081    public void run() {
082        Logging.debug("Load Thumbnails");
083        tracker = new MediaTracker(MainApplication.getMap().mapView);
084        for (ImageEntry entry : data) {
085            if (stop) return;
086
087            // Do not load thumbnails that were loaded before.
088            if (!entry.hasThumbnail()) {
089                entry.setThumbnail(loadThumb(entry));
090
091                if (layer != null && MainApplication.isDisplayingMapView()) {
092                    layer.updateBufferAndRepaint();
093                }
094            }
095        }
096        if (layer != null) {
097            layer.thumbsLoaded();
098            layer.updateBufferAndRepaint();
099        }
100    }
101
102    private BufferedImage loadThumb(ImageEntry entry) {
103        final String cacheIdent = entry.getFile().toString()+':'+maxSize;
104
105        if (!cacheOff && cache != null) {
106            try {
107                BufferedImageCacheEntry cacheEntry = cache.get(cacheIdent);
108                if (cacheEntry != null && cacheEntry.getImage() != null) {
109                    Logging.debug(" from cache");
110                    return cacheEntry.getImage();
111                }
112            } catch (IOException e) {
113                Logging.warn(e);
114            }
115        }
116
117        Image img = Toolkit.getDefaultToolkit().createImage(entry.getFile().getPath());
118        tracker.addImage(img, 0);
119        try {
120            tracker.waitForID(0);
121        } catch (InterruptedException e) {
122            Logging.error(" InterruptedException while loading thumb");
123            Thread.currentThread().interrupt();
124            return null;
125        }
126        if (tracker.isErrorID(1) || img.getWidth(null) <= 0 || img.getHeight(null) <= 0) {
127            Logging.error(" Invalid image");
128            return null;
129        }
130
131        final int w = img.getWidth(null);
132        final int h = img.getHeight(null);
133        final int hh, ww;
134        final Integer exifOrientation = entry.getExifOrientation();
135        if (exifOrientation != null && ExifReader.orientationSwitchesDimensions(exifOrientation)) {
136            ww = h;
137            hh = w;
138        } else {
139            ww = w;
140            hh = h;
141        }
142
143        Rectangle targetSize = ImageDisplay.calculateDrawImageRectangle(
144                new VisRect(0, 0, ww, hh),
145                new Rectangle(0, 0, maxSize, maxSize));
146        BufferedImage scaledBI = new BufferedImage(targetSize.width, targetSize.height, BufferedImage.TYPE_INT_RGB);
147        Graphics2D g = scaledBI.createGraphics();
148
149        final AffineTransform scale = AffineTransform.getScaleInstance((double) targetSize.width / ww, (double) targetSize.height / hh);
150        if (exifOrientation != null) {
151            final AffineTransform restoreOrientation = ExifReader.getRestoreOrientationTransform(exifOrientation, w, h);
152            scale.concatenate(restoreOrientation);
153        }
154
155        while (!g.drawImage(img, scale, null)) {
156            try {
157                Thread.sleep(10);
158            } catch (InterruptedException e) {
159                Logging.warn("InterruptedException while drawing thumb");
160                Thread.currentThread().interrupt();
161            }
162        }
163        g.dispose();
164        tracker.removeImage(img);
165
166        if (scaledBI.getWidth() <= 0 || scaledBI.getHeight() <= 0) {
167            Logging.error(" Invalid image");
168            return null;
169        }
170
171        if (!cacheOff && cache != null) {
172            try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
173                ImageIO.write(scaledBI, "png", output);
174                cache.put(cacheIdent, new BufferedImageCacheEntry(output.toByteArray()));
175            } catch (IOException e) {
176                Logging.warn("Failed to save geoimage thumb to cache");
177                Logging.warn(e);
178            }
179        }
180
181        return scaledBI;
182    }
183}