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