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.Objects; 008import java.util.function.Consumer; 009 010import org.openstreetmap.josm.data.coor.CachedLatLon; 011import org.openstreetmap.josm.data.coor.LatLon; 012import org.openstreetmap.josm.tools.ExifReader; 013import org.openstreetmap.josm.tools.JosmRuntimeException; 014import org.openstreetmap.josm.tools.Logging; 015 016import com.drew.imaging.jpeg.JpegMetadataReader; 017import com.drew.lang.CompoundException; 018import com.drew.metadata.Directory; 019import com.drew.metadata.Metadata; 020import com.drew.metadata.MetadataException; 021import com.drew.metadata.exif.ExifIFD0Directory; 022import com.drew.metadata.exif.GpsDirectory; 023import com.drew.metadata.jpeg.JpegDirectory; 024 025/** 026 * Stores info about each image 027 * @since 14205 (extracted from gui.layer.geoimage.ImageEntry) 028 */ 029public class GpxImageEntry implements Comparable<GpxImageEntry> { 030 private File file; 031 private Integer exifOrientation; 032 private LatLon exifCoor; 033 private Double exifImgDir; 034 private Date exifTime; 035 /** 036 * Flag isNewGpsData indicates that the GPS data of the image is new or has changed. 037 * GPS data includes the position, speed, elevation, time (e.g. as extracted from the GPS track). 038 * The flag can used to decide for which image file the EXIF GPS data is (re-)written. 039 */ 040 private boolean isNewGpsData; 041 /** Temporary source of GPS time if not correlated with GPX track. */ 042 private Date exifGpsTime; 043 044 /** 045 * The following values are computed from the correlation with the gpx track 046 * or extracted from the image EXIF data. 047 */ 048 private CachedLatLon pos; 049 /** Speed in kilometer per hour */ 050 private Double speed; 051 /** Elevation (altitude) in meters */ 052 private Double elevation; 053 /** The time after correlation with a gpx track */ 054 private Date gpsTime; 055 056 private int width; 057 private int height; 058 059 /** 060 * When the correlation dialog is open, we like to show the image position 061 * for the current time offset on the map in real time. 062 * On the other hand, when the user aborts this operation, the old values 063 * should be restored. We have a temporary copy, that overrides 064 * the normal values if it is not null. (This may be not the most elegant 065 * solution for this, but it works.) 066 */ 067 private GpxImageEntry tmp; 068 069 /** 070 * Constructs a new {@code GpxImageEntry}. 071 */ 072 public GpxImageEntry() {} 073 074 /** 075 * Constructs a new {@code GpxImageEntry} from an existing instance. 076 * @param other existing instance 077 * @since 14624 078 */ 079 public GpxImageEntry(GpxImageEntry other) { 080 file = other.file; 081 exifOrientation = other.exifOrientation; 082 exifCoor = other.exifCoor; 083 exifImgDir = other.exifImgDir; 084 exifTime = other.exifTime; 085 isNewGpsData = other.isNewGpsData; 086 exifGpsTime = other.exifGpsTime; 087 pos = other.pos; 088 speed = other.speed; 089 elevation = other.elevation; 090 gpsTime = other.gpsTime; 091 width = other.width; 092 height = other.height; 093 tmp = other.tmp; 094 } 095 096 /** 097 * Constructs a new {@code GpxImageEntry}. 098 * @param file Path to image file on disk 099 */ 100 public GpxImageEntry(File file) { 101 setFile(file); 102 } 103 104 /** 105 * Returns width of the image this GpxImageEntry represents. 106 * @return width of the image this GpxImageEntry represents 107 * @since 13220 108 */ 109 public int getWidth() { 110 return width; 111 } 112 113 /** 114 * Returns height of the image this GpxImageEntry represents. 115 * @return height of the image this GpxImageEntry represents 116 * @since 13220 117 */ 118 public int getHeight() { 119 return height; 120 } 121 122 /** 123 * Returns the position value. The position value from the temporary copy 124 * is returned if that copy exists. 125 * @return the position value 126 */ 127 public CachedLatLon getPos() { 128 if (tmp != null) 129 return tmp.pos; 130 return pos; 131 } 132 133 /** 134 * Returns the speed value. The speed value from the temporary copy is 135 * returned if that copy exists. 136 * @return the speed value 137 */ 138 public Double getSpeed() { 139 if (tmp != null) 140 return tmp.speed; 141 return speed; 142 } 143 144 /** 145 * Returns the elevation value. The elevation value from the temporary 146 * copy is returned if that copy exists. 147 * @return the elevation value 148 */ 149 public Double getElevation() { 150 if (tmp != null) 151 return tmp.elevation; 152 return elevation; 153 } 154 155 /** 156 * Returns the GPS time value. The GPS time value from the temporary copy 157 * is returned if that copy exists. 158 * @return the GPS time value 159 */ 160 public Date getGpsTime() { 161 if (tmp != null) 162 return getDefensiveDate(tmp.gpsTime); 163 return getDefensiveDate(gpsTime); 164 } 165 166 /** 167 * Convenient way to determine if this entry has a GPS time, without the cost of building a defensive copy. 168 * @return {@code true} if this entry has a GPS time 169 * @since 6450 170 */ 171 public boolean hasGpsTime() { 172 return (tmp != null && tmp.gpsTime != null) || gpsTime != null; 173 } 174 175 /** 176 * Returns associated file. 177 * @return associated file 178 */ 179 public File getFile() { 180 return file; 181 } 182 183 /** 184 * Returns EXIF orientation 185 * @return EXIF orientation 186 */ 187 public Integer getExifOrientation() { 188 return exifOrientation != null ? exifOrientation : 1; 189 } 190 191 /** 192 * Returns EXIF time 193 * @return EXIF time 194 */ 195 public Date getExifTime() { 196 return getDefensiveDate(exifTime); 197 } 198 199 /** 200 * Convenient way to determine if this entry has a EXIF time, without the cost of building a defensive copy. 201 * @return {@code true} if this entry has a EXIF time 202 * @since 6450 203 */ 204 public boolean hasExifTime() { 205 return exifTime != null; 206 } 207 208 /** 209 * Returns the EXIF GPS time. 210 * @return the EXIF GPS time 211 * @since 6392 212 */ 213 public Date getExifGpsTime() { 214 return getDefensiveDate(exifGpsTime); 215 } 216 217 /** 218 * Convenient way to determine if this entry has a EXIF GPS time, without the cost of building a defensive copy. 219 * @return {@code true} if this entry has a EXIF GPS time 220 * @since 6450 221 */ 222 public boolean hasExifGpsTime() { 223 return exifGpsTime != null; 224 } 225 226 private static Date getDefensiveDate(Date date) { 227 if (date == null) 228 return null; 229 return new Date(date.getTime()); 230 } 231 232 public LatLon getExifCoor() { 233 return exifCoor; 234 } 235 236 public Double getExifImgDir() { 237 if (tmp != null) 238 return tmp.exifImgDir; 239 return exifImgDir; 240 } 241 242 /** 243 * Sets the width of this GpxImageEntry. 244 * @param width set the width of this GpxImageEntry 245 * @since 13220 246 */ 247 public void setWidth(int width) { 248 this.width = width; 249 } 250 251 /** 252 * Sets the height of this GpxImageEntry. 253 * @param height set the height of this GpxImageEntry 254 * @since 13220 255 */ 256 public void setHeight(int height) { 257 this.height = height; 258 } 259 260 /** 261 * Sets the position. 262 * @param pos cached position 263 */ 264 public void setPos(CachedLatLon pos) { 265 this.pos = pos; 266 } 267 268 /** 269 * Sets the position. 270 * @param pos position (will be cached) 271 */ 272 public void setPos(LatLon pos) { 273 setPos(pos != null ? new CachedLatLon(pos) : null); 274 } 275 276 /** 277 * Sets the speed. 278 * @param speed speed 279 */ 280 public void setSpeed(Double speed) { 281 this.speed = speed; 282 } 283 284 /** 285 * Sets the elevation. 286 * @param elevation elevation 287 */ 288 public void setElevation(Double elevation) { 289 this.elevation = elevation; 290 } 291 292 /** 293 * Sets associated file. 294 * @param file associated file 295 */ 296 public void setFile(File file) { 297 this.file = file; 298 } 299 300 /** 301 * Sets EXIF orientation. 302 * @param exifOrientation EXIF orientation 303 */ 304 public void setExifOrientation(Integer exifOrientation) { 305 this.exifOrientation = exifOrientation; 306 } 307 308 /** 309 * Sets EXIF time. 310 * @param exifTime EXIF time 311 */ 312 public void setExifTime(Date exifTime) { 313 this.exifTime = getDefensiveDate(exifTime); 314 } 315 316 /** 317 * Sets the EXIF GPS time. 318 * @param exifGpsTime the EXIF GPS time 319 * @since 6392 320 */ 321 public void setExifGpsTime(Date exifGpsTime) { 322 this.exifGpsTime = getDefensiveDate(exifGpsTime); 323 } 324 325 public void setGpsTime(Date gpsTime) { 326 this.gpsTime = getDefensiveDate(gpsTime); 327 } 328 329 public void setExifCoor(LatLon exifCoor) { 330 this.exifCoor = exifCoor; 331 } 332 333 public void setExifImgDir(Double exifDir) { 334 this.exifImgDir = exifDir; 335 } 336 337 @Override 338 public int compareTo(GpxImageEntry image) { 339 if (exifTime != null && image.exifTime != null) 340 return exifTime.compareTo(image.exifTime); 341 else if (exifTime == null && image.exifTime == null) 342 return 0; 343 else if (exifTime == null) 344 return -1; 345 else 346 return 1; 347 } 348 349 @Override 350 public int hashCode() { 351 return Objects.hash(height, width, isNewGpsData, 352 elevation, exifCoor, exifGpsTime, exifImgDir, exifOrientation, exifTime, 353 file, gpsTime, pos, speed, tmp); 354 } 355 356 @Override 357 public boolean equals(Object obj) { 358 if (this == obj) 359 return true; 360 if (obj == null || getClass() != obj.getClass()) 361 return false; 362 GpxImageEntry other = (GpxImageEntry) obj; 363 return height == other.height 364 && width == other.width 365 && isNewGpsData == other.isNewGpsData 366 && Objects.equals(elevation, other.elevation) 367 && Objects.equals(exifCoor, other.exifCoor) 368 && Objects.equals(exifGpsTime, other.exifGpsTime) 369 && Objects.equals(exifImgDir, other.exifImgDir) 370 && Objects.equals(exifOrientation, other.exifOrientation) 371 && Objects.equals(exifTime, other.exifTime) 372 && Objects.equals(file, other.file) 373 && Objects.equals(gpsTime, other.gpsTime) 374 && Objects.equals(pos, other.pos) 375 && Objects.equals(speed, other.speed) 376 && Objects.equals(tmp, other.tmp); 377 } 378 379 /** 380 * Make a fresh copy and save it in the temporary variable. Use 381 * {@link #applyTmp()} or {@link #discardTmp()} if the temporary variable 382 * is not needed anymore. 383 */ 384 public void createTmp() { 385 tmp = new GpxImageEntry(this); 386 tmp.tmp = null; 387 } 388 389 /** 390 * Get temporary variable that is used for real time parameter 391 * adjustments. The temporary variable is created if it does not exist 392 * yet. Use {@link #applyTmp()} or {@link #discardTmp()} if the temporary 393 * variable is not needed anymore. 394 * @return temporary variable 395 */ 396 public GpxImageEntry getTmp() { 397 if (tmp == null) { 398 createTmp(); 399 } 400 return tmp; 401 } 402 403 /** 404 * Copy the values from the temporary variable to the main instance. The 405 * temporary variable is deleted. 406 * @see #discardTmp() 407 */ 408 public void applyTmp() { 409 if (tmp != null) { 410 pos = tmp.pos; 411 speed = tmp.speed; 412 elevation = tmp.elevation; 413 gpsTime = tmp.gpsTime; 414 exifImgDir = tmp.exifImgDir; 415 isNewGpsData = tmp.isNewGpsData; 416 tmp = null; 417 } 418 } 419 420 /** 421 * Delete the temporary variable. Temporary modifications are lost. 422 * @see #applyTmp() 423 */ 424 public void discardTmp() { 425 tmp = null; 426 } 427 428 /** 429 * If it has been tagged i.e. matched to a gpx track or retrieved lat/lon from exif 430 * @return {@code true} if it has been tagged 431 */ 432 public boolean isTagged() { 433 return pos != null; 434 } 435 436 /** 437 * String representation. (only partial info) 438 */ 439 @Override 440 public String toString() { 441 return file.getName()+": "+ 442 "pos = "+pos+" | "+ 443 "exifCoor = "+exifCoor+" | "+ 444 (tmp == null ? " tmp==null" : 445 " [tmp] pos = "+tmp.pos); 446 } 447 448 /** 449 * Indicates that the image has new GPS data. 450 * That flag is set by new GPS data providers. It is used e.g. by the photo_geotagging plugin 451 * to decide for which image file the EXIF GPS data needs to be (re-)written. 452 * @since 6392 453 */ 454 public void flagNewGpsData() { 455 isNewGpsData = true; 456 } 457 458 /** 459 * Remove the flag that indicates new GPS data. 460 * The flag is cleared by a new GPS data consumer. 461 */ 462 public void unflagNewGpsData() { 463 isNewGpsData = false; 464 } 465 466 /** 467 * Queries whether the GPS data changed. The flag value from the temporary 468 * copy is returned if that copy exists. 469 * @return {@code true} if GPS data changed, {@code false} otherwise 470 * @since 6392 471 */ 472 public boolean hasNewGpsData() { 473 if (tmp != null) 474 return tmp.isNewGpsData; 475 return isNewGpsData; 476 } 477 478 /** 479 * Extract GPS metadata from image EXIF. Has no effect if the image file is not set 480 * 481 * If successful, fills in the LatLon, speed, elevation, image direction, and other attributes 482 * @since 9270 483 */ 484 public void extractExif() { 485 486 Metadata metadata; 487 488 if (file == null) { 489 return; 490 } 491 492 try { 493 metadata = JpegMetadataReader.readMetadata(file); 494 } catch (CompoundException | IOException ex) { 495 Logging.error(ex); 496 setExifTime(null); 497 setExifCoor(null); 498 setPos(null); 499 return; 500 } 501 502 // Changed to silently cope with no time info in exif. One case 503 // of person having time that couldn't be parsed, but valid GPS info 504 try { 505 setExifTime(ExifReader.readTime(metadata)); 506 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException ex) { 507 Logging.warn(ex); 508 setExifTime(null); 509 } 510 511 final Directory dir = metadata.getFirstDirectoryOfType(JpegDirectory.class); 512 final Directory dirExif = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); 513 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class); 514 515 try { 516 if (dirExif != null) { 517 setExifOrientation(dirExif.getInt(ExifIFD0Directory.TAG_ORIENTATION)); 518 } 519 } catch (MetadataException ex) { 520 Logging.debug(ex); 521 } 522 523 try { 524 if (dir != null) { 525 // there are cases where these do not match width and height stored in dirExif 526 setWidth(dir.getInt(JpegDirectory.TAG_IMAGE_WIDTH)); 527 setHeight(dir.getInt(JpegDirectory.TAG_IMAGE_HEIGHT)); 528 } 529 } catch (MetadataException ex) { 530 Logging.debug(ex); 531 } 532 533 if (dirGps == null) { 534 setExifCoor(null); 535 setPos(null); 536 return; 537 } 538 539 ifNotNull(ExifReader.readSpeed(dirGps), this::setSpeed); 540 ifNotNull(ExifReader.readElevation(dirGps), this::setElevation); 541 542 try { 543 setExifCoor(ExifReader.readLatLon(dirGps)); 544 setPos(getExifCoor()); 545 } catch (MetadataException | IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271) 546 Logging.error("Error reading EXIF from file: " + ex); 547 setExifCoor(null); 548 setPos(null); 549 } 550 551 try { 552 ifNotNull(ExifReader.readDirection(dirGps), this::setExifImgDir); 553 } catch (IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271) 554 Logging.debug(ex); 555 } 556 557 ifNotNull(dirGps.getGpsDate(), this::setExifGpsTime); 558 } 559 560 private static <T> void ifNotNull(T value, Consumer<T> setter) { 561 if (value != null) { 562 setter.accept(value); 563 } 564 } 565}