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}