001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer.geoimage;
003
004import java.awt.Image;
005import java.io.File;
006import java.io.IOException;
007import java.util.Collections;
008import java.util.Date;
009
010import org.openstreetmap.josm.Main;
011import org.openstreetmap.josm.data.SystemOfMeasurement;
012import org.openstreetmap.josm.data.coor.CachedLatLon;
013import org.openstreetmap.josm.data.coor.LatLon;
014import org.openstreetmap.josm.tools.ExifReader;
015
016import com.drew.imaging.jpeg.JpegMetadataReader;
017import com.drew.lang.CompoundException;
018import com.drew.metadata.Directory;
019import com.drew.metadata.Metadata;
020import com.drew.metadata.MetadataException;
021import com.drew.metadata.exif.ExifIFD0Directory;
022import com.drew.metadata.exif.GpsDirectory;
023
024/**
025 * Stores info about each image
026 */
027public final class ImageEntry implements Comparable<ImageEntry>, Cloneable {
028    private File file;
029    private Integer exifOrientation;
030    private LatLon exifCoor;
031    private Double exifImgDir;
032    private Date exifTime;
033    /**
034     * Flag isNewGpsData indicates that the GPS data of the image is new or has changed.
035     * GPS data includes the position, speed, elevation, time (e.g. as extracted from the GPS track).
036     * The flag can used to decide for which image file the EXIF GPS data is (re-)written.
037     */
038    private boolean isNewGpsData;
039    /** Temporary source of GPS time if not correlated with GPX track. */
040    private Date exifGpsTime;
041    private Image thumbnail;
042
043    /**
044     * The following values are computed from the correlation with the gpx track
045     * or extracted from the image EXIF data.
046     */
047    private CachedLatLon pos;
048    /** Speed in kilometer per hour */
049    private Double speed;
050    /** Elevation (altitude) in meters */
051    private Double elevation;
052    /** The time after correlation with a gpx track */
053    private Date gpsTime;
054
055    /**
056     * When the correlation dialog is open, we like to show the image position
057     * for the current time offset on the map in real time.
058     * On the other hand, when the user aborts this operation, the old values
059     * should be restored. We have a temporary copy, that overrides
060     * the normal values if it is not null. (This may be not the most elegant
061     * solution for this, but it works.)
062     */
063    ImageEntry tmp;
064
065    /**
066     * Constructs a new {@code ImageEntry}.
067     */
068    public ImageEntry() {}
069
070    /**
071     * Constructs a new {@code ImageEntry}.
072     * @param file Path to image file on disk
073     */
074    public ImageEntry(File file) {
075        setFile(file);
076    }
077
078    /**
079     * Returns the position value. The position value from the temporary copy
080     * is returned if that copy exists.
081     * @return the position value
082     */
083    public CachedLatLon getPos() {
084        if (tmp != null)
085            return tmp.pos;
086        return pos;
087    }
088
089    /**
090     * Returns the speed value. The speed value from the temporary copy is
091     * returned if that copy exists.
092     * @return the speed value
093     */
094    public Double getSpeed() {
095        if (tmp != null)
096            return tmp.speed;
097        return speed;
098    }
099
100    /**
101     * Returns the elevation value. The elevation value from the temporary
102     * copy is returned if that copy exists.
103     * @return the elevation value
104     */
105    public Double getElevation() {
106        if (tmp != null)
107            return tmp.elevation;
108        return elevation;
109    }
110
111    /**
112     * Returns the GPS time value. The GPS time value from the temporary copy
113     * is returned if that copy exists.
114     * @return the GPS time value
115     */
116    public Date getGpsTime() {
117        if (tmp != null)
118            return getDefensiveDate(tmp.gpsTime);
119        return getDefensiveDate(gpsTime);
120    }
121
122    /**
123     * Convenient way to determine if this entry has a GPS time, without the cost of building a defensive copy.
124     * @return {@code true} if this entry has a GPS time
125     * @since 6450
126     */
127    public boolean hasGpsTime() {
128        return (tmp != null && tmp.gpsTime != null) || gpsTime != null;
129    }
130
131    /**
132     * Returns associated file.
133     * @return associated file
134     */
135    public File getFile() {
136        return file;
137    }
138
139    /**
140     * Returns EXIF orientation
141     * @return EXIF orientation
142     */
143    public Integer getExifOrientation() {
144        return exifOrientation;
145    }
146
147    /**
148     * Returns EXIF time
149     * @return EXIF time
150     */
151    public Date getExifTime() {
152        return getDefensiveDate(exifTime);
153    }
154
155    /**
156     * Convenient way to determine if this entry has a EXIF time, without the cost of building a defensive copy.
157     * @return {@code true} if this entry has a EXIF time
158     * @since 6450
159     */
160    public boolean hasExifTime() {
161        return exifTime != null;
162    }
163
164    /**
165     * Returns the EXIF GPS time.
166     * @return the EXIF GPS time
167     * @since 6392
168     */
169    public Date getExifGpsTime() {
170        return getDefensiveDate(exifGpsTime);
171    }
172
173    /**
174     * Convenient way to determine if this entry has a EXIF GPS time, without the cost of building a defensive copy.
175     * @return {@code true} if this entry has a EXIF GPS time
176     * @since 6450
177     */
178    public boolean hasExifGpsTime() {
179        return exifGpsTime != null;
180    }
181
182    private static Date getDefensiveDate(Date date) {
183        if (date == null)
184            return null;
185        return new Date(date.getTime());
186    }
187
188    public LatLon getExifCoor() {
189        return exifCoor;
190    }
191
192    public Double getExifImgDir() {
193        if (tmp != null)
194            return tmp.exifImgDir;
195        return exifImgDir;
196    }
197
198    /**
199     * Determines whether a thumbnail is set
200     * @return {@code true} if a thumbnail is set
201     */
202    public boolean hasThumbnail() {
203        return thumbnail != null;
204    }
205
206    /**
207     * Returns the thumbnail.
208     * @return the thumbnail
209     */
210    public Image getThumbnail() {
211        return thumbnail;
212    }
213
214    /**
215     * Sets the thumbnail.
216     * @param thumbnail thumbnail
217     */
218    public void setThumbnail(Image thumbnail) {
219        this.thumbnail = thumbnail;
220    }
221
222    /**
223     * Loads the thumbnail if it was not loaded yet.
224     * @see ThumbsLoader
225     */
226    public void loadThumbnail() {
227        if (thumbnail == null) {
228            new ThumbsLoader(Collections.singleton(this)).run();
229        }
230    }
231
232    /**
233     * Sets the position.
234     * @param pos cached position
235     */
236    public void setPos(CachedLatLon pos) {
237        this.pos = pos;
238    }
239
240    /**
241     * Sets the position.
242     * @param pos position (will be cached)
243     */
244    public void setPos(LatLon pos) {
245        setPos(pos != null ? new CachedLatLon(pos) : null);
246    }
247
248    /**
249     * Sets the speed.
250     * @param speed speed
251     */
252    public void setSpeed(Double speed) {
253        this.speed = speed;
254    }
255
256    /**
257     * Sets the elevation.
258     * @param elevation elevation
259     */
260    public void setElevation(Double elevation) {
261        this.elevation = elevation;
262    }
263
264    /**
265     * Sets associated file.
266     * @param file associated file
267     */
268    public void setFile(File file) {
269        this.file = file;
270    }
271
272    /**
273     * Sets EXIF orientation.
274     * @param exifOrientation EXIF orientation
275     */
276    public void setExifOrientation(Integer exifOrientation) {
277        this.exifOrientation = exifOrientation;
278    }
279
280    /**
281     * Sets EXIF time.
282     * @param exifTime EXIF time
283     */
284    public void setExifTime(Date exifTime) {
285        this.exifTime = getDefensiveDate(exifTime);
286    }
287
288    /**
289     * Sets the EXIF GPS time.
290     * @param exifGpsTime the EXIF GPS time
291     * @since 6392
292     */
293    public void setExifGpsTime(Date exifGpsTime) {
294        this.exifGpsTime = getDefensiveDate(exifGpsTime);
295    }
296
297    public void setGpsTime(Date gpsTime) {
298        this.gpsTime = getDefensiveDate(gpsTime);
299    }
300
301    public void setExifCoor(LatLon exifCoor) {
302        this.exifCoor = exifCoor;
303    }
304
305    public void setExifImgDir(Double exifDir) {
306        this.exifImgDir = exifDir;
307    }
308
309    @Override
310    public ImageEntry clone() {
311        try {
312            return (ImageEntry) super.clone();
313        } catch (CloneNotSupportedException e) {
314            throw new IllegalStateException(e);
315        }
316    }
317
318    @Override
319    public int compareTo(ImageEntry image) {
320        if (exifTime != null && image.exifTime != null)
321            return exifTime.compareTo(image.exifTime);
322        else if (exifTime == null && image.exifTime == null)
323            return 0;
324        else if (exifTime == null)
325            return -1;
326        else
327            return 1;
328    }
329
330    /**
331     * Make a fresh copy and save it in the temporary variable. Use
332     * {@link #applyTmp()} or {@link #discardTmp()} if the temporary variable
333     * is not needed anymore.
334     */
335    public void createTmp() {
336        tmp = clone();
337        tmp.tmp = null;
338    }
339
340    /**
341     * Get temporary variable that is used for real time parameter
342     * adjustments. The temporary variable is created if it does not exist
343     * yet. Use {@link #applyTmp()} or {@link #discardTmp()} if the temporary
344     * variable is not needed anymore.
345     * @return temporary variable
346     */
347    public ImageEntry getTmp() {
348        if (tmp == null) {
349            createTmp();
350        }
351        return tmp;
352    }
353
354    /**
355     * Copy the values from the temporary variable to the main instance. The
356     * temporary variable is deleted.
357     * @see #discardTmp()
358     */
359    public void applyTmp() {
360        if (tmp != null) {
361            pos = tmp.pos;
362            speed = tmp.speed;
363            elevation = tmp.elevation;
364            gpsTime = tmp.gpsTime;
365            exifImgDir = tmp.exifImgDir;
366            tmp = null;
367        }
368    }
369
370    /**
371     * Delete the temporary variable. Temporary modifications are lost.
372     * @see #applyTmp()
373     */
374    public void discardTmp() {
375        tmp = null;
376    }
377
378    /**
379     * If it has been tagged i.e. matched to a gpx track or retrieved lat/lon from exif
380     * @return {@code true} if it has been tagged
381     */
382    public boolean isTagged() {
383        return pos != null;
384    }
385
386    /**
387     * String representation. (only partial info)
388     */
389    @Override
390    public String toString() {
391        return file.getName()+": "+
392        "pos = "+pos+" | "+
393        "exifCoor = "+exifCoor+" | "+
394        (tmp == null ? " tmp==null" :
395            " [tmp] pos = "+tmp.pos);
396    }
397
398    /**
399     * Indicates that the image has new GPS data.
400     * That flag is set by new GPS data providers.  It is used e.g. by the photo_geotagging plugin
401     * to decide for which image file the EXIF GPS data needs to be (re-)written.
402     * @since 6392
403     */
404    public void flagNewGpsData() {
405        isNewGpsData = true;
406   }
407
408    /**
409     * Remove the flag that indicates new GPS data.
410     * The flag is cleared by a new GPS data consumer.
411     */
412    public void unflagNewGpsData() {
413        isNewGpsData = false;
414    }
415
416    /**
417     * Queries whether the GPS data changed.
418     * @return {@code true} if GPS data changed, {@code false} otherwise
419     * @since 6392
420     */
421    public boolean hasNewGpsData() {
422        return isNewGpsData;
423    }
424
425    /**
426     * Extract GPS metadata from image EXIF. Has no effect if the image file is not set
427     *
428     * If successful, fills in the LatLon, speed, elevation, image direction, and other attributes
429     * @since 9270
430     */
431    public void extractExif() {
432
433        Metadata metadata;
434        Directory dirExif;
435        GpsDirectory dirGps;
436
437        if (file == null) {
438            return;
439        }
440
441        // Changed to silently cope with no time info in exif. One case
442        // of person having time that couldn't be parsed, but valid GPS info
443        try {
444            setExifTime(ExifReader.readTime(file));
445        } catch (RuntimeException ex) {
446            Main.warn(ex);
447            setExifTime(null);
448        }
449
450        try {
451            metadata = JpegMetadataReader.readMetadata(file);
452            dirExif = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
453            dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
454        } catch (CompoundException | IOException p) {
455            Main.warn(p);
456            setExifCoor(null);
457            setPos(null);
458            return;
459        }
460
461        try {
462            if (dirExif != null) {
463                int orientation = dirExif.getInt(ExifIFD0Directory.TAG_ORIENTATION);
464                setExifOrientation(orientation);
465            }
466        } catch (MetadataException ex) {
467            Main.debug(ex);
468        }
469
470        if (dirGps == null) {
471            setExifCoor(null);
472            setPos(null);
473            return;
474        }
475
476        try {
477            double speed = dirGps.getDouble(GpsDirectory.TAG_SPEED);
478            String speedRef = dirGps.getString(GpsDirectory.TAG_SPEED_REF);
479            if ("M".equalsIgnoreCase(speedRef)) {
480                // miles per hour
481                speed *= SystemOfMeasurement.IMPERIAL.bValue / 1000;
482            } else if ("N".equalsIgnoreCase(speedRef)) {
483                // knots == nautical miles per hour
484                speed *= SystemOfMeasurement.NAUTICAL_MILE.bValue / 1000;
485            }
486            // default is K (km/h)
487            setSpeed(speed);
488        } catch (MetadataException ex) {
489            Main.debug(ex);
490        }
491
492        try {
493            double ele = dirGps.getDouble(GpsDirectory.TAG_ALTITUDE);
494            int d = dirGps.getInt(GpsDirectory.TAG_ALTITUDE_REF);
495            if (d == 1) {
496                ele *= -1;
497            }
498            setElevation(ele);
499        } catch (MetadataException ex) {
500            Main.debug(ex);
501        }
502
503        try {
504            LatLon latlon = ExifReader.readLatLon(dirGps);
505            setExifCoor(latlon);
506            setPos(getExifCoor());
507
508        } catch (MetadataException | IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
509            Main.error("Error reading EXIF from file: " + ex);
510            setExifCoor(null);
511            setPos(null);
512        }
513
514        try {
515            Double direction = ExifReader.readDirection(dirGps);
516            if (direction != null) {
517                setExifImgDir(direction);
518            }
519        } catch (IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
520            Main.debug(ex);
521        }
522
523        final Date gpsDate = dirGps.getGpsDate();
524        if (gpsDate != null) {
525            setExifGpsTime(gpsDate);
526        }
527    }
528}