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}