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}