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

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

see #17848 - update hashCode/equals methods

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