001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.gpx; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Dimension; 007import java.awt.image.BufferedImage; 008import java.io.File; 009import java.io.IOException; 010import java.time.Instant; 011import java.util.Date; 012import java.util.List; 013import java.util.Locale; 014import java.util.Map; 015import java.util.Objects; 016import java.util.function.Consumer; 017import java.util.stream.Stream; 018 019import javax.imageio.IIOParam; 020 021import org.openstreetmap.josm.data.IQuadBucketType; 022import org.openstreetmap.josm.data.coor.CachedLatLon; 023import org.openstreetmap.josm.data.coor.LatLon; 024import org.openstreetmap.josm.data.imagery.street_level.Projections; 025import org.openstreetmap.josm.data.osm.BBox; 026import org.openstreetmap.josm.tools.ExifReader; 027import org.openstreetmap.josm.tools.JosmRuntimeException; 028import org.openstreetmap.josm.tools.Logging; 029 030import com.drew.imaging.jpeg.JpegMetadataReader; 031import com.drew.imaging.jpeg.JpegProcessingException; 032import com.drew.imaging.png.PngMetadataReader; 033import com.drew.imaging.png.PngProcessingException; 034import com.drew.imaging.tiff.TiffMetadataReader; 035import com.drew.imaging.tiff.TiffProcessingException; 036import com.drew.metadata.Directory; 037import com.drew.metadata.Metadata; 038import com.drew.metadata.MetadataException; 039import com.drew.metadata.exif.ExifIFD0Directory; 040import com.drew.metadata.exif.GpsDirectory; 041import com.drew.metadata.iptc.IptcDirectory; 042import com.drew.metadata.jpeg.JpegDirectory; 043import com.drew.metadata.xmp.XmpDirectory; 044 045/** 046 * Stores info about each image 047 * @since 14205 (extracted from gui.layer.geoimage.ImageEntry) 048 */ 049public class GpxImageEntry implements Comparable<GpxImageEntry>, IQuadBucketType { 050 private File file; 051 private Integer exifOrientation; 052 private LatLon exifCoor; 053 private Double exifImgDir; 054 private Instant exifTime; 055 private Projections cameraProjection = Projections.UNKNOWN; 056 /** 057 * Flag isNewGpsData indicates that the GPS data of the image is new or has changed. 058 * GPS data includes the position, speed, elevation, time (e.g. as extracted from the GPS track). 059 * The flag can used to decide for which image file the EXIF GPS data is (re-)written. 060 */ 061 private boolean isNewGpsData; 062 /** Temporary source of GPS time if not correlated with GPX track. */ 063 private Instant exifGpsTime; 064 065 private String iptcCaption; 066 private String iptcHeadline; 067 private List<String> iptcKeywords; 068 private String iptcObjectName; 069 070 /** 071 * The following values are computed from the correlation with the gpx track 072 * or extracted from the image EXIF data. 073 */ 074 private CachedLatLon pos; 075 /** Speed in kilometer per hour */ 076 private Double speed; 077 /** Elevation (altitude) in meters */ 078 private Double elevation; 079 /** The time after correlation with a gpx track */ 080 private Instant gpsTime; 081 082 private int width; 083 private int height; 084 085 /** 086 * When the correlation dialog is open, we like to show the image position 087 * for the current time offset on the map in real time. 088 * On the other hand, when the user aborts this operation, the old values 089 * should be restored. We have a temporary copy, that overrides 090 * the normal values if it is not null. (This may be not the most elegant 091 * solution for this, but it works.) 092 */ 093 private GpxImageEntry tmp; 094 095 /** 096 * Constructs a new {@code GpxImageEntry}. 097 */ 098 public GpxImageEntry() {} 099 100 /** 101 * Constructs a new {@code GpxImageEntry} from an existing instance. 102 * @param other existing instance 103 * @since 14624 104 */ 105 public GpxImageEntry(GpxImageEntry other) { 106 file = other.file; 107 exifOrientation = other.exifOrientation; 108 exifCoor = other.exifCoor; 109 exifImgDir = other.exifImgDir; 110 exifTime = other.exifTime; 111 isNewGpsData = other.isNewGpsData; 112 exifGpsTime = other.exifGpsTime; 113 pos = other.pos; 114 speed = other.speed; 115 elevation = other.elevation; 116 gpsTime = other.gpsTime; 117 width = other.width; 118 height = other.height; 119 tmp = other.tmp; 120 } 121 122 /** 123 * Constructs a new {@code GpxImageEntry}. 124 * @param file Path to image file on disk 125 */ 126 public GpxImageEntry(File file) { 127 setFile(file); 128 } 129 130 /** 131 * Returns width of the image this GpxImageEntry represents. 132 * @return width of the image this GpxImageEntry represents 133 * @since 13220 134 */ 135 public int getWidth() { 136 return width; 137 } 138 139 /** 140 * Returns height of the image this GpxImageEntry represents. 141 * @return height of the image this GpxImageEntry represents 142 * @since 13220 143 */ 144 public int getHeight() { 145 return height; 146 } 147 148 /** 149 * Returns the position value. The position value from the temporary copy 150 * is returned if that copy exists. 151 * @return the position value 152 */ 153 public CachedLatLon getPos() { 154 if (tmp != null) 155 return tmp.pos; 156 return pos; 157 } 158 159 /** 160 * Returns the speed value. The speed value from the temporary copy is 161 * returned if that copy exists. 162 * @return the speed value 163 */ 164 public Double getSpeed() { 165 if (tmp != null) 166 return tmp.speed; 167 return speed; 168 } 169 170 /** 171 * Returns the elevation value. The elevation value from the temporary 172 * copy is returned if that copy exists. 173 * @return the elevation value 174 */ 175 public Double getElevation() { 176 if (tmp != null) 177 return tmp.elevation; 178 return elevation; 179 } 180 181 /** 182 * Returns the GPS time value. The GPS time value from the temporary copy 183 * is returned if that copy exists. 184 * @return the GPS time value 185 * @deprecated Use {@link #getGpsInstant} 186 */ 187 @Deprecated 188 public Date getGpsTime() { 189 if (tmp != null) 190 return getDefensiveDate(tmp.gpsTime); 191 return getDefensiveDate(gpsTime); 192 } 193 194 /** 195 * Returns the GPS time value. The GPS time value from the temporary copy 196 * is returned if that copy exists. 197 * @return the GPS time value 198 */ 199 public Instant getGpsInstant() { 200 return tmp != null ? tmp.gpsTime : gpsTime; 201 } 202 203 /** 204 * Convenient way to determine if this entry has a GPS time, without the cost of building a defensive copy. 205 * @return {@code true} if this entry has a GPS time 206 * @since 6450 207 */ 208 public boolean hasGpsTime() { 209 return (tmp != null && tmp.gpsTime != null) || gpsTime != null; 210 } 211 212 /** 213 * Returns associated file. 214 * @return associated file 215 */ 216 public File getFile() { 217 return file; 218 } 219 220 /** 221 * Returns a display name for this entry 222 * @return a display name for this entry 223 */ 224 public String getDisplayName() { 225 return file == null ? "" : file.getName(); 226 } 227 228 /** 229 * Returns EXIF orientation 230 * @return EXIF orientation 231 */ 232 public Integer getExifOrientation() { 233 return exifOrientation != null ? exifOrientation : 1; 234 } 235 236 /** 237 * Returns EXIF time 238 * @return EXIF time 239 * @since 17715 240 */ 241 public Instant getExifInstant() { 242 return exifTime; 243 } 244 245 /** 246 * Convenient way to determine if this entry has a EXIF time, without the cost of building a defensive copy. 247 * @return {@code true} if this entry has a EXIF time 248 * @since 6450 249 */ 250 public boolean hasExifTime() { 251 return exifTime != null; 252 } 253 254 /** 255 * Returns the EXIF GPS time. 256 * @return the EXIF GPS time 257 * @since 6392 258 * @deprecated Use {@link #getExifGpsInstant} 259 */ 260 @Deprecated 261 public Date getExifGpsTime() { 262 return getDefensiveDate(exifGpsTime); 263 } 264 265 /** 266 * Returns the EXIF GPS time. 267 * @return the EXIF GPS time 268 * @since 17715 269 */ 270 public Instant getExifGpsInstant() { 271 return exifGpsTime; 272 } 273 274 /** 275 * Convenient way to determine if this entry has a EXIF GPS time, without the cost of building a defensive copy. 276 * @return {@code true} if this entry has a EXIF GPS time 277 * @since 6450 278 */ 279 public boolean hasExifGpsTime() { 280 return exifGpsTime != null; 281 } 282 283 private static Date getDefensiveDate(Instant date) { 284 if (date == null) 285 return null; 286 return Date.from(date); 287 } 288 289 public LatLon getExifCoor() { 290 return exifCoor; 291 } 292 293 public Double getExifImgDir() { 294 if (tmp != null) 295 return tmp.exifImgDir; 296 return exifImgDir; 297 } 298 299 /** 300 * Sets the width of this GpxImageEntry. 301 * @param width set the width of this GpxImageEntry 302 * @since 13220 303 */ 304 public void setWidth(int width) { 305 this.width = width; 306 } 307 308 /** 309 * Sets the height of this GpxImageEntry. 310 * @param height set the height of this GpxImageEntry 311 * @since 13220 312 */ 313 public void setHeight(int height) { 314 this.height = height; 315 } 316 317 /** 318 * Sets the position. 319 * @param pos cached position 320 */ 321 public void setPos(CachedLatLon pos) { 322 this.pos = pos; 323 } 324 325 /** 326 * Sets the position. 327 * @param pos position (will be cached) 328 */ 329 public void setPos(LatLon pos) { 330 setPos(pos != null ? new CachedLatLon(pos) : null); 331 } 332 333 /** 334 * Sets the speed. 335 * @param speed speed 336 */ 337 public void setSpeed(Double speed) { 338 this.speed = speed; 339 } 340 341 /** 342 * Sets the elevation. 343 * @param elevation elevation 344 */ 345 public void setElevation(Double elevation) { 346 this.elevation = elevation; 347 } 348 349 /** 350 * Sets associated file. 351 * @param file associated file 352 */ 353 public void setFile(File file) { 354 this.file = file; 355 } 356 357 /** 358 * Sets EXIF orientation. 359 * @param exifOrientation EXIF orientation 360 */ 361 public void setExifOrientation(Integer exifOrientation) { 362 this.exifOrientation = exifOrientation; 363 } 364 365 /** 366 * Sets EXIF time. 367 * @param exifTime EXIF time 368 * @since 17715 369 */ 370 public void setExifTime(Instant exifTime) { 371 this.exifTime = exifTime; 372 } 373 374 /** 375 * Sets the EXIF GPS time. 376 * @param exifGpsTime the EXIF GPS time 377 * @since 17715 378 */ 379 public void setExifGpsTime(Instant exifGpsTime) { 380 this.exifGpsTime = exifGpsTime; 381 } 382 383 /** 384 * Sets the GPS time. 385 * @param gpsTime the GPS time 386 * @since 17715 387 */ 388 public void setGpsTime(Instant gpsTime) { 389 this.gpsTime = gpsTime; 390 } 391 392 public void setExifCoor(LatLon exifCoor) { 393 this.exifCoor = exifCoor; 394 } 395 396 public void setExifImgDir(Double exifDir) { 397 this.exifImgDir = exifDir; 398 } 399 400 /** 401 * Sets the IPTC caption. 402 * @param iptcCaption the IPTC caption 403 * @since 15219 404 */ 405 public void setIptcCaption(String iptcCaption) { 406 this.iptcCaption = iptcCaption; 407 } 408 409 /** 410 * Sets the IPTC headline. 411 * @param iptcHeadline the IPTC headline 412 * @since 15219 413 */ 414 public void setIptcHeadline(String iptcHeadline) { 415 this.iptcHeadline = iptcHeadline; 416 } 417 418 /** 419 * Sets the IPTC keywords. 420 * @param iptcKeywords the IPTC keywords 421 * @since 15219 422 */ 423 public void setIptcKeywords(List<String> iptcKeywords) { 424 this.iptcKeywords = iptcKeywords; 425 } 426 427 /** 428 * Sets the IPTC object name. 429 * @param iptcObjectName the IPTC object name 430 * @since 15219 431 */ 432 public void setIptcObjectName(String iptcObjectName) { 433 this.iptcObjectName = iptcObjectName; 434 } 435 436 /** 437 * Returns the IPTC caption. 438 * @return the IPTC caption 439 * @since 15219 440 */ 441 public String getIptcCaption() { 442 return iptcCaption; 443 } 444 445 /** 446 * Returns the IPTC headline. 447 * @return the IPTC headline 448 * @since 15219 449 */ 450 public String getIptcHeadline() { 451 return iptcHeadline; 452 } 453 454 /** 455 * Returns the IPTC keywords. 456 * @return the IPTC keywords 457 * @since 15219 458 */ 459 public List<String> getIptcKeywords() { 460 return iptcKeywords; 461 } 462 463 /** 464 * Returns the IPTC object name. 465 * @return the IPTC object name 466 * @since 15219 467 */ 468 public String getIptcObjectName() { 469 return iptcObjectName; 470 } 471 472 @Override 473 public int compareTo(GpxImageEntry image) { 474 if (exifTime != null && image.exifTime != null) 475 return exifTime.compareTo(image.exifTime); 476 else if (exifTime == null && image.exifTime == null) 477 return 0; 478 else if (exifTime == null) 479 return -1; 480 else 481 return 1; 482 } 483 484 @Override 485 public int hashCode() { 486 return Objects.hash(height, width, isNewGpsData, 487 elevation, exifCoor, exifGpsTime, exifImgDir, exifOrientation, exifTime, 488 iptcCaption, iptcHeadline, iptcKeywords, iptcObjectName, 489 file, gpsTime, pos, speed, tmp, cameraProjection); 490 } 491 492 @Override 493 public boolean equals(Object obj) { 494 if (this == obj) 495 return true; 496 if (obj == null || getClass() != obj.getClass()) 497 return false; 498 GpxImageEntry other = (GpxImageEntry) obj; 499 return height == other.height 500 && width == other.width 501 && isNewGpsData == other.isNewGpsData 502 && Objects.equals(elevation, other.elevation) 503 && Objects.equals(exifCoor, other.exifCoor) 504 && Objects.equals(exifGpsTime, other.exifGpsTime) 505 && Objects.equals(exifImgDir, other.exifImgDir) 506 && Objects.equals(exifOrientation, other.exifOrientation) 507 && Objects.equals(exifTime, other.exifTime) 508 && Objects.equals(iptcCaption, other.iptcCaption) 509 && Objects.equals(iptcHeadline, other.iptcHeadline) 510 && Objects.equals(iptcKeywords, other.iptcKeywords) 511 && Objects.equals(iptcObjectName, other.iptcObjectName) 512 && Objects.equals(file, other.file) 513 && Objects.equals(gpsTime, other.gpsTime) 514 && Objects.equals(pos, other.pos) 515 && Objects.equals(speed, other.speed) 516 && Objects.equals(tmp, other.tmp) 517 && cameraProjection == other.cameraProjection; 518 } 519 520 /** 521 * Make a fresh copy and save it in the temporary variable. Use 522 * {@link #applyTmp()} or {@link #discardTmp()} if the temporary variable 523 * is not needed anymore. 524 * @return the fresh copy. 525 */ 526 public GpxImageEntry createTmp() { 527 tmp = new GpxImageEntry(this); 528 tmp.tmp = null; 529 return tmp; 530 } 531 532 /** 533 * Get temporary variable that is used for real time parameter 534 * adjustments. The temporary variable is created if it does not exist 535 * yet. Use {@link #applyTmp()} or {@link #discardTmp()} if the temporary 536 * variable is not needed anymore. 537 * @return temporary variable 538 */ 539 public GpxImageEntry getTmp() { 540 if (tmp == null) { 541 createTmp(); 542 } 543 return tmp; 544 } 545 546 /** 547 * Copy the values from the temporary variable to the main instance. The 548 * temporary variable is deleted. 549 * @see #discardTmp() 550 */ 551 public void applyTmp() { 552 if (tmp != null) { 553 pos = tmp.pos; 554 speed = tmp.speed; 555 elevation = tmp.elevation; 556 gpsTime = tmp.gpsTime; 557 exifImgDir = tmp.exifImgDir; 558 isNewGpsData = isNewGpsData || tmp.isNewGpsData; 559 tmp = null; 560 } 561 tmpUpdated(); 562 } 563 564 /** 565 * Delete the temporary variable. Temporary modifications are lost. 566 * @see #applyTmp() 567 */ 568 public void discardTmp() { 569 tmp = null; 570 tmpUpdated(); 571 } 572 573 /** 574 * If it has been tagged i.e. matched to a gpx track or retrieved lat/lon from exif 575 * @return {@code true} if it has been tagged 576 */ 577 public boolean isTagged() { 578 return pos != null; 579 } 580 581 /** 582 * String representation. (only partial info) 583 */ 584 @Override 585 public String toString() { 586 return file.getName()+": "+ 587 "pos = "+pos+" | "+ 588 "exifCoor = "+exifCoor+" | "+ 589 (tmp == null ? " tmp==null" : 590 " [tmp] pos = "+tmp.pos); 591 } 592 593 /** 594 * Indicates that the image has new GPS data. 595 * That flag is set by new GPS data providers. It is used e.g. by the photo_geotagging plugin 596 * to decide for which image file the EXIF GPS data needs to be (re-)written. 597 * @since 6392 598 */ 599 public void flagNewGpsData() { 600 isNewGpsData = true; 601 } 602 603 /** 604 * Indicate that the temporary copy has been updated. Mostly used to prevent UI issues. 605 * By default, this is a no-op. Override when needed in subclasses. 606 * @since 17579 607 */ 608 protected void tmpUpdated() { 609 // No-op by default 610 } 611 612 @Override 613 public BBox getBBox() { 614 // new BBox(LatLon) is null safe. 615 // Use `getPos` instead of `getExifCoor` since the image may be correlated against a GPX track 616 return new BBox(this.getPos()); 617 } 618 619 /** 620 * Remove the flag that indicates new GPS data. 621 * The flag is cleared by a new GPS data consumer. 622 */ 623 public void unflagNewGpsData() { 624 isNewGpsData = false; 625 } 626 627 /** 628 * Queries whether the GPS data changed. The flag value from the temporary 629 * copy is returned if that copy exists. 630 * @return {@code true} if GPS data changed, {@code false} otherwise 631 * @since 6392 632 */ 633 public boolean hasNewGpsData() { 634 if (tmp != null) 635 return tmp.isNewGpsData; 636 return isNewGpsData; 637 } 638 639 /** 640 * Extract GPS metadata from image EXIF. Has no effect if the image file is not set 641 * 642 * If successful, fills in the LatLon, speed, elevation, image direction, and other attributes 643 * @since 9270 644 */ 645 public void extractExif() { 646 647 Metadata metadata; 648 649 if (file == null) { 650 return; 651 } 652 653 String fn = file.getName(); 654 655 try { 656 // try to parse metadata according to extension 657 String ext = fn.substring(fn.lastIndexOf('.') + 1).toLowerCase(Locale.US); 658 switch (ext) { 659 case "jpg": 660 case "jpeg": 661 metadata = JpegMetadataReader.readMetadata(file); 662 break; 663 case "tif": 664 case "tiff": 665 metadata = TiffMetadataReader.readMetadata(file); 666 break; 667 case "png": 668 metadata = PngMetadataReader.readMetadata(file); 669 break; 670 default: 671 throw new NoMetadataReaderWarning(ext); 672 } 673 } catch (JpegProcessingException | TiffProcessingException | PngProcessingException | IOException 674 | NoMetadataReaderWarning topException) { 675 //try other formats (e.g. JPEG file with .png extension) 676 try { 677 metadata = JpegMetadataReader.readMetadata(file); 678 } catch (JpegProcessingException | IOException ex1) { 679 try { 680 metadata = TiffMetadataReader.readMetadata(file); 681 } catch (TiffProcessingException | IOException ex2) { 682 try { 683 metadata = PngMetadataReader.readMetadata(file); 684 } catch (PngProcessingException | IOException ex3) { 685 Logging.warn(topException); 686 Logging.info(tr("Can''t parse metadata for file \"{0}\". Using last modified date as timestamp.", fn)); 687 setExifTime(Instant.ofEpochMilli(file.lastModified())); 688 setExifCoor(null); 689 setPos(null); 690 return; 691 } 692 } 693 } 694 } 695 696 IptcDirectory dirIptc = metadata.getFirstDirectoryOfType(IptcDirectory.class); 697 if (dirIptc != null) { 698 ifNotNull(ExifReader.readCaption(dirIptc), this::setIptcCaption); 699 ifNotNull(ExifReader.readHeadline(dirIptc), this::setIptcHeadline); 700 ifNotNull(ExifReader.readKeywords(dirIptc), this::setIptcKeywords); 701 ifNotNull(ExifReader.readObjectName(dirIptc), this::setIptcObjectName); 702 } 703 704 for (XmpDirectory xmpDirectory : metadata.getDirectoriesOfType(XmpDirectory.class)) { 705 Map<String, String> properties = xmpDirectory.getXmpProperties(); 706 final String projectionType = "GPano:ProjectionType"; 707 if (properties.containsKey(projectionType)) { 708 Stream.of(Projections.values()).filter(p -> p.name().equalsIgnoreCase(properties.get(projectionType))) 709 .findFirst().ifPresent(projection -> this.cameraProjection = projection); 710 break; 711 } 712 } 713 714 // Changed to silently cope with no time info in exif. One case 715 // of person having time that couldn't be parsed, but valid GPS info 716 Instant time = null; 717 try { 718 time = ExifReader.readInstant(metadata); 719 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException ex) { 720 Logging.warn(ex); 721 } 722 723 if (time == null) { 724 Logging.info(tr("No EXIF time in file \"{0}\". Using last modified date as timestamp.", fn)); 725 time = Instant.ofEpochMilli(file.lastModified()); //use lastModified time if no EXIF time present 726 } 727 setExifTime(time); 728 729 final Directory dir = metadata.getFirstDirectoryOfType(JpegDirectory.class); 730 final Directory dirExif = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); 731 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class); 732 733 try { 734 if (dirExif != null && dirExif.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) { 735 setExifOrientation(dirExif.getInt(ExifIFD0Directory.TAG_ORIENTATION)); 736 } 737 } catch (MetadataException ex) { 738 Logging.debug(ex); 739 } 740 741 try { 742 if (dir != null && dir.containsTag(JpegDirectory.TAG_IMAGE_WIDTH) && dir.containsTag(JpegDirectory.TAG_IMAGE_HEIGHT)) { 743 // there are cases where these do not match width and height stored in dirExif 744 setWidth(dir.getInt(JpegDirectory.TAG_IMAGE_WIDTH)); 745 setHeight(dir.getInt(JpegDirectory.TAG_IMAGE_HEIGHT)); 746 } 747 } catch (MetadataException ex) { 748 Logging.debug(ex); 749 } 750 751 if (dirGps == null || dirGps.getTagCount() <= 1) { 752 setExifCoor(null); 753 setPos(null); 754 return; 755 } 756 757 ifNotNull(ExifReader.readSpeed(dirGps), this::setSpeed); 758 ifNotNull(ExifReader.readElevation(dirGps), this::setElevation); 759 760 try { 761 setExifCoor(ExifReader.readLatLon(dirGps)); 762 setPos(getExifCoor()); 763 } catch (MetadataException | IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271) 764 Logging.error("Error reading EXIF from file: " + ex); 765 setExifCoor(null); 766 setPos(null); 767 } 768 769 try { 770 ifNotNull(ExifReader.readDirection(dirGps), this::setExifImgDir); 771 } catch (IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271) 772 Logging.debug(ex); 773 } 774 775 ifNotNull(dirGps.getGpsDate(), d -> setExifGpsTime(d.toInstant())); 776 } 777 778 /** 779 * Reads the image represented by this entry in the given target dimension. 780 * @param target the desired dimension used for {@linkplain IIOParam#setSourceSubsampling subsampling} or {@code null} 781 * @return the read image, or {@code null} 782 * @throws IOException if any I/O error occurs 783 * @since 18246 784 */ 785 public BufferedImage read(Dimension target) throws IOException { 786 throw new UnsupportedOperationException("read not implemented for " + this.getClass().getSimpleName()); 787 } 788 789 private static class NoMetadataReaderWarning extends Exception { 790 NoMetadataReaderWarning(String ext) { 791 super("No metadata reader for format *." + ext); 792 } 793 } 794 795 private static <T> void ifNotNull(T value, Consumer<T> setter) { 796 if (value != null) { 797 setter.accept(value); 798 } 799 } 800 801 /** 802 * Get the projection type for this entry 803 * @return The projection type 804 * @since 18246 805 */ 806 public Projections getProjectionType() { 807 return this.cameraProjection; 808 } 809 810 /** 811 * Returns a {@link WayPoint} representation of this GPX image entry. 812 * @return a {@code WayPoint} representation of this GPX image entry (containing position, instant and elevation) 813 * @since 18065 814 */ 815 public WayPoint asWayPoint() { 816 CachedLatLon position = getPos(); 817 WayPoint wpt = null; 818 if (position != null) { 819 wpt = new WayPoint(position); 820 wpt.setInstant(exifTime); 821 Double ele = getElevation(); 822 if (ele != null) { 823 wpt.put(GpxConstants.PT_ELE, ele.toString()); 824 } 825 } 826 return wpt; 827 } 828}