001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer.geoimage; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Dimension; 007import java.awt.Graphics2D; 008import java.awt.Image; 009import java.awt.geom.AffineTransform; 010import java.awt.image.BufferedImage; 011import java.io.File; 012import java.io.IOException; 013import java.io.UncheckedIOException; 014import java.net.MalformedURLException; 015import java.net.URL; 016import java.util.Collections; 017import java.util.Objects; 018import javax.imageio.IIOParam; 019import javax.imageio.ImageReadParam; 020import javax.imageio.ImageReader; 021 022import org.openstreetmap.josm.data.ImageData; 023import org.openstreetmap.josm.data.gpx.GpxImageEntry; 024import org.openstreetmap.josm.data.imagery.street_level.IImageEntry; 025import org.openstreetmap.josm.tools.ExifReader; 026import org.openstreetmap.josm.tools.ImageProvider; 027import org.openstreetmap.josm.tools.Logging; 028import org.openstreetmap.josm.tools.Utils; 029 030/** 031 * Stores info about each image, with an optional thumbnail 032 * @since 2662 033 */ 034public class ImageEntry extends GpxImageEntry implements IImageEntry<ImageEntry> { 035 036 private Image thumbnail; 037 private ImageData dataSet; 038 039 /** 040 * Constructs a new {@code ImageEntry}. 041 */ 042 public ImageEntry() { 043 } 044 045 /** 046 * Constructs a new {@code ImageEntry} from an existing instance. 047 * @param other existing instance 048 * @since 14625 049 */ 050 public ImageEntry(ImageEntry other) { 051 super(other); 052 thumbnail = other.thumbnail; 053 dataSet = other.dataSet; 054 } 055 056 /** 057 * Constructs a new {@code ImageEntry}. 058 * @param file Path to image file on disk 059 */ 060 public ImageEntry(File file) { 061 super(file); 062 } 063 064 /** 065 * Determines whether a thumbnail is set 066 * @return {@code true} if a thumbnail is set 067 */ 068 public boolean hasThumbnail() { 069 return thumbnail != null; 070 } 071 072 /** 073 * Returns the thumbnail. 074 * @return the thumbnail 075 */ 076 public Image getThumbnail() { 077 return thumbnail; 078 } 079 080 /** 081 * Sets the thumbnail. 082 * @param thumbnail thumbnail 083 */ 084 public void setThumbnail(Image thumbnail) { 085 this.thumbnail = thumbnail; 086 } 087 088 /** 089 * Loads the thumbnail if it was not loaded yet. 090 * @see ThumbsLoader 091 */ 092 public void loadThumbnail() { 093 if (thumbnail == null) { 094 new ThumbsLoader(Collections.singleton(this)).run(); 095 } 096 } 097 098 @Override 099 protected void tmpUpdated() { 100 super.tmpUpdated(); 101 if (this.dataSet != null) { 102 this.dataSet.fireNodeMoved(this); 103 } 104 } 105 106 /** 107 * Set the dataset for this image 108 * @param imageData The dataset 109 * @since 17574 110 */ 111 public void setDataSet(ImageData imageData) { 112 this.dataSet = imageData; 113 } 114 115 /** 116 * Get the dataset for this image 117 * @return The dataset 118 * @since 17574 119 */ 120 public ImageData getDataSet() { 121 return this.dataSet; 122 } 123 124 @Override 125 public int hashCode() { 126 return Objects.hash(super.hashCode(), thumbnail, dataSet); 127 } 128 129 @Override 130 public boolean equals(Object obj) { 131 if (this == obj) 132 return true; 133 if (!super.equals(obj) || getClass() != obj.getClass()) 134 return false; 135 ImageEntry other = (ImageEntry) obj; 136 return Objects.equals(thumbnail, other.thumbnail) && Objects.equals(dataSet, other.dataSet); 137 } 138 139 @Override 140 public ImageEntry getNextImage() { 141 return this.dataSet.getNextImage(); 142 } 143 144 @Override 145 public ImageEntry getPreviousImage() { 146 return this.dataSet.getPreviousImage(); 147 } 148 149 @Override 150 public ImageEntry getFirstImage() { 151 return this.dataSet.getFirstImage(); 152 } 153 154 @Override 155 public void selectImage(ImageViewerDialog imageViewerDialog, IImageEntry<?> entry) { 156 IImageEntry.super.selectImage(imageViewerDialog, entry); 157 if (entry instanceof ImageEntry) { 158 this.dataSet.setSelectedImage((ImageEntry) entry); 159 } 160 } 161 162 @Override 163 public ImageEntry getLastImage() { 164 return this.dataSet.getLastImage(); 165 } 166 167 @Override 168 public boolean isRemoveSupported() { 169 return true; 170 } 171 172 @Override 173 public boolean remove() { 174 this.dataSet.removeImage(this, false); 175 return true; 176 } 177 178 @Override 179 public boolean isDeleteSupported() { 180 return true; 181 } 182 183 @Override 184 public boolean delete() { 185 return Utils.deleteFile(this.getFile()); 186 } 187 188 /** 189 * Reads the image represented by this entry in the given target dimension. 190 * @param target the desired dimension used for {@linkplain IIOParam#setSourceSubsampling subsampling} or {@code null} 191 * @return the read image, or {@code null} 192 * @throws IOException if any I/O error occurs 193 */ 194 @Override 195 public BufferedImage read(Dimension target) throws IOException { 196 URL imageUrl = getImageUrl(); 197 Logging.info(tr("Loading {0}", imageUrl)); 198 BufferedImage image = ImageProvider.read(imageUrl, false, false, 199 r -> target == null ? r.getDefaultReadParam() : withSubsampling(r, target)); 200 if (image == null) { 201 Logging.warn("Unable to load {0}", imageUrl); 202 return null; 203 } 204 Logging.debug("Loaded {0} with dimensions {1}x{2} memoryTaken={3}m exifOrientationSwitchedDimension={4}", 205 imageUrl, image.getWidth(), image.getHeight(), image.getWidth() * image.getHeight() * 4 / 1024 / 1024, 206 ExifReader.orientationSwitchesDimensions(getExifOrientation())); 207 return applyExifRotation(image); 208 } 209 210 protected URL getImageUrl() throws MalformedURLException { 211 return getFile().toURI().toURL(); 212 } 213 214 private ImageReadParam withSubsampling(ImageReader reader, Dimension target) { 215 try { 216 ImageReadParam param = reader.getDefaultReadParam(); 217 Dimension source = new Dimension(reader.getWidth(0), reader.getHeight(0)); 218 if (source.getWidth() > target.getWidth() || source.getHeight() > target.getHeight()) { 219 int subsampling = (int) Math.floor(Math.max( 220 source.getWidth() / target.getWidth(), 221 source.getHeight() / target.getHeight())); 222 param.setSourceSubsampling(subsampling, subsampling, 0, 0); 223 } 224 return param; 225 } catch (IOException e) { 226 throw new UncheckedIOException(e); 227 } 228 } 229 230 private BufferedImage applyExifRotation(BufferedImage img) { 231 Integer exifOrientation = getExifOrientation(); 232 if (!ExifReader.orientationNeedsCorrection(exifOrientation)) { 233 return img; 234 } 235 boolean switchesDimensions = ExifReader.orientationSwitchesDimensions(exifOrientation); 236 int width = switchesDimensions ? img.getHeight() : img.getWidth(); 237 int height = switchesDimensions ? img.getWidth() : img.getHeight(); 238 BufferedImage rotated = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); 239 AffineTransform transform = ExifReader.getRestoreOrientationTransform(exifOrientation, img.getWidth(), img.getHeight()); 240 Graphics2D g = rotated.createGraphics(); 241 g.drawImage(img, transform, null); 242 g.dispose(); 243 return rotated; 244 } 245}