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