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}