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