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