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

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

fix #20598 - see #20363 - Use getPos instead of getExifCoor (patch by taylor.smock)

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