// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.layer.geoimage; import java.awt.Image; import java.io.File; import java.io.IOException; import java.util.Collections; import java.util.Date; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.coor.CachedLatLon; import org.openstreetmap.josm.data.coor.LatLon; import org.openstreetmap.josm.tools.ExifReader; import org.openstreetmap.josm.tools.JosmRuntimeException; import com.drew.imaging.jpeg.JpegMetadataReader; import com.drew.lang.CompoundException; import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import com.drew.metadata.MetadataException; import com.drew.metadata.exif.ExifIFD0Directory; import com.drew.metadata.exif.GpsDirectory; /** * Stores info about each image */ public final class ImageEntry implements Comparable<ImageEntry>, Cloneable { private File file; private Integer exifOrientation; private LatLon exifCoor; private Double exifImgDir; private Date exifTime; /** * Flag isNewGpsData indicates that the GPS data of the image is new or has changed. * GPS data includes the position, speed, elevation, time (e.g. as extracted from the GPS track). * The flag can used to decide for which image file the EXIF GPS data is (re-)written. */ private boolean isNewGpsData; /** Temporary source of GPS time if not correlated with GPX track. */ private Date exifGpsTime; private Image thumbnail; /** * The following values are computed from the correlation with the gpx track * or extracted from the image EXIF data. */ private CachedLatLon pos; /** Speed in kilometer per hour */ private Double speed; /** Elevation (altitude) in meters */ private Double elevation; /** The time after correlation with a gpx track */ private Date gpsTime; /** * When the correlation dialog is open, we like to show the image position * for the current time offset on the map in real time. * On the other hand, when the user aborts this operation, the old values * should be restored. We have a temporary copy, that overrides * the normal values if it is not null. (This may be not the most elegant * solution for this, but it works.) */ ImageEntry tmp; /** * Constructs a new {@code ImageEntry}. */ public ImageEntry() {} /** * Constructs a new {@code ImageEntry}. * @param file Path to image file on disk */ public ImageEntry(File file) { setFile(file); } /** * Returns the position value. The position value from the temporary copy * is returned if that copy exists. * @return the position value */ public CachedLatLon getPos() { if (tmp != null) return tmp.pos; return pos; } /** * Returns the speed value. The speed value from the temporary copy is * returned if that copy exists. * @return the speed value */ public Double getSpeed() { if (tmp != null) return tmp.speed; return speed; } /** * Returns the elevation value. The elevation value from the temporary * copy is returned if that copy exists. * @return the elevation value */ public Double getElevation() { if (tmp != null) return tmp.elevation; return elevation; } /** * Returns the GPS time value. The GPS time value from the temporary copy * is returned if that copy exists. * @return the GPS time value */ public Date getGpsTime() { if (tmp != null) return getDefensiveDate(tmp.gpsTime); return getDefensiveDate(gpsTime); } /** * Convenient way to determine if this entry has a GPS time, without the cost of building a defensive copy. * @return {@code true} if this entry has a GPS time * @since 6450 */ public boolean hasGpsTime() { return (tmp != null && tmp.gpsTime != null) || gpsTime != null; } /** * Returns associated file. * @return associated file */ public File getFile() { return file; } /** * Returns EXIF orientation * @return EXIF orientation */ public Integer getExifOrientation() { return exifOrientation; } /** * Returns EXIF time * @return EXIF time */ public Date getExifTime() { return getDefensiveDate(exifTime); } /** * Convenient way to determine if this entry has a EXIF time, without the cost of building a defensive copy. * @return {@code true} if this entry has a EXIF time * @since 6450 */ public boolean hasExifTime() { return exifTime != null; } /** * Returns the EXIF GPS time. * @return the EXIF GPS time * @since 6392 */ public Date getExifGpsTime() { return getDefensiveDate(exifGpsTime); } /** * Convenient way to determine if this entry has a EXIF GPS time, without the cost of building a defensive copy. * @return {@code true} if this entry has a EXIF GPS time * @since 6450 */ public boolean hasExifGpsTime() { return exifGpsTime != null; } private static Date getDefensiveDate(Date date) { if (date == null) return null; return new Date(date.getTime()); } public LatLon getExifCoor() { return exifCoor; } public Double getExifImgDir() { if (tmp != null) return tmp.exifImgDir; return exifImgDir; } /** * Determines whether a thumbnail is set * @return {@code true} if a thumbnail is set */ public boolean hasThumbnail() { return thumbnail != null; } /** * Returns the thumbnail. * @return the thumbnail */ public Image getThumbnail() { return thumbnail; } /** * Sets the thumbnail. * @param thumbnail thumbnail */ public void setThumbnail(Image thumbnail) { this.thumbnail = thumbnail; } /** * Loads the thumbnail if it was not loaded yet. * @see ThumbsLoader */ public void loadThumbnail() { if (thumbnail == null) { new ThumbsLoader(Collections.singleton(this)).run(); } } /** * Sets the position. * @param pos cached position */ public void setPos(CachedLatLon pos) { this.pos = pos; } /** * Sets the position. * @param pos position (will be cached) */ public void setPos(LatLon pos) { setPos(pos != null ? new CachedLatLon(pos) : null); } /** * Sets the speed. * @param speed speed */ public void setSpeed(Double speed) { this.speed = speed; } /** * Sets the elevation. * @param elevation elevation */ public void setElevation(Double elevation) { this.elevation = elevation; } /** * Sets associated file. * @param file associated file */ public void setFile(File file) { this.file = file; } /** * Sets EXIF orientation. * @param exifOrientation EXIF orientation */ public void setExifOrientation(Integer exifOrientation) { this.exifOrientation = exifOrientation; } /** * Sets EXIF time. * @param exifTime EXIF time */ public void setExifTime(Date exifTime) { this.exifTime = getDefensiveDate(exifTime); } /** * Sets the EXIF GPS time. * @param exifGpsTime the EXIF GPS time * @since 6392 */ public void setExifGpsTime(Date exifGpsTime) { this.exifGpsTime = getDefensiveDate(exifGpsTime); } public void setGpsTime(Date gpsTime) { this.gpsTime = getDefensiveDate(gpsTime); } public void setExifCoor(LatLon exifCoor) { this.exifCoor = exifCoor; } public void setExifImgDir(Double exifDir) { this.exifImgDir = exifDir; } @Override public ImageEntry clone() { try { return (ImageEntry) super.clone(); } catch (CloneNotSupportedException e) { throw new IllegalStateException(e); } } @Override public int compareTo(ImageEntry image) { if (exifTime != null && image.exifTime != null) return exifTime.compareTo(image.exifTime); else if (exifTime == null && image.exifTime == null) return 0; else if (exifTime == null) return -1; else return 1; } /** * Make a fresh copy and save it in the temporary variable. Use * {@link #applyTmp()} or {@link #discardTmp()} if the temporary variable * is not needed anymore. */ public void createTmp() { tmp = clone(); tmp.tmp = null; } /** * Get temporary variable that is used for real time parameter * adjustments. The temporary variable is created if it does not exist * yet. Use {@link #applyTmp()} or {@link #discardTmp()} if the temporary * variable is not needed anymore. * @return temporary variable */ public ImageEntry getTmp() { if (tmp == null) { createTmp(); } return tmp; } /** * Copy the values from the temporary variable to the main instance. The * temporary variable is deleted. * @see #discardTmp() */ public void applyTmp() { if (tmp != null) { pos = tmp.pos; speed = tmp.speed; elevation = tmp.elevation; gpsTime = tmp.gpsTime; exifImgDir = tmp.exifImgDir; tmp = null; } } /** * Delete the temporary variable. Temporary modifications are lost. * @see #applyTmp() */ public void discardTmp() { tmp = null; } /** * If it has been tagged i.e. matched to a gpx track or retrieved lat/lon from exif * @return {@code true} if it has been tagged */ public boolean isTagged() { return pos != null; } /** * String representation. (only partial info) */ @Override public String toString() { return file.getName()+": "+ "pos = "+pos+" | "+ "exifCoor = "+exifCoor+" | "+ (tmp == null ? " tmp==null" : " [tmp] pos = "+tmp.pos); } /** * Indicates that the image has new GPS data. * That flag is set by new GPS data providers. It is used e.g. by the photo_geotagging plugin * to decide for which image file the EXIF GPS data needs to be (re-)written. * @since 6392 */ public void flagNewGpsData() { isNewGpsData = true; } /** * Remove the flag that indicates new GPS data. * The flag is cleared by a new GPS data consumer. */ public void unflagNewGpsData() { isNewGpsData = false; } /** * Queries whether the GPS data changed. * @return {@code true} if GPS data changed, {@code false} otherwise * @since 6392 */ public boolean hasNewGpsData() { return isNewGpsData; } /** * Extract GPS metadata from image EXIF. Has no effect if the image file is not set * * If successful, fills in the LatLon, speed, elevation, image direction, and other attributes * @since 9270 */ public void extractExif() { Metadata metadata; if (file == null) { return; } try { metadata = JpegMetadataReader.readMetadata(file); } catch (CompoundException | IOException ex) { Main.error(ex); setExifTime(null); setExifCoor(null); setPos(null); return; } // Changed to silently cope with no time info in exif. One case // of person having time that couldn't be parsed, but valid GPS info try { setExifTime(ExifReader.readTime(metadata)); } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException ex) { Main.warn(ex); setExifTime(null); } final Directory dirExif = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class); try { if (dirExif != null) { int orientation = dirExif.getInt(ExifIFD0Directory.TAG_ORIENTATION); setExifOrientation(orientation); } } catch (MetadataException ex) { Main.debug(ex); } if (dirGps == null) { setExifCoor(null); setPos(null); return; } final Double speed = ExifReader.readSpeed(dirGps); if (speed != null) { setSpeed(speed); } final Double ele = ExifReader.readElevation(dirGps); if (ele != null) { setElevation(ele); } try { final LatLon latlon = ExifReader.readLatLon(dirGps); setExifCoor(latlon); setPos(getExifCoor()); } catch (MetadataException | IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271) Main.error("Error reading EXIF from file: " + ex); setExifCoor(null); setPos(null); } try { final Double direction = ExifReader.readDirection(dirGps); if (direction != null) { setExifImgDir(direction); } } catch (IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271) Main.debug(ex); } final Date gpsDate = dirGps.getGpsDate(); if (gpsDate != null) { setExifGpsTime(gpsDate); } } }