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 Object c; 315 try { 316 c = super.clone(); 317 } catch (CloneNotSupportedException e) { 318 throw new RuntimeException(e); 319 } 320 return (ImageEntry) c; 321 } 322 323 @Override 324 public int compareTo(ImageEntry image) { 325 if (exifTime != null && image.exifTime != null) 326 return exifTime.compareTo(image.exifTime); 327 else if (exifTime == null && image.exifTime == null) 328 return 0; 329 else if (exifTime == null) 330 return -1; 331 else 332 return 1; 333 } 334 335 /** 336 * Make a fresh copy and save it in the temporary variable. Use 337 * {@link #applyTmp()} or {@link #discardTmp()} if the temporary variable 338 * is not needed anymore. 339 */ 340 public void createTmp() { 341 tmp = clone(); 342 tmp.tmp = null; 343 } 344 345 /** 346 * Get temporary variable that is used for real time parameter 347 * adjustments. The temporary variable is created if it does not exist 348 * yet. Use {@link #applyTmp()} or {@link #discardTmp()} if the temporary 349 * variable is not needed anymore. 350 * @return temporary variable 351 */ 352 public ImageEntry getTmp() { 353 if (tmp == null) { 354 createTmp(); 355 } 356 return tmp; 357 } 358 359 /** 360 * Copy the values from the temporary variable to the main instance. The 361 * temporary variable is deleted. 362 * @see #discardTmp() 363 */ 364 public void applyTmp() { 365 if (tmp != null) { 366 pos = tmp.pos; 367 speed = tmp.speed; 368 elevation = tmp.elevation; 369 gpsTime = tmp.gpsTime; 370 exifImgDir = tmp.exifImgDir; 371 tmp = null; 372 } 373 } 374 375 /** 376 * Delete the temporary variable. Temporary modifications are lost. 377 * @see #applyTmp() 378 */ 379 public void discardTmp() { 380 tmp = null; 381 } 382 383 /** 384 * If it has been tagged i.e. matched to a gpx track or retrieved lat/lon from exif 385 * @return {@code true} if it has been tagged 386 */ 387 public boolean isTagged() { 388 return pos != null; 389 } 390 391 /** 392 * String representation. (only partial info) 393 */ 394 @Override 395 public String toString() { 396 return file.getName()+": "+ 397 "pos = "+pos+" | "+ 398 "exifCoor = "+exifCoor+" | "+ 399 (tmp == null ? " tmp==null" : 400 " [tmp] pos = "+tmp.pos); 401 } 402 403 /** 404 * Indicates that the image has new GPS data. 405 * That flag is set by new GPS data providers. It is used e.g. by the photo_geotagging plugin 406 * to decide for which image file the EXIF GPS data needs to be (re-)written. 407 * @since 6392 408 */ 409 public void flagNewGpsData() { 410 isNewGpsData = true; 411 } 412 413 /** 414 * Remove the flag that indicates new GPS data. 415 * The flag is cleared by a new GPS data consumer. 416 */ 417 public void unflagNewGpsData() { 418 isNewGpsData = false; 419 } 420 421 /** 422 * Queries whether the GPS data changed. 423 * @return {@code true} if GPS data changed, {@code false} otherwise 424 * @since 6392 425 */ 426 public boolean hasNewGpsData() { 427 return isNewGpsData; 428 } 429 430 /** 431 * Extract GPS metadata from image EXIF. Has no effect if the image file is not set 432 * 433 * If successful, fills in the LatLon, speed, elevation, image direction, and other attributes 434 * @since 9270 435 */ 436 public void extractExif() { 437 438 Metadata metadata; 439 Directory dirExif; 440 GpsDirectory dirGps; 441 442 if (file == null) { 443 return; 444 } 445 446 // Changed to silently cope with no time info in exif. One case 447 // of person having time that couldn't be parsed, but valid GPS info 448 try { 449 setExifTime(ExifReader.readTime(file)); 450 } catch (RuntimeException ex) { 451 setExifTime(null); 452 } 453 454 try { 455 metadata = JpegMetadataReader.readMetadata(file); 456 dirExif = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); 457 dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class); 458 } catch (CompoundException | IOException p) { 459 Main.warn(p); 460 setExifCoor(null); 461 setPos(null); 462 return; 463 } 464 465 try { 466 if (dirExif != null) { 467 int orientation = dirExif.getInt(ExifIFD0Directory.TAG_ORIENTATION); 468 setExifOrientation(orientation); 469 } 470 } catch (MetadataException ex) { 471 if (Main.isDebugEnabled()) { 472 Main.debug(ex.getMessage()); 473 } 474 } 475 476 if (dirGps == null) { 477 setExifCoor(null); 478 setPos(null); 479 return; 480 } 481 482 try { 483 double speed = dirGps.getDouble(GpsDirectory.TAG_SPEED); 484 String speedRef = dirGps.getString(GpsDirectory.TAG_SPEED_REF); 485 if ("M".equalsIgnoreCase(speedRef)) { 486 // miles per hour 487 speed *= SystemOfMeasurement.IMPERIAL.bValue / 1000; 488 } else if ("N".equalsIgnoreCase(speedRef)) { 489 // knots == nautical miles per hour 490 speed *= SystemOfMeasurement.NAUTICAL_MILE.bValue / 1000; 491 } 492 // default is K (km/h) 493 setSpeed(speed); 494 } catch (Exception ex) { 495 if (Main.isDebugEnabled()) { 496 Main.debug(ex.getMessage()); 497 } 498 } 499 500 try { 501 double ele = dirGps.getDouble(GpsDirectory.TAG_ALTITUDE); 502 int d = dirGps.getInt(GpsDirectory.TAG_ALTITUDE_REF); 503 if (d == 1) { 504 ele *= -1; 505 } 506 setElevation(ele); 507 } catch (MetadataException ex) { 508 if (Main.isDebugEnabled()) { 509 Main.debug(ex.getMessage()); 510 } 511 } 512 513 try { 514 LatLon latlon = ExifReader.readLatLon(dirGps); 515 setExifCoor(latlon); 516 setPos(getExifCoor()); 517 518 } catch (Exception ex) { // (other exceptions, e.g. #5271) 519 Main.error("Error reading EXIF from file: " + ex); 520 setExifCoor(null); 521 setPos(null); 522 } 523 524 try { 525 Double direction = ExifReader.readDirection(dirGps); 526 if (direction != null) { 527 setExifImgDir(direction); 528 } 529 } catch (Exception ex) { // (CompoundException and other exceptions, e.g. #5271) 530 if (Main.isDebugEnabled()) { 531 Main.debug(ex.getMessage()); 532 } 533 } 534 535 // Time and date. We can have these cases: 536 // 1) GPS_TIME_STAMP not set -> date/time will be null 537 // 2) GPS_DATE_STAMP not set -> use EXIF date or set to default 538 // 3) GPS_TIME_STAMP and GPS_DATE_STAMP are set 539 int[] timeStampComps = dirGps.getIntArray(GpsDirectory.TAG_TIME_STAMP); 540 if (timeStampComps != null) { 541 int gpsHour = timeStampComps[0]; 542 int gpsMin = timeStampComps[1]; 543 int gpsSec = timeStampComps[2]; 544 Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC")); 545 546 // We have the time. Next step is to check if the GPS date stamp is set. 547 // dirGps.getString() always succeeds, but the return value might be null. 548 String dateStampStr = dirGps.getString(GpsDirectory.TAG_DATE_STAMP); 549 if (dateStampStr != null && dateStampStr.matches("^\\d+:\\d+:\\d+$")) { 550 String[] dateStampComps = dateStampStr.split(":"); 551 cal.set(Calendar.YEAR, Integer.parseInt(dateStampComps[0])); 552 cal.set(Calendar.MONTH, Integer.parseInt(dateStampComps[1]) - 1); 553 cal.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dateStampComps[2])); 554 } else { 555 // No GPS date stamp in EXIF data. Copy it from EXIF time. 556 // Date is not set if EXIF time is not available. 557 if (hasExifTime()) { 558 // Time not set yet, so we can copy everything, not just date. 559 cal.setTime(getExifTime()); 560 } 561 } 562 563 cal.set(Calendar.HOUR_OF_DAY, gpsHour); 564 cal.set(Calendar.MINUTE, gpsMin); 565 cal.set(Calendar.SECOND, gpsSec); 566 567 setExifGpsTime(cal.getTime()); 568 } 569 } 570}