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

Last change on this file since 17715 was 17715, checked in by simon04, 3 years ago

see #14176 - Migrate GPX to Instant

  • Property svn:eol-style set to native
File size: 23.1 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 EXIF orientation
213 * @return EXIF orientation
214 */
215 public Integer getExifOrientation() {
216 return exifOrientation != null ? exifOrientation : 1;
217 }
218
219 /**
220 * Returns EXIF time
221 * @return EXIF time
222 * @deprecated Use {@link #getExifInstant}
223 */
224 @Deprecated
225 public Date getExifTime() {
226 return getDefensiveDate(exifTime);
227 }
228
229 /**
230 * Returns EXIF time
231 * @return EXIF time
232 */
233 public Instant getExifInstant() {
234 return exifTime;
235 }
236
237 /**
238 * Convenient way to determine if this entry has a EXIF time, without the cost of building a defensive copy.
239 * @return {@code true} if this entry has a EXIF time
240 * @since 6450
241 */
242 public boolean hasExifTime() {
243 return exifTime != null;
244 }
245
246 /**
247 * Returns the EXIF GPS time.
248 * @return the EXIF GPS time
249 * @since 6392
250 * @deprecated Use {@link #getExifGpsInstant}
251 */
252 @Deprecated
253 public Date getExifGpsTime() {
254 return getDefensiveDate(exifGpsTime);
255 }
256
257 /**
258 * Returns the EXIF GPS time.
259 * @return the EXIF GPS time
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 * @deprecated Use {@link #setExifTime(Instant)}
360 */
361 @Deprecated
362 public void setExifTime(Date exifTime) {
363 this.exifTime = exifTime == null ? null : exifTime.toInstant();
364 }
365
366 /**
367 * Sets the EXIF GPS time.
368 * @param exifGpsTime the EXIF GPS time
369 * @since 6392
370 * @deprecated Use {@link #setExifGpsTime(Instant)}
371 */
372 @Deprecated
373 public void setExifGpsTime(Date exifGpsTime) {
374 this.exifGpsTime = exifGpsTime == null ? null : exifGpsTime.toInstant();
375 }
376
377 /**
378 * Sets the GPS time.
379 * @param gpsTime the GPS time
380 * @deprecated Use {@link #setGpsTime(Instant)}
381 */
382 @Deprecated
383 public void setGpsTime(Date gpsTime) {
384 this.gpsTime = gpsTime == null ? null : gpsTime.toInstant();
385 }
386
387 /**
388 * Sets EXIF time.
389 * @param exifTime EXIF time
390 */
391 public void setExifTime(Instant exifTime) {
392 this.exifTime = exifTime;
393 }
394
395 /**
396 * Sets the EXIF GPS time.
397 * @param exifGpsTime the EXIF GPS time
398 */
399 public void setExifGpsTime(Instant exifGpsTime) {
400 this.exifGpsTime = exifGpsTime;
401 }
402
403 /**
404 * Sets the GPS time.
405 * @param gpsTime the GPS time
406 */
407 public void setGpsTime(Instant gpsTime) {
408 this.gpsTime = gpsTime;
409 }
410
411 public void setExifCoor(LatLon exifCoor) {
412 this.exifCoor = exifCoor;
413 }
414
415 public void setExifImgDir(Double exifDir) {
416 this.exifImgDir = exifDir;
417 }
418
419 /**
420 * Sets the IPTC caption.
421 * @param iptcCaption the IPTC caption
422 * @since 15219
423 */
424 public void setIptcCaption(String iptcCaption) {
425 this.iptcCaption = iptcCaption;
426 }
427
428 /**
429 * Sets the IPTC headline.
430 * @param iptcHeadline the IPTC headline
431 * @since 15219
432 */
433 public void setIptcHeadline(String iptcHeadline) {
434 this.iptcHeadline = iptcHeadline;
435 }
436
437 /**
438 * Sets the IPTC keywords.
439 * @param iptcKeywords the IPTC keywords
440 * @since 15219
441 */
442 public void setIptcKeywords(List<String> iptcKeywords) {
443 this.iptcKeywords = iptcKeywords;
444 }
445
446 /**
447 * Sets the IPTC object name.
448 * @param iptcObjectName the IPTC object name
449 * @since 15219
450 */
451 public void setIptcObjectName(String iptcObjectName) {
452 this.iptcObjectName = iptcObjectName;
453 }
454
455 /**
456 * Returns the IPTC caption.
457 * @return the IPTC caption
458 * @since 15219
459 */
460 public String getIptcCaption() {
461 return iptcCaption;
462 }
463
464 /**
465 * Returns the IPTC headline.
466 * @return the IPTC headline
467 * @since 15219
468 */
469 public String getIptcHeadline() {
470 return iptcHeadline;
471 }
472
473 /**
474 * Returns the IPTC keywords.
475 * @return the IPTC keywords
476 * @since 15219
477 */
478 public List<String> getIptcKeywords() {
479 return iptcKeywords;
480 }
481
482 /**
483 * Returns the IPTC object name.
484 * @return the IPTC object name
485 * @since 15219
486 */
487 public String getIptcObjectName() {
488 return iptcObjectName;
489 }
490
491 @Override
492 public int compareTo(GpxImageEntry image) {
493 if (exifTime != null && image.exifTime != null)
494 return exifTime.compareTo(image.exifTime);
495 else if (exifTime == null && image.exifTime == null)
496 return 0;
497 else if (exifTime == null)
498 return -1;
499 else
500 return 1;
501 }
502
503 @Override
504 public int hashCode() {
505 return Objects.hash(height, width, isNewGpsData,
506 elevation, exifCoor, exifGpsTime, exifImgDir, exifOrientation, exifTime,
507 iptcCaption, iptcHeadline, iptcKeywords, iptcObjectName,
508 file, gpsTime, pos, speed, tmp);
509 }
510
511 @Override
512 public boolean equals(Object obj) {
513 if (this == obj)
514 return true;
515 if (obj == null || getClass() != obj.getClass())
516 return false;
517 GpxImageEntry other = (GpxImageEntry) obj;
518 return height == other.height
519 && width == other.width
520 && isNewGpsData == other.isNewGpsData
521 && Objects.equals(elevation, other.elevation)
522 && Objects.equals(exifCoor, other.exifCoor)
523 && Objects.equals(exifGpsTime, other.exifGpsTime)
524 && Objects.equals(exifImgDir, other.exifImgDir)
525 && Objects.equals(exifOrientation, other.exifOrientation)
526 && Objects.equals(exifTime, other.exifTime)
527 && Objects.equals(iptcCaption, other.iptcCaption)
528 && Objects.equals(iptcHeadline, other.iptcHeadline)
529 && Objects.equals(iptcKeywords, other.iptcKeywords)
530 && Objects.equals(iptcObjectName, other.iptcObjectName)
531 && Objects.equals(file, other.file)
532 && Objects.equals(gpsTime, other.gpsTime)
533 && Objects.equals(pos, other.pos)
534 && Objects.equals(speed, other.speed)
535 && Objects.equals(tmp, other.tmp);
536 }
537
538 /**
539 * Make a fresh copy and save it in the temporary variable. Use
540 * {@link #applyTmp()} or {@link #discardTmp()} if the temporary variable
541 * is not needed anymore.
542 */
543 public void createTmp() {
544 tmp = new GpxImageEntry(this);
545 tmp.tmp = null;
546 }
547
548 /**
549 * Get temporary variable that is used for real time parameter
550 * adjustments. The temporary variable is created if it does not exist
551 * yet. Use {@link #applyTmp()} or {@link #discardTmp()} if the temporary
552 * variable is not needed anymore.
553 * @return temporary variable
554 */
555 public GpxImageEntry getTmp() {
556 if (tmp == null) {
557 createTmp();
558 }
559 return tmp;
560 }
561
562 /**
563 * Copy the values from the temporary variable to the main instance. The
564 * temporary variable is deleted.
565 * @see #discardTmp()
566 */
567 public void applyTmp() {
568 if (tmp != null) {
569 pos = tmp.pos;
570 speed = tmp.speed;
571 elevation = tmp.elevation;
572 gpsTime = tmp.gpsTime;
573 exifImgDir = tmp.exifImgDir;
574 isNewGpsData = tmp.isNewGpsData;
575 tmp = null;
576 }
577 tmpUpdated();
578 }
579
580 /**
581 * Delete the temporary variable. Temporary modifications are lost.
582 * @see #applyTmp()
583 */
584 public void discardTmp() {
585 tmp = null;
586 tmpUpdated();
587 }
588
589 /**
590 * If it has been tagged i.e. matched to a gpx track or retrieved lat/lon from exif
591 * @return {@code true} if it has been tagged
592 */
593 public boolean isTagged() {
594 return pos != null;
595 }
596
597 /**
598 * String representation. (only partial info)
599 */
600 @Override
601 public String toString() {
602 return file.getName()+": "+
603 "pos = "+pos+" | "+
604 "exifCoor = "+exifCoor+" | "+
605 (tmp == null ? " tmp==null" :
606 " [tmp] pos = "+tmp.pos);
607 }
608
609 /**
610 * Indicates that the image has new GPS data.
611 * That flag is set by new GPS data providers. It is used e.g. by the photo_geotagging plugin
612 * to decide for which image file the EXIF GPS data needs to be (re-)written.
613 * @since 6392
614 */
615 public void flagNewGpsData() {
616 isNewGpsData = true;
617 }
618
619 /**
620 * Indicate that the temporary copy has been updated. Mostly used to prevent UI issues.
621 * By default, this is a no-op. Override when needed in subclasses.
622 * @since 17579
623 */
624 protected void tmpUpdated() {
625 // No-op by default
626 }
627
628 @Override
629 public BBox getBBox() {
630 // new BBox(LatLon) is null safe.
631 // Use `getPos` instead of `getExifCoor` since the image may be correlated against a GPX track
632 return new BBox(this.getPos());
633 }
634
635 /**
636 * Remove the flag that indicates new GPS data.
637 * The flag is cleared by a new GPS data consumer.
638 */
639 public void unflagNewGpsData() {
640 isNewGpsData = false;
641 }
642
643 /**
644 * Queries whether the GPS data changed. The flag value from the temporary
645 * copy is returned if that copy exists.
646 * @return {@code true} if GPS data changed, {@code false} otherwise
647 * @since 6392
648 */
649 public boolean hasNewGpsData() {
650 if (tmp != null)
651 return tmp.isNewGpsData;
652 return isNewGpsData;
653 }
654
655 /**
656 * Extract GPS metadata from image EXIF. Has no effect if the image file is not set
657 *
658 * If successful, fills in the LatLon, speed, elevation, image direction, and other attributes
659 * @since 9270
660 */
661 public void extractExif() {
662
663 Metadata metadata;
664
665 if (file == null) {
666 return;
667 }
668
669 String fn = file.getName();
670
671 try {
672 // try to parse metadata according to extension
673 String ext = fn.substring(fn.lastIndexOf('.') + 1).toLowerCase(Locale.US);
674 switch (ext) {
675 case "jpg":
676 case "jpeg":
677 metadata = JpegMetadataReader.readMetadata(file);
678 break;
679 case "tif":
680 case "tiff":
681 metadata = TiffMetadataReader.readMetadata(file);
682 break;
683 case "png":
684 metadata = PngMetadataReader.readMetadata(file);
685 break;
686 default:
687 throw new NoMetadataReaderWarning(ext);
688 }
689 } catch (JpegProcessingException | TiffProcessingException | PngProcessingException | IOException
690 | NoMetadataReaderWarning topException) {
691 //try other formats (e.g. JPEG file with .png extension)
692 try {
693 metadata = JpegMetadataReader.readMetadata(file);
694 } catch (JpegProcessingException | IOException ex1) {
695 try {
696 metadata = TiffMetadataReader.readMetadata(file);
697 } catch (TiffProcessingException | IOException ex2) {
698 try {
699 metadata = PngMetadataReader.readMetadata(file);
700 } catch (PngProcessingException | IOException ex3) {
701 Logging.warn(topException);
702 Logging.info(tr("Can''t parse metadata for file \"{0}\". Using last modified date as timestamp.", fn));
703 setExifTime(Instant.ofEpochMilli(file.lastModified()));
704 setExifCoor(null);
705 setPos(null);
706 return;
707 }
708 }
709 }
710 }
711
712 // Changed to silently cope with no time info in exif. One case
713 // of person having time that couldn't be parsed, but valid GPS info
714 Instant time = null;
715 try {
716 time = ExifReader.readInstant(metadata);
717 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException ex) {
718 Logging.warn(ex);
719 }
720
721 if (time == null) {
722 Logging.info(tr("No EXIF time in file \"{0}\". Using last modified date as timestamp.", fn));
723 time = Instant.ofEpochMilli(file.lastModified()); //use lastModified time if no EXIF time present
724 }
725 setExifTime(time);
726
727 final Directory dir = metadata.getFirstDirectoryOfType(JpegDirectory.class);
728 final Directory dirExif = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
729 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
730
731 try {
732 if (dirExif != null) {
733 setExifOrientation(dirExif.getInt(ExifIFD0Directory.TAG_ORIENTATION));
734 }
735 } catch (MetadataException ex) {
736 Logging.debug(ex);
737 }
738
739 try {
740 if (dir != null) {
741 // there are cases where these do not match width and height stored in dirExif
742 setWidth(dir.getInt(JpegDirectory.TAG_IMAGE_WIDTH));
743 setHeight(dir.getInt(JpegDirectory.TAG_IMAGE_HEIGHT));
744 }
745 } catch (MetadataException ex) {
746 Logging.debug(ex);
747 }
748
749 if (dirGps == null || dirGps.getTagCount() <= 1) {
750 setExifCoor(null);
751 setPos(null);
752 return;
753 }
754
755 ifNotNull(ExifReader.readSpeed(dirGps), this::setSpeed);
756 ifNotNull(ExifReader.readElevation(dirGps), this::setElevation);
757
758 try {
759 setExifCoor(ExifReader.readLatLon(dirGps));
760 setPos(getExifCoor());
761 } catch (MetadataException | IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
762 Logging.error("Error reading EXIF from file: " + ex);
763 setExifCoor(null);
764 setPos(null);
765 }
766
767 try {
768 ifNotNull(ExifReader.readDirection(dirGps), this::setExifImgDir);
769 } catch (IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
770 Logging.debug(ex);
771 }
772
773 ifNotNull(dirGps.getGpsDate(), d -> setExifGpsTime(d.toInstant()));
774
775 IptcDirectory dirIptc = metadata.getFirstDirectoryOfType(IptcDirectory.class);
776 if (dirIptc != null) {
777 ifNotNull(ExifReader.readCaption(dirIptc), this::setIptcCaption);
778 ifNotNull(ExifReader.readHeadline(dirIptc), this::setIptcHeadline);
779 ifNotNull(ExifReader.readKeywords(dirIptc), this::setIptcKeywords);
780 ifNotNull(ExifReader.readObjectName(dirIptc), this::setIptcObjectName);
781 }
782 }
783
784 private static class NoMetadataReaderWarning extends Exception {
785 NoMetadataReaderWarning(String ext) {
786 super("No metadata reader for format *." + ext);
787 }
788 }
789
790 private static <T> void ifNotNull(T value, Consumer<T> setter) {
791 if (value != null) {
792 setter.accept(value);
793 }
794 }
795}
Note: See TracBrowser for help on using the repository browser.