source: josm/trunk/src/org/openstreetmap/josm/data/gpx/GpxImageEntry.java@ 18120

Last change on this file since 18120 was 18120, checked in by Don-vip, 4 years ago

see #21131 - use always exifTime over exifGpsTime in waypoint conversion, more precise as it usually includes milliseconds

  • Property svn:eol-style set to native
File size: 23.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.gpx;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.File;
7import java.io.IOException;
8import java.time.Instant;
9import java.util.Date;
10import java.util.List;
11import java.util.Locale;
12import java.util.Objects;
13import java.util.function.Consumer;
14
15import org.openstreetmap.josm.data.IQuadBucketType;
16import org.openstreetmap.josm.data.coor.CachedLatLon;
17import org.openstreetmap.josm.data.coor.LatLon;
18import org.openstreetmap.josm.data.osm.BBox;
19import org.openstreetmap.josm.tools.ExifReader;
20import org.openstreetmap.josm.tools.JosmRuntimeException;
21import org.openstreetmap.josm.tools.Logging;
22
23import com.drew.imaging.jpeg.JpegMetadataReader;
24import com.drew.imaging.jpeg.JpegProcessingException;
25import com.drew.imaging.png.PngMetadataReader;
26import com.drew.imaging.png.PngProcessingException;
27import com.drew.imaging.tiff.TiffMetadataReader;
28import com.drew.imaging.tiff.TiffProcessingException;
29import com.drew.metadata.Directory;
30import com.drew.metadata.Metadata;
31import com.drew.metadata.MetadataException;
32import com.drew.metadata.exif.ExifIFD0Directory;
33import com.drew.metadata.exif.GpsDirectory;
34import com.drew.metadata.iptc.IptcDirectory;
35import com.drew.metadata.jpeg.JpegDirectory;
36
37/**
38 * Stores info about each image
39 * @since 14205 (extracted from gui.layer.geoimage.ImageEntry)
40 */
41public class GpxImageEntry implements Comparable<GpxImageEntry>, IQuadBucketType {
42 private File file;
43 private Integer exifOrientation;
44 private LatLon exifCoor;
45 private Double exifImgDir;
46 private Instant exifTime;
47 /**
48 * Flag isNewGpsData indicates that the GPS data of the image is new or has changed.
49 * GPS data includes the position, speed, elevation, time (e.g. as extracted from the GPS track).
50 * The flag can used to decide for which image file the EXIF GPS data is (re-)written.
51 */
52 private boolean isNewGpsData;
53 /** Temporary source of GPS time if not correlated with GPX track. */
54 private Instant exifGpsTime;
55
56 private String iptcCaption;
57 private String iptcHeadline;
58 private List<String> iptcKeywords;
59 private String iptcObjectName;
60
61 /**
62 * The following values are computed from the correlation with the gpx track
63 * or extracted from the image EXIF data.
64 */
65 private CachedLatLon pos;
66 /** Speed in kilometer per hour */
67 private Double speed;
68 /** Elevation (altitude) in meters */
69 private Double elevation;
70 /** The time after correlation with a gpx track */
71 private Instant gpsTime;
72
73 private int width;
74 private int height;
75
76 /**
77 * When the correlation dialog is open, we like to show the image position
78 * for the current time offset on the map in real time.
79 * On the other hand, when the user aborts this operation, the old values
80 * should be restored. We have a temporary copy, that overrides
81 * the normal values if it is not null. (This may be not the most elegant
82 * solution for this, but it works.)
83 */
84 private GpxImageEntry tmp;
85
86 /**
87 * Constructs a new {@code GpxImageEntry}.
88 */
89 public GpxImageEntry() {}
90
91 /**
92 * Constructs a new {@code GpxImageEntry} from an existing instance.
93 * @param other existing instance
94 * @since 14624
95 */
96 public GpxImageEntry(GpxImageEntry other) {
97 file = other.file;
98 exifOrientation = other.exifOrientation;
99 exifCoor = other.exifCoor;
100 exifImgDir = other.exifImgDir;
101 exifTime = other.exifTime;
102 isNewGpsData = other.isNewGpsData;
103 exifGpsTime = other.exifGpsTime;
104 pos = other.pos;
105 speed = other.speed;
106 elevation = other.elevation;
107 gpsTime = other.gpsTime;
108 width = other.width;
109 height = other.height;
110 tmp = other.tmp;
111 }
112
113 /**
114 * Constructs a new {@code GpxImageEntry}.
115 * @param file Path to image file on disk
116 */
117 public GpxImageEntry(File file) {
118 setFile(file);
119 }
120
121 /**
122 * Returns width of the image this GpxImageEntry represents.
123 * @return width of the image this GpxImageEntry represents
124 * @since 13220
125 */
126 public int getWidth() {
127 return width;
128 }
129
130 /**
131 * Returns height of the image this GpxImageEntry represents.
132 * @return height of the image this GpxImageEntry represents
133 * @since 13220
134 */
135 public int getHeight() {
136 return height;
137 }
138
139 /**
140 * Returns the position value. The position value from the temporary copy
141 * is returned if that copy exists.
142 * @return the position value
143 */
144 public CachedLatLon getPos() {
145 if (tmp != null)
146 return tmp.pos;
147 return pos;
148 }
149
150 /**
151 * Returns the speed value. The speed value from the temporary copy is
152 * returned if that copy exists.
153 * @return the speed value
154 */
155 public Double getSpeed() {
156 if (tmp != null)
157 return tmp.speed;
158 return speed;
159 }
160
161 /**
162 * Returns the elevation value. The elevation value from the temporary
163 * copy is returned if that copy exists.
164 * @return the elevation value
165 */
166 public Double getElevation() {
167 if (tmp != null)
168 return tmp.elevation;
169 return elevation;
170 }
171
172 /**
173 * Returns the GPS time value. The GPS time value from the temporary copy
174 * is returned if that copy exists.
175 * @return the GPS time value
176 * @deprecated Use {@link #getGpsInstant}
177 */
178 @Deprecated
179 public Date getGpsTime() {
180 if (tmp != null)
181 return getDefensiveDate(tmp.gpsTime);
182 return getDefensiveDate(gpsTime);
183 }
184
185 /**
186 * Returns the GPS time value. The GPS time value from the temporary copy
187 * is returned if that copy exists.
188 * @return the GPS time value
189 */
190 public Instant getGpsInstant() {
191 return tmp != null ? tmp.gpsTime : gpsTime;
192 }
193
194 /**
195 * Convenient way to determine if this entry has a GPS time, without the cost of building a defensive copy.
196 * @return {@code true} if this entry has a GPS time
197 * @since 6450
198 */
199 public boolean hasGpsTime() {
200 return (tmp != null && tmp.gpsTime != null) || gpsTime != null;
201 }
202
203 /**
204 * Returns associated file.
205 * @return associated file
206 */
207 public File getFile() {
208 return file;
209 }
210
211 /**
212 * Returns a display name for this entry
213 * @return a display name for this entry
214 */
215 public String getDisplayName() {
216 return file == null ? "" : file.getName();
217 }
218
219 /**
220 * Returns EXIF orientation
221 * @return EXIF orientation
222 */
223 public Integer getExifOrientation() {
224 return exifOrientation != null ? exifOrientation : 1;
225 }
226
227 /**
228 * Returns EXIF time
229 * @return EXIF time
230 * @since 17715
231 */
232 public Instant getExifInstant() {
233 return exifTime;
234 }
235
236 /**
237 * Convenient way to determine if this entry has a EXIF time, without the cost of building a defensive copy.
238 * @return {@code true} if this entry has a EXIF time
239 * @since 6450
240 */
241 public boolean hasExifTime() {
242 return exifTime != null;
243 }
244
245 /**
246 * Returns the EXIF GPS time.
247 * @return the EXIF GPS time
248 * @since 6392
249 * @deprecated Use {@link #getExifGpsInstant}
250 */
251 @Deprecated
252 public Date getExifGpsTime() {
253 return getDefensiveDate(exifGpsTime);
254 }
255
256 /**
257 * Returns the EXIF GPS time.
258 * @return the EXIF GPS time
259 * @since 17715
260 */
261 public Instant getExifGpsInstant() {
262 return exifGpsTime;
263 }
264
265 /**
266 * Convenient way to determine if this entry has a EXIF GPS time, without the cost of building a defensive copy.
267 * @return {@code true} if this entry has a EXIF GPS time
268 * @since 6450
269 */
270 public boolean hasExifGpsTime() {
271 return exifGpsTime != null;
272 }
273
274 private static Date getDefensiveDate(Instant date) {
275 if (date == null)
276 return null;
277 return Date.from(date);
278 }
279
280 public LatLon getExifCoor() {
281 return exifCoor;
282 }
283
284 public Double getExifImgDir() {
285 if (tmp != null)
286 return tmp.exifImgDir;
287 return exifImgDir;
288 }
289
290 /**
291 * Sets the width of this GpxImageEntry.
292 * @param width set the width of this GpxImageEntry
293 * @since 13220
294 */
295 public void setWidth(int width) {
296 this.width = width;
297 }
298
299 /**
300 * Sets the height of this GpxImageEntry.
301 * @param height set the height of this GpxImageEntry
302 * @since 13220
303 */
304 public void setHeight(int height) {
305 this.height = height;
306 }
307
308 /**
309 * Sets the position.
310 * @param pos cached position
311 */
312 public void setPos(CachedLatLon pos) {
313 this.pos = pos;
314 }
315
316 /**
317 * Sets the position.
318 * @param pos position (will be cached)
319 */
320 public void setPos(LatLon pos) {
321 setPos(pos != null ? new CachedLatLon(pos) : null);
322 }
323
324 /**
325 * Sets the speed.
326 * @param speed speed
327 */
328 public void setSpeed(Double speed) {
329 this.speed = speed;
330 }
331
332 /**
333 * Sets the elevation.
334 * @param elevation elevation
335 */
336 public void setElevation(Double elevation) {
337 this.elevation = elevation;
338 }
339
340 /**
341 * Sets associated file.
342 * @param file associated file
343 */
344 public void setFile(File file) {
345 this.file = file;
346 }
347
348 /**
349 * Sets EXIF orientation.
350 * @param exifOrientation EXIF orientation
351 */
352 public void setExifOrientation(Integer exifOrientation) {
353 this.exifOrientation = exifOrientation;
354 }
355
356 /**
357 * Sets EXIF time.
358 * @param exifTime EXIF time
359 * @since 17715
360 */
361 public void setExifTime(Instant exifTime) {
362 this.exifTime = exifTime;
363 }
364
365 /**
366 * Sets the EXIF GPS time.
367 * @param exifGpsTime the EXIF GPS time
368 * @since 17715
369 */
370 public void setExifGpsTime(Instant exifGpsTime) {
371 this.exifGpsTime = exifGpsTime;
372 }
373
374 /**
375 * Sets the GPS time.
376 * @param gpsTime the GPS time
377 * @since 17715
378 */
379 public void setGpsTime(Instant gpsTime) {
380 this.gpsTime = gpsTime;
381 }
382
383 public void setExifCoor(LatLon exifCoor) {
384 this.exifCoor = exifCoor;
385 }
386
387 public void setExifImgDir(Double exifDir) {
388 this.exifImgDir = exifDir;
389 }
390
391 /**
392 * Sets the IPTC caption.
393 * @param iptcCaption the IPTC caption
394 * @since 15219
395 */
396 public void setIptcCaption(String iptcCaption) {
397 this.iptcCaption = iptcCaption;
398 }
399
400 /**
401 * Sets the IPTC headline.
402 * @param iptcHeadline the IPTC headline
403 * @since 15219
404 */
405 public void setIptcHeadline(String iptcHeadline) {
406 this.iptcHeadline = iptcHeadline;
407 }
408
409 /**
410 * Sets the IPTC keywords.
411 * @param iptcKeywords the IPTC keywords
412 * @since 15219
413 */
414 public void setIptcKeywords(List<String> iptcKeywords) {
415 this.iptcKeywords = iptcKeywords;
416 }
417
418 /**
419 * Sets the IPTC object name.
420 * @param iptcObjectName the IPTC object name
421 * @since 15219
422 */
423 public void setIptcObjectName(String iptcObjectName) {
424 this.iptcObjectName = iptcObjectName;
425 }
426
427 /**
428 * Returns the IPTC caption.
429 * @return the IPTC caption
430 * @since 15219
431 */
432 public String getIptcCaption() {
433 return iptcCaption;
434 }
435
436 /**
437 * Returns the IPTC headline.
438 * @return the IPTC headline
439 * @since 15219
440 */
441 public String getIptcHeadline() {
442 return iptcHeadline;
443 }
444
445 /**
446 * Returns the IPTC keywords.
447 * @return the IPTC keywords
448 * @since 15219
449 */
450 public List<String> getIptcKeywords() {
451 return iptcKeywords;
452 }
453
454 /**
455 * Returns the IPTC object name.
456 * @return the IPTC object name
457 * @since 15219
458 */
459 public String getIptcObjectName() {
460 return iptcObjectName;
461 }
462
463 @Override
464 public int compareTo(GpxImageEntry image) {
465 if (exifTime != null && image.exifTime != null)
466 return exifTime.compareTo(image.exifTime);
467 else if (exifTime == null && image.exifTime == null)
468 return 0;
469 else if (exifTime == null)
470 return -1;
471 else
472 return 1;
473 }
474
475 @Override
476 public int hashCode() {
477 return Objects.hash(height, width, isNewGpsData,
478 elevation, exifCoor, exifGpsTime, exifImgDir, exifOrientation, exifTime,
479 iptcCaption, iptcHeadline, iptcKeywords, iptcObjectName,
480 file, gpsTime, pos, speed, tmp);
481 }
482
483 @Override
484 public boolean equals(Object obj) {
485 if (this == obj)
486 return true;
487 if (obj == null || getClass() != obj.getClass())
488 return false;
489 GpxImageEntry other = (GpxImageEntry) obj;
490 return height == other.height
491 && width == other.width
492 && isNewGpsData == other.isNewGpsData
493 && Objects.equals(elevation, other.elevation)
494 && Objects.equals(exifCoor, other.exifCoor)
495 && Objects.equals(exifGpsTime, other.exifGpsTime)
496 && Objects.equals(exifImgDir, other.exifImgDir)
497 && Objects.equals(exifOrientation, other.exifOrientation)
498 && Objects.equals(exifTime, other.exifTime)
499 && Objects.equals(iptcCaption, other.iptcCaption)
500 && Objects.equals(iptcHeadline, other.iptcHeadline)
501 && Objects.equals(iptcKeywords, other.iptcKeywords)
502 && Objects.equals(iptcObjectName, other.iptcObjectName)
503 && Objects.equals(file, other.file)
504 && Objects.equals(gpsTime, other.gpsTime)
505 && Objects.equals(pos, other.pos)
506 && Objects.equals(speed, other.speed)
507 && Objects.equals(tmp, other.tmp);
508 }
509
510 /**
511 * Make a fresh copy and save it in the temporary variable. Use
512 * {@link #applyTmp()} or {@link #discardTmp()} if the temporary variable
513 * is not needed anymore.
514 * @return the fresh copy.
515 */
516 public GpxImageEntry createTmp() {
517 tmp = new GpxImageEntry(this);
518 tmp.tmp = null;
519 return tmp;
520 }
521
522 /**
523 * Get temporary variable that is used for real time parameter
524 * adjustments. The temporary variable is created if it does not exist
525 * yet. Use {@link #applyTmp()} or {@link #discardTmp()} if the temporary
526 * variable is not needed anymore.
527 * @return temporary variable
528 */
529 public GpxImageEntry getTmp() {
530 if (tmp == null) {
531 createTmp();
532 }
533 return tmp;
534 }
535
536 /**
537 * Copy the values from the temporary variable to the main instance. The
538 * temporary variable is deleted.
539 * @see #discardTmp()
540 */
541 public void applyTmp() {
542 if (tmp != null) {
543 pos = tmp.pos;
544 speed = tmp.speed;
545 elevation = tmp.elevation;
546 gpsTime = tmp.gpsTime;
547 exifImgDir = tmp.exifImgDir;
548 isNewGpsData = isNewGpsData || tmp.isNewGpsData;
549 tmp = null;
550 }
551 tmpUpdated();
552 }
553
554 /**
555 * Delete the temporary variable. Temporary modifications are lost.
556 * @see #applyTmp()
557 */
558 public void discardTmp() {
559 tmp = null;
560 tmpUpdated();
561 }
562
563 /**
564 * If it has been tagged i.e. matched to a gpx track or retrieved lat/lon from exif
565 * @return {@code true} if it has been tagged
566 */
567 public boolean isTagged() {
568 return pos != null;
569 }
570
571 /**
572 * String representation. (only partial info)
573 */
574 @Override
575 public String toString() {
576 return file.getName()+": "+
577 "pos = "+pos+" | "+
578 "exifCoor = "+exifCoor+" | "+
579 (tmp == null ? " tmp==null" :
580 " [tmp] pos = "+tmp.pos);
581 }
582
583 /**
584 * Indicates that the image has new GPS data.
585 * That flag is set by new GPS data providers. It is used e.g. by the photo_geotagging plugin
586 * to decide for which image file the EXIF GPS data needs to be (re-)written.
587 * @since 6392
588 */
589 public void flagNewGpsData() {
590 isNewGpsData = true;
591 }
592
593 /**
594 * Indicate that the temporary copy has been updated. Mostly used to prevent UI issues.
595 * By default, this is a no-op. Override when needed in subclasses.
596 * @since 17579
597 */
598 protected void tmpUpdated() {
599 // No-op by default
600 }
601
602 @Override
603 public BBox getBBox() {
604 // new BBox(LatLon) is null safe.
605 // Use `getPos` instead of `getExifCoor` since the image may be correlated against a GPX track
606 return new BBox(this.getPos());
607 }
608
609 /**
610 * Remove the flag that indicates new GPS data.
611 * The flag is cleared by a new GPS data consumer.
612 */
613 public void unflagNewGpsData() {
614 isNewGpsData = false;
615 }
616
617 /**
618 * Queries whether the GPS data changed. The flag value from the temporary
619 * copy is returned if that copy exists.
620 * @return {@code true} if GPS data changed, {@code false} otherwise
621 * @since 6392
622 */
623 public boolean hasNewGpsData() {
624 if (tmp != null)
625 return tmp.isNewGpsData;
626 return isNewGpsData;
627 }
628
629 /**
630 * Extract GPS metadata from image EXIF. Has no effect if the image file is not set
631 *
632 * If successful, fills in the LatLon, speed, elevation, image direction, and other attributes
633 * @since 9270
634 */
635 public void extractExif() {
636
637 Metadata metadata;
638
639 if (file == null) {
640 return;
641 }
642
643 String fn = file.getName();
644
645 try {
646 // try to parse metadata according to extension
647 String ext = fn.substring(fn.lastIndexOf('.') + 1).toLowerCase(Locale.US);
648 switch (ext) {
649 case "jpg":
650 case "jpeg":
651 metadata = JpegMetadataReader.readMetadata(file);
652 break;
653 case "tif":
654 case "tiff":
655 metadata = TiffMetadataReader.readMetadata(file);
656 break;
657 case "png":
658 metadata = PngMetadataReader.readMetadata(file);
659 break;
660 default:
661 throw new NoMetadataReaderWarning(ext);
662 }
663 } catch (JpegProcessingException | TiffProcessingException | PngProcessingException | IOException
664 | NoMetadataReaderWarning topException) {
665 //try other formats (e.g. JPEG file with .png extension)
666 try {
667 metadata = JpegMetadataReader.readMetadata(file);
668 } catch (JpegProcessingException | IOException ex1) {
669 try {
670 metadata = TiffMetadataReader.readMetadata(file);
671 } catch (TiffProcessingException | IOException ex2) {
672 try {
673 metadata = PngMetadataReader.readMetadata(file);
674 } catch (PngProcessingException | IOException ex3) {
675 Logging.warn(topException);
676 Logging.info(tr("Can''t parse metadata for file \"{0}\". Using last modified date as timestamp.", fn));
677 setExifTime(Instant.ofEpochMilli(file.lastModified()));
678 setExifCoor(null);
679 setPos(null);
680 return;
681 }
682 }
683 }
684 }
685
686 // Changed to silently cope with no time info in exif. One case
687 // of person having time that couldn't be parsed, but valid GPS info
688 Instant time = null;
689 try {
690 time = ExifReader.readInstant(metadata);
691 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException ex) {
692 Logging.warn(ex);
693 }
694
695 if (time == null) {
696 Logging.info(tr("No EXIF time in file \"{0}\". Using last modified date as timestamp.", fn));
697 time = Instant.ofEpochMilli(file.lastModified()); //use lastModified time if no EXIF time present
698 }
699 setExifTime(time);
700
701 final Directory dir = metadata.getFirstDirectoryOfType(JpegDirectory.class);
702 final Directory dirExif = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
703 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
704
705 try {
706 if (dirExif != null) {
707 setExifOrientation(dirExif.getInt(ExifIFD0Directory.TAG_ORIENTATION));
708 }
709 } catch (MetadataException ex) {
710 Logging.debug(ex);
711 }
712
713 try {
714 if (dir != null) {
715 // there are cases where these do not match width and height stored in dirExif
716 setWidth(dir.getInt(JpegDirectory.TAG_IMAGE_WIDTH));
717 setHeight(dir.getInt(JpegDirectory.TAG_IMAGE_HEIGHT));
718 }
719 } catch (MetadataException ex) {
720 Logging.debug(ex);
721 }
722
723 if (dirGps == null || dirGps.getTagCount() <= 1) {
724 setExifCoor(null);
725 setPos(null);
726 return;
727 }
728
729 ifNotNull(ExifReader.readSpeed(dirGps), this::setSpeed);
730 ifNotNull(ExifReader.readElevation(dirGps), this::setElevation);
731
732 try {
733 setExifCoor(ExifReader.readLatLon(dirGps));
734 setPos(getExifCoor());
735 } catch (MetadataException | IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
736 Logging.error("Error reading EXIF from file: " + ex);
737 setExifCoor(null);
738 setPos(null);
739 }
740
741 try {
742 ifNotNull(ExifReader.readDirection(dirGps), this::setExifImgDir);
743 } catch (IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
744 Logging.debug(ex);
745 }
746
747 ifNotNull(dirGps.getGpsDate(), d -> setExifGpsTime(d.toInstant()));
748
749 IptcDirectory dirIptc = metadata.getFirstDirectoryOfType(IptcDirectory.class);
750 if (dirIptc != null) {
751 ifNotNull(ExifReader.readCaption(dirIptc), this::setIptcCaption);
752 ifNotNull(ExifReader.readHeadline(dirIptc), this::setIptcHeadline);
753 ifNotNull(ExifReader.readKeywords(dirIptc), this::setIptcKeywords);
754 ifNotNull(ExifReader.readObjectName(dirIptc), this::setIptcObjectName);
755 }
756 }
757
758 private static class NoMetadataReaderWarning extends Exception {
759 NoMetadataReaderWarning(String ext) {
760 super("No metadata reader for format *." + ext);
761 }
762 }
763
764 private static <T> void ifNotNull(T value, Consumer<T> setter) {
765 if (value != null) {
766 setter.accept(value);
767 }
768 }
769
770 /**
771 * Returns a {@link WayPoint} representation of this GPX image entry.
772 * @return a {@code WayPoint} representation of this GPX image entry (containing position, instant and elevation)
773 * @since 18065
774 */
775 public WayPoint asWayPoint() {
776 CachedLatLon position = getPos();
777 WayPoint wpt = null;
778 if (position != null) {
779 wpt = new WayPoint(position);
780 wpt.setInstant(exifTime);
781 Double ele = getElevation();
782 if (ele != null) {
783 wpt.put(GpxConstants.PT_ELE, ele.toString());
784 }
785 }
786 return wpt;
787 }
788}
Note: See TracBrowser for help on using the repository browser.