001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.gpx; 003 004import java.io.File; 005import java.io.IOException; 006import java.util.Date; 007import java.util.List; 008import java.util.Objects; 009import java.util.function.Consumer; 010 011import org.openstreetmap.josm.data.coor.CachedLatLon; 012import org.openstreetmap.josm.data.coor.LatLon; 013import org.openstreetmap.josm.tools.ExifReader; 014import org.openstreetmap.josm.tools.JosmRuntimeException; 015import org.openstreetmap.josm.tools.Logging; 016 017import com.drew.imaging.jpeg.JpegMetadataReader; 018import com.drew.lang.CompoundException; 019import com.drew.metadata.Directory; 020import com.drew.metadata.Metadata; 021import com.drew.metadata.MetadataException; 022import com.drew.metadata.exif.ExifIFD0Directory; 023import com.drew.metadata.exif.GpsDirectory; 024import com.drew.metadata.iptc.IptcDirectory; 025import com.drew.metadata.jpeg.JpegDirectory; 026 027/** 028 * Stores info about each image 029 * @since 14205 (extracted from gui.layer.geoimage.ImageEntry) 030 */ 031public class GpxImageEntry implements Comparable<GpxImageEntry> { 032 private File file; 033 private Integer exifOrientation; 034 private LatLon exifCoor; 035 private Double exifImgDir; 036 private Date exifTime; 037 /** 038 * Flag isNewGpsData indicates that the GPS data of the image is new or has changed. 039 * GPS data includes the position, speed, elevation, time (e.g. as extracted from the GPS track). 040 * The flag can used to decide for which image file the EXIF GPS data is (re-)written. 041 */ 042 private boolean isNewGpsData; 043 /** Temporary source of GPS time if not correlated with GPX track. */ 044 private Date exifGpsTime; 045 046 private String iptcCaption; 047 private String iptcHeadline; 048 private List<String> iptcKeywords; 049 private String iptcObjectName; 050 051 /** 052 * The following values are computed from the correlation with the gpx track 053 * or extracted from the image EXIF data. 054 */ 055 private CachedLatLon pos; 056 /** Speed in kilometer per hour */ 057 private Double speed; 058 /** Elevation (altitude) in meters */ 059 private Double elevation; 060 /** The time after correlation with a gpx track */ 061 private Date gpsTime; 062 063 private int width; 064 private int height; 065 066 /** 067 * When the correlation dialog is open, we like to show the image position 068 * for the current time offset on the map in real time. 069 * On the other hand, when the user aborts this operation, the old values 070 * should be restored. We have a temporary copy, that overrides 071 * the normal values if it is not null. (This may be not the most elegant 072 * solution for this, but it works.) 073 */ 074 private GpxImageEntry tmp; 075 076 /** 077 * Constructs a new {@code GpxImageEntry}. 078 */ 079 public GpxImageEntry() {} 080 081 /** 082 * Constructs a new {@code GpxImageEntry} from an existing instance. 083 * @param other existing instance 084 * @since 14624 085 */ 086 public GpxImageEntry(GpxImageEntry other) { 087 file = other.file; 088 exifOrientation = other.exifOrientation; 089 exifCoor = other.exifCoor; 090 exifImgDir = other.exifImgDir; 091 exifTime = other.exifTime; 092 isNewGpsData = other.isNewGpsData; 093 exifGpsTime = other.exifGpsTime; 094 pos = other.pos; 095 speed = other.speed; 096 elevation = other.elevation; 097 gpsTime = other.gpsTime; 098 width = other.width; 099 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}