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