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