Ticket #22337: 22337.patch

File 22337.patch, 59.6 KB (added by taylor.smock, 4 years ago)

Initial patch (no tests). Makes ImageMarker use the maintained image viewer, adds some interfaces to make it easier to read and apply exif data to orthogonal classes, move ImageViewerDialog creation and addition to map frame from GeoImageLayer to ImageViewerDialog#getInstance, add default read method in IImageEntry

  • src/org/openstreetmap/josm/data/gpx/GpxImageEntry.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/gpx/GpxImageEntry.java b/src/org/openstreetmap/josm/data/gpx/GpxImageEntry.java
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data.gpx;
    33
    4 import static org.openstreetmap.josm.tools.I18n.tr;
    5 
    64import java.awt.Dimension;
    75import java.awt.image.BufferedImage;
    86import java.io.File;
    97import java.io.IOException;
     8import java.io.InputStream;
     9import java.net.URI;
     10import java.nio.file.Files;
    1011import java.time.Instant;
    1112import java.util.Date;
    1213import java.util.List;
    13 import java.util.Locale;
    14 import java.util.Map;
    1514import java.util.Objects;
    16 import java.util.function.Consumer;
    17 import java.util.stream.Stream;
    1815
    1916import javax.imageio.IIOParam;
    2017
    2118import org.openstreetmap.josm.data.IQuadBucketType;
    2219import org.openstreetmap.josm.data.coor.CachedLatLon;
     20import org.openstreetmap.josm.data.coor.ILatLon;
    2321import org.openstreetmap.josm.data.coor.LatLon;
    2422import org.openstreetmap.josm.data.imagery.street_level.Projections;
    2523import org.openstreetmap.josm.data.osm.BBox;
    26 import org.openstreetmap.josm.tools.ExifReader;
    27 import org.openstreetmap.josm.tools.JosmRuntimeException;
    28 import org.openstreetmap.josm.tools.Logging;
    29 
    30 import com.drew.imaging.jpeg.JpegMetadataReader;
    31 import com.drew.imaging.jpeg.JpegProcessingException;
    32 import com.drew.imaging.png.PngMetadataReader;
    33 import com.drew.imaging.png.PngProcessingException;
    34 import com.drew.imaging.tiff.TiffMetadataReader;
    35 import com.drew.imaging.tiff.TiffProcessingException;
    36 import com.drew.metadata.Directory;
    37 import com.drew.metadata.Metadata;
    38 import com.drew.metadata.MetadataException;
    39 import com.drew.metadata.exif.ExifIFD0Directory;
    40 import com.drew.metadata.exif.GpsDirectory;
    41 import com.drew.metadata.iptc.IptcDirectory;
    42 import com.drew.metadata.jpeg.JpegDirectory;
    43 import com.drew.metadata.xmp.XmpDirectory;
     24import org.openstreetmap.josm.gui.layer.geoimage.ImageMetadata;
    4425
    4526/**
    4627 * Stores info about each image
    4728 * @since 14205 (extracted from gui.layer.geoimage.ImageEntry)
    4829 */
    49 public class GpxImageEntry implements Comparable<GpxImageEntry>, IQuadBucketType {
     30public class GpxImageEntry implements Comparable<GpxImageEntry>, IQuadBucketType, ImageMetadata {
    5031    private File file;
    5132    private Integer exifOrientation;
    5233    private LatLon exifCoor;
     
    127108        setFile(file);
    128109    }
    129110
     111    @Override
     112    public URI getImageURI() {
     113        return this.getFile().toURI();
     114    }
     115
    130116    /**
    131117     * Returns width of the image this GpxImageEntry represents.
    132118     * @return width of the image this GpxImageEntry represents
    133119     * @since 13220
    134120     */
     121    @Override
    135122    public int getWidth() {
    136123        return width;
    137124    }
     
    141128     * @return height of the image this GpxImageEntry represents
    142129     * @since 13220
    143130     */
     131    @Override
    144132    public int getHeight() {
    145133        return height;
    146134    }
     
    150138     * is returned if that copy exists.
    151139     * @return the position value
    152140     */
     141    @Override
    153142    public CachedLatLon getPos() {
    154143        if (tmp != null)
    155144            return tmp.pos;
     
    161150     * returned if that copy exists.
    162151     * @return the speed value
    163152     */
     153    @Override
    164154    public Double getSpeed() {
    165155        if (tmp != null)
    166156            return tmp.speed;
     
    172162     * copy is returned if that copy exists.
    173163     * @return the elevation value
    174164     */
     165    @Override
    175166    public Double getElevation() {
    176167        if (tmp != null)
    177168            return tmp.elevation;
     
    196187     * is returned if that copy exists.
    197188     * @return the GPS time value
    198189     */
     190    @Override
    199191    public Instant getGpsInstant() {
    200192        return tmp != null ? tmp.gpsTime : gpsTime;
    201193    }
     
    205197     * @return {@code true} if this entry has a GPS time
    206198     * @since 6450
    207199     */
     200    @Override
    208201    public boolean hasGpsTime() {
    209202        return (tmp != null && tmp.gpsTime != null) || gpsTime != null;
    210203    }
     
    229222     * Returns EXIF orientation
    230223     * @return EXIF orientation
    231224     */
     225    @Override
    232226    public Integer getExifOrientation() {
    233227        return exifOrientation != null ? exifOrientation : 1;
    234228    }
     
    238232     * @return EXIF time
    239233     * @since 17715
    240234     */
     235    @Override
    241236    public Instant getExifInstant() {
    242237        return exifTime;
    243238    }
     
    247242     * @return {@code true} if this entry has a EXIF time
    248243     * @since 6450
    249244     */
     245    @Override
    250246    public boolean hasExifTime() {
    251247        return exifTime != null;
    252248    }
     
    267263     * @return the EXIF GPS time
    268264     * @since 17715
    269265     */
     266    @Override
    270267    public Instant getExifGpsInstant() {
    271268        return exifGpsTime;
    272269    }
     
    276273     * @return {@code true} if this entry has a EXIF GPS time
    277274     * @since 6450
    278275     */
     276    @Override
    279277    public boolean hasExifGpsTime() {
    280278        return exifGpsTime != null;
    281279    }
     
    286284        return Date.from(date);
    287285    }
    288286
     287    @Override
    289288    public LatLon getExifCoor() {
    290289        return exifCoor;
    291290    }
    292291
     292    @Override
    293293    public Double getExifImgDir() {
    294294        if (tmp != null)
    295295            return tmp.exifImgDir;
    296296        return exifImgDir;
    297297    }
    298298
     299    @Override
     300    public Instant getLastModified() {
     301        return Instant.ofEpochMilli(this.getFile().lastModified());
     302    }
     303
    299304    /**
    300305     * Sets the width of this GpxImageEntry.
    301306     * @param width set the width of this GpxImageEntry
    302307     * @since 13220
    303308     */
     309    @Override
    304310    public void setWidth(int width) {
    305311        this.width = width;
    306312    }
     
    310316     * @param height set the height of this GpxImageEntry
    311317     * @since 13220
    312318     */
     319    @Override
    313320    public void setHeight(int height) {
    314321        this.height = height;
    315322    }
     
    330337        setPos(pos != null ? new CachedLatLon(pos) : null);
    331338    }
    332339
     340    @Override
     341    public void setPos(ILatLon pos) {
     342        if (pos instanceof CachedLatLon) {
     343            this.setPos((CachedLatLon) pos);
     344        } else if (pos instanceof LatLon) {
     345            this.setPos((LatLon) pos);
     346        } else if (pos != null) {
     347            this.setPos(new LatLon(pos));
     348        } else {
     349            this.setPos(null);
     350        }
     351    }
     352
    333353    /**
    334354     * Sets the speed.
    335355     * @param speed speed
    336356     */
     357    @Override
    337358    public void setSpeed(Double speed) {
    338359        this.speed = speed;
    339360    }
     
    342363     * Sets the elevation.
    343364     * @param elevation elevation
    344365     */
     366    @Override
    345367    public void setElevation(Double elevation) {
    346368        this.elevation = elevation;
    347369    }
     
    358380     * Sets EXIF orientation.
    359381     * @param exifOrientation EXIF orientation
    360382     */
     383    @Override
    361384    public void setExifOrientation(Integer exifOrientation) {
    362385        this.exifOrientation = exifOrientation;
    363386    }
     
    367390     * @param exifTime EXIF time
    368391     * @since 17715
    369392     */
     393    @Override
    370394    public void setExifTime(Instant exifTime) {
    371395        this.exifTime = exifTime;
    372396    }
     
    376400     * @param exifGpsTime the EXIF GPS time
    377401     * @since 17715
    378402     */
     403    @Override
    379404    public void setExifGpsTime(Instant exifGpsTime) {
    380405        this.exifGpsTime = exifGpsTime;
    381406    }
     
    385410     * @param gpsTime the GPS time
    386411     * @since 17715
    387412     */
     413    @Override
    388414    public void setGpsTime(Instant gpsTime) {
    389415        this.gpsTime = gpsTime;
    390416    }
     
    393419        this.exifCoor = exifCoor;
    394420    }
    395421
     422    @Override
     423    public void setExifCoor(ILatLon exifCoor) {
     424        if (exifCoor instanceof LatLon) {
     425            this.exifCoor = (LatLon) exifCoor;
     426        } else if (exifCoor != null) {
     427            this.exifCoor = new LatLon(exifCoor);
     428        } else {
     429            this.exifCoor = null;
     430        }
     431    }
     432
     433    @Override
    396434    public void setExifImgDir(Double exifDir) {
    397435        this.exifImgDir = exifDir;
    398436    }
     
    402440     * @param iptcCaption the IPTC caption
    403441     * @since 15219
    404442     */
     443    @Override
    405444    public void setIptcCaption(String iptcCaption) {
    406445        this.iptcCaption = iptcCaption;
    407446    }
     
    411450     * @param iptcHeadline the IPTC headline
    412451     * @since 15219
    413452     */
     453    @Override
    414454    public void setIptcHeadline(String iptcHeadline) {
    415455        this.iptcHeadline = iptcHeadline;
    416456    }
     
    420460     * @param iptcKeywords the IPTC keywords
    421461     * @since 15219
    422462     */
     463    @Override
    423464    public void setIptcKeywords(List<String> iptcKeywords) {
    424465        this.iptcKeywords = iptcKeywords;
    425466    }
     
    429470     * @param iptcObjectName the IPTC object name
    430471     * @since 15219
    431472     */
     473    @Override
    432474    public void setIptcObjectName(String iptcObjectName) {
    433475        this.iptcObjectName = iptcObjectName;
    434476    }
     
    438480     * @return the IPTC caption
    439481     * @since 15219
    440482     */
     483    @Override
    441484    public String getIptcCaption() {
    442485        return iptcCaption;
    443486    }
     
    447490     * @return the IPTC headline
    448491     * @since 15219
    449492     */
     493    @Override
    450494    public String getIptcHeadline() {
    451495        return iptcHeadline;
    452496    }
     
    456500     * @return the IPTC keywords
    457501     * @since 15219
    458502     */
     503    @Override
    459504    public List<String> getIptcKeywords() {
    460505        return iptcKeywords;
    461506    }
     
    465510     * @return the IPTC object name
    466511     * @since 15219
    467512     */
     513    @Override
    468514    public String getIptcObjectName() {
    469515        return iptcObjectName;
    470516    }
     
    642688     * If successful, fills in the LatLon, speed, elevation, image direction, and other attributes
    643689     * @since 9270
    644690     */
     691    @Override
    645692    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         }
     693        ImageMetadata.super.extractExif();
     694    }
    774695
    775         ifNotNull(dirGps.getGpsDate(), d -> setExifGpsTime(d.toInstant()));
     696    @Override
     697    public InputStream getInputStream() throws IOException {
     698        return Files.newInputStream(this.getFile().toPath());
    776699    }
    777700
    778701    /**
     
    786709        throw new UnsupportedOperationException("read not implemented for " + this.getClass().getSimpleName());
    787710    }
    788711
    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 
    801712    /**
    802713     * Get the projection type for this entry
    803714     * @return The projection type
    804715     * @since 18246
    805716     */
     717    @Override
    806718    public Projections getProjectionType() {
    807719        return this.cameraProjection;
    808720    }
    809721
     722    @Override
     723    public void setProjectionType(Projections newProjection) {
     724        this.cameraProjection = newProjection;
     725    }
     726
    810727    /**
    811728     * Returns a {@link WayPoint} representation of this GPX image entry.
    812729     * @return a {@code WayPoint} representation of this GPX image entry (containing position, instant and elevation)
  • src/org/openstreetmap/josm/data/imagery/street_level/IImageEntry.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/street_level/IImageEntry.java b/src/org/openstreetmap/josm/data/imagery/street_level/IImageEntry.java
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data.imagery.street_level;
    33
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
    46import java.awt.Dimension;
    57import java.awt.image.BufferedImage;
    68import java.io.File;
     
    1214import javax.imageio.IIOParam;
    1315
    1416import org.openstreetmap.josm.data.coor.ILatLon;
     17import org.openstreetmap.josm.gui.layer.geoimage.ImageMetadata;
     18import org.openstreetmap.josm.gui.layer.geoimage.ImageUtils;
    1519import org.openstreetmap.josm.gui.layer.geoimage.ImageViewerDialog;
     20import org.openstreetmap.josm.tools.ExifReader;
     21import org.openstreetmap.josm.tools.ImageProvider;
     22import org.openstreetmap.josm.tools.Logging;
    1623
    1724/**
    1825 * An interface for image entries that will be shown in {@link org.openstreetmap.josm.gui.layer.geoimage.ImageDisplay}
     
    137144     * @return the read image, or {@code null}
    138145     * @throws IOException if any I/O error occurs
    139146     */
    140     BufferedImage read(Dimension target) throws IOException;
     147    default BufferedImage read(Dimension target) throws IOException {
     148        URI imageUrl = getImageURI();
     149        Logging.info(tr("Loading {0}", imageUrl));
     150        BufferedImage image = ImageProvider.read(imageUrl.toURL(), false, false,
     151                r -> target == null ? r.getDefaultReadParam() : ImageUtils.withSubsampling(r, target));
     152        if (image == null) {
     153            Logging.warn("Unable to load {0}", imageUrl);
     154            return null;
     155        }
     156        if (this instanceof ImageMetadata) {
     157            ImageMetadata data = (ImageMetadata) this;
     158            Logging.debug("Loaded {0} with dimensions {1}x{2} memoryTaken={3}m exifOrientationSwitchedDimension={4}",
     159                    imageUrl, image.getWidth(), image.getHeight(), image.getWidth() * image.getHeight() * 4 / 1024 / 1024,
     160                    ExifReader.orientationSwitchesDimensions(data.getExifOrientation()));
     161            return ImageUtils.applyExifRotation(image, data.getExifOrientation());
     162        }
     163        return image;
     164    }
    141165
    142166    /**
    143167     * Sets the width of this ImageEntry.
  • src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java b/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java
    a b  
    704704            }
    705705        };
    706706        MainApplication.getLayerManager().addActiveLayerChangeListener(activeLayerChangeListener);
    707 
    708         MapFrame map = MainApplication.getMap();
    709         if (map.getToggleDialog(ImageViewerDialog.class) == null) {
    710             ImageViewerDialog.createInstance();
    711             map.addToggleDialog(ImageViewerDialog.getInstance());
    712         }
    713707    }
    714708
    715709    @Override
  • src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.gui.layer.geoimage;
    33
    4 import static org.openstreetmap.josm.tools.I18n.tr;
    5 
    64import java.awt.Dimension;
    7 import java.awt.Graphics2D;
    85import java.awt.Image;
    9 import java.awt.geom.AffineTransform;
    106import java.awt.image.BufferedImage;
    117import java.io.File;
    128import java.io.IOException;
    13 import java.io.UncheckedIOException;
    149import java.net.MalformedURLException;
    1510import java.net.URL;
    1611import java.util.Collections;
    1712import java.util.Objects;
     13
    1814import javax.imageio.IIOParam;
    19 import javax.imageio.ImageReadParam;
    20 import javax.imageio.ImageReader;
    2115
    2216import org.openstreetmap.josm.data.ImageData;
    2317import org.openstreetmap.josm.data.gpx.GpxImageEntry;
    2418import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
    25 import org.openstreetmap.josm.tools.ExifReader;
    26 import org.openstreetmap.josm.tools.ImageProvider;
    27 import org.openstreetmap.josm.tools.Logging;
    2819import org.openstreetmap.josm.tools.Utils;
    2920
    3021/**
     
    193184     */
    194185    @Override
    195186    public BufferedImage read(Dimension target) throws IOException {
    196         URL imageUrl = getImageUrl();
    197         Logging.info(tr("Loading {0}", imageUrl));
    198         BufferedImage image = ImageProvider.read(imageUrl, false, false,
    199                 r -> target == null ? r.getDefaultReadParam() : withSubsampling(r, target));
    200         if (image == null) {
    201             Logging.warn("Unable to load {0}", imageUrl);
    202             return null;
    203         }
    204         Logging.debug("Loaded {0} with dimensions {1}x{2} memoryTaken={3}m exifOrientationSwitchedDimension={4}",
    205                 imageUrl, image.getWidth(), image.getHeight(), image.getWidth() * image.getHeight() * 4 / 1024 / 1024,
    206                 ExifReader.orientationSwitchesDimensions(getExifOrientation()));
    207         return applyExifRotation(image);
     187        return IImageEntry.super.read(target);
    208188    }
    209189
    210190    protected URL getImageUrl() throws MalformedURLException {
    211191        return getFile().toURI().toURL();
    212192    }
    213 
    214     private ImageReadParam withSubsampling(ImageReader reader, Dimension target) {
    215         try {
    216             ImageReadParam param = reader.getDefaultReadParam();
    217             Dimension source = new Dimension(reader.getWidth(0), reader.getHeight(0));
    218             if (source.getWidth() > target.getWidth() || source.getHeight() > target.getHeight()) {
    219                 int subsampling = (int) Math.floor(Math.max(
    220                         source.getWidth() / target.getWidth(),
    221                         source.getHeight() / target.getHeight()));
    222                 param.setSourceSubsampling(subsampling, subsampling, 0, 0);
    223             }
    224             return param;
    225         } catch (IOException e) {
    226             throw new UncheckedIOException(e);
    227         }
    228     }
    229 
    230     private BufferedImage applyExifRotation(BufferedImage img) {
    231         Integer exifOrientation = getExifOrientation();
    232         if (!ExifReader.orientationNeedsCorrection(exifOrientation)) {
    233             return img;
    234         }
    235         boolean switchesDimensions = ExifReader.orientationSwitchesDimensions(exifOrientation);
    236         int width = switchesDimensions ? img.getHeight() : img.getWidth();
    237         int height = switchesDimensions ? img.getWidth() : img.getHeight();
    238         BufferedImage rotated = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
    239         AffineTransform transform = ExifReader.getRestoreOrientationTransform(exifOrientation, img.getWidth(), img.getHeight());
    240         Graphics2D g = rotated.createGraphics();
    241         g.drawImage(img, transform, null);
    242         g.dispose();
    243         return rotated;
    244     }
    245193}
  • new file src/org/openstreetmap/josm/gui/layer/geoimage/ImageMetadata.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/ImageMetadata.java b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageMetadata.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.geoimage;
     3
     4import java.io.BufferedInputStream;
     5import java.io.IOException;
     6import java.io.InputStream;
     7import java.io.UncheckedIOException;
     8import java.net.URI;
     9import java.time.Instant;
     10import java.util.List;
     11
     12import org.openstreetmap.josm.data.coor.ILatLon;
     13import org.openstreetmap.josm.data.imagery.street_level.Projections;
     14
     15/**
     16 * An interface for images with metadata
     17 * @author Taylor Smock
     18 * @since xxx
     19 */
     20public interface ImageMetadata {
     21    /**
     22     * Get the image location
     23     * @return The image location
     24     */
     25    URI getImageURI();
     26
     27    /**
     28     * Returns width of the image this ImageMetadata represents.
     29     * @return width of the image this ImageMetadata represents
     30     */
     31    int getWidth();
     32
     33    /**
     34     * Returns height of the image this ImageMetadata represents.
     35     * @return height of the image this ImageMetadata represents
     36     * @since 13220
     37     */
     38    int getHeight();
     39
     40    /**
     41     * Returns the position value. The position value from the temporary copy
     42     * is returned if that copy exists.
     43     * @return the position value
     44     */
     45    ILatLon getPos();
     46
     47    /**
     48     * Returns the speed value. The speed value from the temporary copy is
     49     * returned if that copy exists.
     50     * @return the speed value
     51     */
     52    Double getSpeed();
     53
     54    /**
     55     * Returns the elevation value. The elevation value from the temporary
     56     * copy is returned if that copy exists.
     57     * @return the elevation value
     58     */
     59    Double getElevation();
     60
     61    /**
     62     * Returns the GPS time value. The GPS time value from the temporary copy
     63     * is returned if that copy exists.
     64     * @return the GPS time value
     65     */
     66    Instant getGpsInstant();
     67
     68    /**
     69     * Convenient way to determine if this entry has a GPS time, without the cost of building a defensive copy.
     70     * @return {@code true} if this entry has a GPS time
     71     * @since 6450
     72     */
     73    boolean hasGpsTime();
     74
     75    /**
     76     * Returns a display name for this entry
     77     * @return a display name for this entry
     78     */
     79    String getDisplayName();
     80
     81    /**
     82     * Returns EXIF orientation
     83     * @return EXIF orientation
     84     */
     85    default Integer getExifOrientation() {
     86        return 1;
     87    }
     88
     89    /**
     90     * Returns EXIF time
     91     * @return EXIF time
     92     * @since 17715
     93     */
     94    Instant getExifInstant();
     95
     96    /**
     97     * Convenient way to determine if this entry has a EXIF time, without the cost of building a defensive copy.
     98     * @return {@code true} if this entry has a EXIF time
     99     * @since 6450
     100     */
     101    boolean hasExifTime();
     102
     103    /**
     104     * Returns the EXIF GPS time.
     105     * @return the EXIF GPS time
     106     * @since 17715
     107     */
     108    Instant getExifGpsInstant();
     109
     110    /**
     111     * Convenient way to determine if this entry has a EXIF GPS time, without the cost of building a defensive copy.
     112     * @return {@code true} if this entry has a EXIF GPS time
     113     * @since 6450
     114     */
     115    boolean hasExifGpsTime();
     116
     117    /**
     118     * Get the exif coordinates
     119     * @return The location of the image
     120     */
     121    ILatLon getExifCoor();
     122
     123    /**
     124     * Get the exif direction
     125     * @return The image direction
     126     */
     127    Double getExifImgDir();
     128
     129    /**
     130     * Get the last time the source was modified
     131     * @return The last time the source was modified
     132     */
     133    Instant getLastModified();
     134
     135    /**
     136     * Sets the width of this ImageMetadata.
     137     * @param width set the width of this ImageMetadata
     138     * @since 13220
     139     */
     140    void setWidth(int width);
     141
     142    /**
     143     * Sets the height of this ImageMetadata.
     144     * @param height set the height of this ImageMetadata
     145     * @since 13220
     146     */
     147    void setHeight(int height);
     148
     149    /**
     150     * Sets the position.
     151     * @param pos position (will be cached)
     152     */
     153    void setPos(ILatLon pos);
     154
     155    /**
     156     * Sets the speed.
     157     * @param speed speed
     158     */
     159    void setSpeed(Double speed);
     160
     161    /**
     162     * Sets the elevation.
     163     * @param elevation elevation
     164     */
     165    void setElevation(Double elevation);
     166
     167    /**
     168     * Sets EXIF orientation.
     169     * @param exifOrientation EXIF orientation
     170     */
     171    void setExifOrientation(Integer exifOrientation);
     172
     173    /**
     174     * Sets EXIF time.
     175     * @param exifTime EXIF time
     176     * @since 17715
     177     */
     178    void setExifTime(Instant exifTime);
     179
     180    /**
     181     * Sets the EXIF GPS time.
     182     * @param exifGpsTime the EXIF GPS time
     183     * @since 17715
     184     */
     185    void setExifGpsTime(Instant exifGpsTime);
     186
     187    /**
     188     * Sets the GPS time.
     189     * @param gpsTime the GPS time
     190     * @since 17715
     191     */
     192    void setGpsTime(Instant gpsTime);
     193
     194    /**
     195     * Set the exif coordinates
     196     * @param exifCoor The exif coordinates
     197     */
     198    void setExifCoor(ILatLon exifCoor);
     199
     200    /**
     201     * Set the exif direction
     202     * @param exifDir The direction
     203     */
     204    void setExifImgDir(Double exifDir);
     205
     206    /**
     207     * Sets the IPTC caption.
     208     * @param iptcCaption the IPTC caption
     209     * @since 15219
     210     */
     211    void setIptcCaption(String iptcCaption);
     212
     213    /**
     214     * Sets the IPTC headline.
     215     * @param iptcHeadline the IPTC headline
     216     * @since 15219
     217     */
     218    void setIptcHeadline(String iptcHeadline);
     219
     220    /**
     221     * Sets the IPTC keywords.
     222     * @param iptcKeywords the IPTC keywords
     223     * @since 15219
     224     */
     225    void setIptcKeywords(List<String> iptcKeywords);
     226
     227    /**
     228     * Sets the IPTC object name.
     229     * @param iptcObjectName the IPTC object name
     230     * @since 15219
     231     */
     232    void setIptcObjectName(String iptcObjectName);
     233
     234    /**
     235     * Returns the IPTC caption.
     236     * @return the IPTC caption
     237     * @since 15219
     238     */
     239    String getIptcCaption();
     240
     241    /**
     242     * Returns the IPTC headline.
     243     * @return the IPTC headline
     244     * @since 15219
     245     */
     246    String getIptcHeadline();
     247
     248    /**
     249     * Returns the IPTC keywords.
     250     * @return the IPTC keywords
     251     * @since 15219
     252     */
     253    List<String> getIptcKeywords();
     254
     255    /**
     256     * Returns the IPTC object name.
     257     * @return the IPTC object name
     258     * @since 15219
     259     */
     260    String getIptcObjectName();
     261
     262    /**
     263     * Extract GPS metadata from image EXIF. Has no effect if the image file is not set
     264     *
     265     * If successful, fills in the LatLon, speed, elevation, image direction, and other attributes
     266     * @since 9270
     267     */
     268    default void extractExif() {
     269        try (InputStream original = getInputStream();
     270             BufferedInputStream bufferedInputStream = new BufferedInputStream(original)) {
     271            ImageUtils.applyExif(this, bufferedInputStream);
     272        } catch (IOException e) {
     273            throw new UncheckedIOException(e);
     274        }
     275    }
     276
     277    InputStream getInputStream() throws IOException;
     278
     279    /**
     280     * Get the projection type for this entry
     281     * @return The projection type
     282     * @since 18246 (extracted in xxx)
     283     */
     284    Projections getProjectionType();
     285
     286    /**
     287     * Set the new projection type
     288     * @param newProjection The new type
     289     */
     290    void setProjectionType(Projections newProjection);
     291}
  • new file src/org/openstreetmap/josm/gui/layer/geoimage/ImageUtils.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/ImageUtils.java b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageUtils.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.geoimage;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.awt.Dimension;
     7import java.awt.Graphics2D;
     8import java.awt.geom.AffineTransform;
     9import java.awt.image.BufferedImage;
     10import java.io.IOException;
     11import java.io.InputStream;
     12import java.io.UncheckedIOException;
     13import java.net.URI;
     14import java.time.Instant;
     15import java.util.Locale;
     16import java.util.Map;
     17import java.util.function.Consumer;
     18import java.util.stream.Stream;
     19
     20import javax.imageio.ImageReadParam;
     21import javax.imageio.ImageReader;
     22
     23import com.drew.imaging.jpeg.JpegMetadataReader;
     24import com.drew.imaging.jpeg.JpegProcessingException;
     25import com.drew.imaging.png.PngMetadataReader;
     26import com.drew.imaging.png.PngProcessingException;
     27import com.drew.imaging.tiff.TiffMetadataReader;
     28import com.drew.imaging.tiff.TiffProcessingException;
     29import com.drew.metadata.Directory;
     30import com.drew.metadata.Metadata;
     31import com.drew.metadata.MetadataException;
     32import com.drew.metadata.exif.ExifIFD0Directory;
     33import com.drew.metadata.exif.GpsDirectory;
     34import com.drew.metadata.iptc.IptcDirectory;
     35import com.drew.metadata.jpeg.JpegDirectory;
     36import com.drew.metadata.xmp.XmpDirectory;
     37import org.openstreetmap.josm.data.imagery.street_level.Projections;
     38import org.openstreetmap.josm.tools.ExifReader;
     39import org.openstreetmap.josm.tools.JosmRuntimeException;
     40import org.openstreetmap.josm.tools.Logging;
     41
     42/**
     43 * Image utilities
     44 * @since xxx
     45 */
     46public final class ImageUtils {
     47    private ImageUtils() {
     48        // Hide constructor
     49    }
     50
     51    /**
     52     * Rotate an image, if needed
     53     * @param img The image to rotate
     54     * @param exifOrientation The exif orientation
     55     * @return The rotated image or the original
     56     */
     57    public static BufferedImage applyExifRotation(BufferedImage img, Integer exifOrientation) {
     58        if (exifOrientation == null || !ExifReader.orientationNeedsCorrection(exifOrientation)) {
     59            return img;
     60        }
     61        boolean switchesDimensions = ExifReader.orientationSwitchesDimensions(exifOrientation);
     62        int width = switchesDimensions ? img.getHeight() : img.getWidth();
     63        int height = switchesDimensions ? img.getWidth() : img.getHeight();
     64        BufferedImage rotated = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
     65        AffineTransform transform = ExifReader.getRestoreOrientationTransform(exifOrientation, img.getWidth(), img.getHeight());
     66        Graphics2D g = rotated.createGraphics();
     67        g.drawImage(img, transform, null);
     68        g.dispose();
     69        return rotated;
     70    }
     71
     72    /**
     73     * Common subsampling method
     74     * @param reader The image reader
     75     * @param target The target area
     76     * @return The sampling parameters
     77     */
     78    public static ImageReadParam withSubsampling(ImageReader reader, Dimension target) {
     79        try {
     80            ImageReadParam param = reader.getDefaultReadParam();
     81            Dimension source = new Dimension(reader.getWidth(0), reader.getHeight(0));
     82            if (source.getWidth() > target.getWidth() || source.getHeight() > target.getHeight()) {
     83                int subsampling = (int) Math.floor(Math.max(
     84                        source.getWidth() / target.getWidth(),
     85                        source.getHeight() / target.getHeight()));
     86                param.setSourceSubsampling(subsampling, subsampling, 0, 0);
     87            }
     88            return param;
     89        } catch (IOException e) {
     90            throw new UncheckedIOException(e);
     91        }
     92    }
     93
     94    /**
     95     * Apply exif information from an {@link InputStream}
     96     * @param image The image to apply information to
     97     * @param inputStream The input stream to read
     98     */
     99    public static void applyExif(ImageMetadata image, InputStream inputStream) {
     100        Metadata metadata;
     101
     102        if (image == null || inputStream == null) {
     103            return;
     104        }
     105
     106        metadata = getMetadata(image.getImageURI(), inputStream);
     107        if (metadata == null) {
     108            image.setExifTime(image.getLastModified());
     109            image.setExifCoor(null);
     110            image.setPos(null);
     111            return;
     112        }
     113        final String fn = image.getImageURI().toString();
     114
     115        IptcDirectory dirIptc = metadata.getFirstDirectoryOfType(IptcDirectory.class);
     116        if (dirIptc != null) {
     117            ifNotNull(ExifReader.readCaption(dirIptc), image::setIptcCaption);
     118            ifNotNull(ExifReader.readHeadline(dirIptc), image::setIptcHeadline);
     119            ifNotNull(ExifReader.readKeywords(dirIptc), image::setIptcKeywords);
     120            ifNotNull(ExifReader.readObjectName(dirIptc), image::setIptcObjectName);
     121        }
     122
     123        for (XmpDirectory xmpDirectory : metadata.getDirectoriesOfType(XmpDirectory.class)) {
     124            Map<String, String> properties = xmpDirectory.getXmpProperties();
     125            final String projectionType = "GPano:ProjectionType";
     126            if (properties.containsKey(projectionType)) {
     127                Stream.of(Projections.values()).filter(p -> p.name().equalsIgnoreCase(properties.get(projectionType)))
     128                        .findFirst().ifPresent(image::setProjectionType);
     129                break;
     130            }
     131        }
     132
     133        // Changed to silently cope with no time info in exif. One case
     134        // of person having time that couldn't be parsed, but valid GPS info
     135        Instant time = null;
     136        try {
     137            time = ExifReader.readInstant(metadata);
     138        } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException ex) {
     139            Logging.warn(ex);
     140        }
     141
     142        if (time == null) {
     143            Logging.info(tr("No EXIF time in file \"{0}\". Using last modified date as timestamp.", fn));
     144            time = image.getLastModified(); //use lastModified time if no EXIF time present
     145        }
     146        image.setExifTime(time);
     147
     148        final Directory dir = metadata.getFirstDirectoryOfType(JpegDirectory.class);
     149        final Directory dirExif = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
     150        final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
     151
     152        try {
     153            if (dirExif != null && dirExif.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
     154                image.setExifOrientation(dirExif.getInt(ExifIFD0Directory.TAG_ORIENTATION));
     155            }
     156        } catch (MetadataException ex) {
     157            Logging.debug(ex);
     158        }
     159
     160        try {
     161            if (dir != null && dir.containsTag(JpegDirectory.TAG_IMAGE_WIDTH) && dir.containsTag(JpegDirectory.TAG_IMAGE_HEIGHT)) {
     162                // there are cases where these do not match width and height stored in dirExif
     163                image.setWidth(dir.getInt(JpegDirectory.TAG_IMAGE_WIDTH));
     164                image.setHeight(dir.getInt(JpegDirectory.TAG_IMAGE_HEIGHT));
     165            }
     166        } catch (MetadataException ex) {
     167            Logging.debug(ex);
     168        }
     169
     170        if (dirGps == null || dirGps.getTagCount() <= 1) {
     171            image.setExifCoor(null);
     172            image.setPos(null);
     173            return;
     174        }
     175
     176        ifNotNull(ExifReader.readSpeed(dirGps), image::setSpeed);
     177        ifNotNull(ExifReader.readElevation(dirGps), image::setElevation);
     178
     179        try {
     180            image.setExifCoor(ExifReader.readLatLon(dirGps));
     181            image.setPos(image.getExifCoor());
     182        } catch (MetadataException | IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
     183            Logging.error("Error reading EXIF from file: " + ex);
     184            image.setExifCoor(null);
     185            image.setPos(null);
     186        }
     187
     188        try {
     189            ifNotNull(ExifReader.readDirection(dirGps), image::setExifImgDir);
     190        } catch (IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
     191            Logging.debug(ex);
     192        }
     193
     194        ifNotNull(dirGps.getGpsDate(), d -> image.setExifGpsTime(d.toInstant()));
     195    }
     196
     197    private static Metadata getMetadata(URI uri, InputStream inputStream) {
     198        inputStream.mark(32);
     199        final Exception topException;
     200        final String fn = uri.toString();
     201        try {
     202            // try to parse metadata according to extension
     203            String ext = fn.substring(fn.lastIndexOf('.') + 1).toLowerCase(Locale.US);
     204            switch (ext) {
     205                case "jpg":
     206                case "jpeg":
     207                    return JpegMetadataReader.readMetadata(inputStream);
     208                case "tif":
     209                case "tiff":
     210                    return TiffMetadataReader.readMetadata(inputStream);
     211                case "png":
     212                    return PngMetadataReader.readMetadata(inputStream);
     213                default:
     214                    throw new NoMetadataReaderWarning(ext);
     215            }
     216        } catch (JpegProcessingException | TiffProcessingException | PngProcessingException | IOException
     217                 | NoMetadataReaderWarning exception) {
     218            //try other formats (e.g. JPEG file with .png extension)
     219            topException = exception;
     220        }
     221        try {
     222            return JpegMetadataReader.readMetadata(inputStream);
     223        } catch (JpegProcessingException | IOException ex1) {
     224            Logging.trace(ex1);
     225        }
     226        try {
     227            return TiffMetadataReader.readMetadata(inputStream);
     228        } catch (TiffProcessingException | IOException ex2) {
     229            Logging.trace(ex2);
     230        }
     231
     232        try {
     233            return PngMetadataReader.readMetadata(inputStream);
     234        } catch (PngProcessingException | IOException ex3) {
     235            Logging.trace(ex3);
     236        }
     237        Logging.warn(topException);
     238        Logging.info(tr("Can''t parse metadata for file \"{0}\". Using last modified date as timestamp.", fn));
     239        return null;
     240    }
     241
     242    private static class NoMetadataReaderWarning extends Exception {
     243        NoMetadataReaderWarning(String ext) {
     244            super("No metadata reader for format *." + ext);
     245        }
     246    }
     247
     248    private static <T> void ifNotNull(T value, Consumer<T> setter) {
     249        if (value != null) {
     250            setter.accept(value);
     251        }
     252    }
     253}
  • src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java
    a b  
    4343import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
    4444import org.openstreetmap.josm.gui.ExtendedDialog;
    4545import org.openstreetmap.josm.gui.MainApplication;
     46import org.openstreetmap.josm.gui.MapFrame;
    4647import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
    4748import org.openstreetmap.josm.gui.dialogs.DialogsPanel;
    4849import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
     
    105106     * @return the unique instance
    106107     */
    107108    public static ImageViewerDialog getInstance() {
    108         if (dialog == null)
    109             throw new AssertionError("a new instance needs to be created first");
     109        MapFrame map = MainApplication.getMap();
     110        synchronized (ImageViewerDialog.class) {
     111            if (dialog == null)
     112                createInstance();
     113            if (map != null && map.getToggleDialog(ImageViewerDialog.class) == null) {
     114                map.addToggleDialog(dialog);
     115            }
     116        }
    110117        return dialog;
    111118    }
    112119
  • new file src/org/openstreetmap/josm/gui/layer/geoimage/RemoteEntry.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/RemoteEntry.java b/src/org/openstreetmap/josm/gui/layer/geoimage/RemoteEntry.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.geoimage;
     3
     4import java.io.File;
     5import java.io.IOException;
     6import java.io.InputStream;
     7import java.io.UncheckedIOException;
     8import java.net.MalformedURLException;
     9import java.net.URI;
     10import java.nio.file.Files;
     11import java.nio.file.Paths;
     12import java.time.Instant;
     13import java.util.List;
     14import java.util.Objects;
     15import java.util.function.Supplier;
     16
     17import org.openstreetmap.josm.data.coor.ILatLon;
     18import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
     19import org.openstreetmap.josm.data.imagery.street_level.Projections;
     20import org.openstreetmap.josm.tools.HttpClient;
     21import org.openstreetmap.josm.tools.JosmRuntimeException;
     22
     23/**
     24 * A remote image entry
     25 * @since xxx
     26 */
     27public class RemoteEntry implements IImageEntry<RemoteEntry>, ImageMetadata {
     28    private final URI uri;
     29    private final Supplier<RemoteEntry> firstImage;
     30    private final Supplier<RemoteEntry> nextImage;
     31    private final Supplier<RemoteEntry> previousImage;
     32    private final Supplier<RemoteEntry> lastImage;
     33    private int width;
     34    private int height;
     35    private ILatLon pos;
     36    private Integer exifOrientation;
     37    private Double elevation;
     38    private Double speed;
     39    private Double exifImgDir;
     40    private ILatLon exifCoor;
     41    private Instant exifTime;
     42    private Instant exifGpsTime;
     43    private Instant gpsTime;
     44    private String iptcObjectName;
     45    private List<String> iptcKeywords;
     46    private String iptcHeadline;
     47    private String iptcCaption;
     48    private Projections projection;
     49    private String title;
     50
     51    /**
     52     * Create a new remote entry
     53     * @param uri The URI to use
     54     * @param firstImage first image supplier
     55     * @param nextImage next image supplier
     56     * @param lastImage last image supplier
     57     * @param previousImage previous image supplier
     58     */
     59    public RemoteEntry(URI uri, Supplier<RemoteEntry> firstImage, Supplier<RemoteEntry> previousImage,
     60                       Supplier<RemoteEntry> nextImage, Supplier<RemoteEntry> lastImage) {
     61        Objects.requireNonNull(uri);
     62        Objects.requireNonNull(firstImage);
     63        Objects.requireNonNull(previousImage);
     64        Objects.requireNonNull(nextImage);
     65        Objects.requireNonNull(lastImage);
     66        this.uri = uri;
     67        this.firstImage = firstImage;
     68        this.previousImage = previousImage;
     69        this.nextImage = nextImage;
     70        this.lastImage = lastImage;
     71    }
     72
     73    @Override
     74    public RemoteEntry getNextImage() {
     75        return this.nextImage.get();
     76    }
     77
     78    @Override
     79    public RemoteEntry getPreviousImage() {
     80        return this.previousImage.get();
     81    }
     82
     83    @Override
     84    public RemoteEntry getFirstImage() {
     85        return this.firstImage.get();
     86    }
     87
     88    @Override
     89    public RemoteEntry getLastImage() {
     90        return this.lastImage.get();
     91    }
     92
     93    @Override
     94    public String getDisplayName() {
     95        return this.title == null ? this.getImageURI().toString() : this.title;
     96    }
     97
     98    @Override
     99    public void setWidth(int width) {
     100        this.width = width;
     101    }
     102
     103    @Override
     104    public void setHeight(int height) {
     105        this.height = height;
     106    }
     107
     108    @Override
     109    public void setPos(ILatLon pos) {
     110        this.pos = pos;
     111    }
     112
     113    @Override
     114    public void setSpeed(Double speed) {
     115        this.speed = speed;
     116    }
     117
     118    @Override
     119    public void setElevation(Double elevation) {
     120        this.elevation = elevation;
     121    }
     122
     123    @Override
     124    public void setExifOrientation(Integer exifOrientation) {
     125        this.exifOrientation = exifOrientation;
     126    }
     127
     128    @Override
     129    public void setExifTime(Instant exifTime) {
     130        this.exifTime = exifTime;
     131    }
     132
     133    @Override
     134    public void setExifGpsTime(Instant exifGpsTime) {
     135        this.exifGpsTime = exifGpsTime;
     136    }
     137
     138    @Override
     139    public void setGpsTime(Instant gpsTime) {
     140        this.gpsTime = gpsTime;
     141    }
     142
     143    @Override
     144    public void setExifCoor(ILatLon exifCoor) {
     145        this.exifCoor = exifCoor;
     146    }
     147
     148    @Override
     149    public void setExifImgDir(Double exifDir) {
     150        this.exifImgDir = exifDir;
     151    }
     152
     153    @Override
     154    public void setIptcCaption(String iptcCaption) {
     155        this.iptcCaption = iptcCaption;
     156    }
     157
     158    @Override
     159    public void setIptcHeadline(String iptcHeadline) {
     160        this.iptcHeadline = iptcHeadline;
     161    }
     162
     163    @Override
     164    public void setIptcKeywords(List<String> iptcKeywords) {
     165        this.iptcKeywords = iptcKeywords;
     166    }
     167
     168    @Override
     169    public void setIptcObjectName(String iptcObjectName) {
     170        this.iptcObjectName = iptcObjectName;
     171    }
     172
     173    @Override
     174    public Integer getExifOrientation() {
     175        return this.exifOrientation != null ? this.exifOrientation : 1;
     176    }
     177
     178    @Override
     179    public File getFile() {
     180        return null;
     181    }
     182
     183    @Override
     184    public URI getImageURI() {
     185        return this.uri;
     186    }
     187
     188    @Override
     189    public int getWidth() {
     190        return this.width;
     191    }
     192
     193    @Override
     194    public int getHeight() {
     195        return this.height;
     196    }
     197
     198    @Override
     199    public ILatLon getPos() {
     200        return this.pos;
     201    }
     202
     203    @Override
     204    public Double getSpeed() {
     205        return this.speed;
     206    }
     207
     208    @Override
     209    public Double getElevation() {
     210        return this.elevation;
     211    }
     212
     213    @Override
     214    public Double getExifImgDir() {
     215        return this.exifImgDir;
     216    }
     217
     218    @Override
     219    public Instant getLastModified() {
     220        if (this.getImageURI().getScheme().contains("file:")) {
     221            try {
     222                return Files.getLastModifiedTime(Paths.get(this.getImageURI())).toInstant();
     223            } catch (IOException e) {
     224                throw new UncheckedIOException(e);
     225            }
     226        }
     227        try {
     228            return Instant.ofEpochMilli(HttpClient.create(this.getImageURI().toURL(), "HEAD").getResponse().getLastModified());
     229        } catch (MalformedURLException e) {
     230            throw new JosmRuntimeException(e);
     231        }
     232    }
     233
     234    @Override
     235    public boolean hasExifTime() {
     236        return this.exifTime != null;
     237    }
     238
     239    @Override
     240    public Instant getExifGpsInstant() {
     241        return this.exifGpsTime;
     242    }
     243
     244    @Override
     245    public boolean hasExifGpsTime() {
     246        return this.exifGpsTime != null;
     247    }
     248
     249    @Override
     250    public ILatLon getExifCoor() {
     251        return this.exifCoor;
     252    }
     253
     254    @Override
     255    public Instant getExifInstant() {
     256        return this.exifTime;
     257    }
     258
     259    @Override
     260    public boolean hasGpsTime() {
     261        return this.gpsTime != null;
     262    }
     263
     264    @Override
     265    public Instant getGpsInstant() {
     266        return this.gpsTime;
     267    }
     268
     269    @Override
     270    public String getIptcCaption() {
     271        return this.iptcCaption;
     272    }
     273
     274    @Override
     275    public String getIptcHeadline() {
     276        return this.iptcHeadline;
     277    }
     278
     279    @Override
     280    public List<String> getIptcKeywords() {
     281        return this.iptcKeywords;
     282    }
     283
     284    @Override
     285    public String getIptcObjectName() {
     286        return this.iptcObjectName;
     287    }
     288
     289    @Override
     290    public Projections getProjectionType() {
     291        return this.projection;
     292    }
     293
     294    @Override
     295    public InputStream getInputStream() throws IOException {
     296        URI u = getImageURI();
     297        if (u.getScheme().contains("file")) {
     298            return Files.newInputStream(Paths.get(u));
     299        }
     300        return HttpClient.create(u.toURL()).connect().getContent();
     301    }
     302
     303    @Override
     304    public void setProjectionType(Projections newProjection) {
     305        this.projection = newProjection;
     306    }
     307
     308    /**
     309     * Set the display name for this entry
     310     * @param text The display name
     311     */
     312    public void setDisplayName(String text) {
     313        this.title = text;
     314    }
     315}
  • src/org/openstreetmap/josm/gui/layer/markerlayer/ImageMarker.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/markerlayer/ImageMarker.java b/src/org/openstreetmap/josm/gui/layer/markerlayer/ImageMarker.java
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.gui.layer.markerlayer;
    33
    4 import java.awt.BorderLayout;
    5 import java.awt.Cursor;
    6 import java.awt.GraphicsEnvironment;
    7 import java.awt.Image;
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
    86import java.awt.event.ActionEvent;
     7import java.net.URISyntaxException;
    98import java.net.URL;
     9import java.time.Instant;
    1010import java.util.Collections;
     11import java.util.function.Supplier;
    1112
    12 import javax.swing.Icon;
    13 import javax.swing.ImageIcon;
    14 import javax.swing.JDialog;
    15 import javax.swing.JLabel;
    1613import javax.swing.JOptionPane;
    17 import javax.swing.JPanel;
    18 import javax.swing.JScrollPane;
    19 import javax.swing.JToggleButton;
    20 import javax.swing.JViewport;
    2114
    2215import org.openstreetmap.josm.data.coor.LatLon;
    2316import org.openstreetmap.josm.data.gpx.GpxConstants;
    2417import org.openstreetmap.josm.data.gpx.GpxLink;
    2518import org.openstreetmap.josm.data.gpx.WayPoint;
    26 import org.openstreetmap.josm.gui.MainApplication;
    27 import org.openstreetmap.josm.tools.ImageProvider;
     19import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
     20import org.openstreetmap.josm.gui.Notification;
     21import org.openstreetmap.josm.gui.layer.geoimage.ImageViewerDialog;
     22import org.openstreetmap.josm.gui.layer.geoimage.RemoteEntry;
     23import org.openstreetmap.josm.tools.Logging;
     24import org.openstreetmap.josm.tools.Utils;
    2825
    2926/**
    3027 * Marker representing an image. Uses a special icon, and when clicked,
     
    4239        this.imageUrl = imageUrl;
    4340    }
    4441
    45     @Override public void actionPerformed(ActionEvent ev) {
    46         final JPanel p = new JPanel(new BorderLayout());
    47         final JScrollPane scroll = new JScrollPane(new JLabel(loadScaledImage(imageUrl, 580)));
    48         final JViewport vp = scroll.getViewport();
    49         p.add(scroll, BorderLayout.CENTER);
     42    @Override
     43    public void actionPerformed(ActionEvent ev) {
     44        ImageViewerDialog.getInstance().displayImage(getRemoteEntry());
     45    }
    5046
    51         final JToggleButton scale = new JToggleButton(ImageProvider.get("misc", "rectangle"));
     47    private RemoteEntry getRemoteEntry() {
     48        try {
     49            final RemoteEntry remoteEntry = new RemoteEntry(imageUrl.toURI(), getFirstImage(), getPreviousImage(),
     50                    getNextImage(), getLastImage());
     51            // First, extract EXIF data
     52            remoteEntry.extractExif();
     53            // Then, apply information from this point. This may overwrite details from
     54            // the exif, but that will (hopefully) be OK.
     55            if (Double.isFinite(this.time)) {
     56                remoteEntry.setGpsTime(Instant.ofEpochMilli((long) (this.time * 1000)));
     57            }
     58            if (this.isLatLonKnown()) {
     59                remoteEntry.setPos(this);
     60            }
     61            if (!Utils.isBlank(this.getText())) {
     62                remoteEntry.setDisplayName(this.getText());
     63            }
     64            return remoteEntry;
     65        } catch (URISyntaxException e) {
     66            Logging.trace(e);
     67            new Notification(tr("Malformed URI: ", this.imageUrl.toExternalForm())).setIcon(JOptionPane.WARNING_MESSAGE).show();
     68        }
     69        return null;
     70    }
    5271
    53         JPanel p2 = new JPanel();
    54         p2.add(scale);
    55         p.add(p2, BorderLayout.SOUTH);
    56         scale.addActionListener(ev1 -> {
    57             p.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
    58             if (scale.getModel().isSelected()) {
    59                 ((JLabel) vp.getView()).setIcon(loadScaledImage(imageUrl, Math.max(vp.getWidth(), vp.getHeight())));
    60             } else {
    61                 ((JLabel) vp.getView()).setIcon(new ImageIcon(imageUrl));
     72    private Supplier<RemoteEntry> getFirstImage() {
     73        for (Marker marker : this.parentLayer.data) {
     74            if (marker instanceof ImageMarker) {
     75                if (marker == this) {
     76                    break;
     77                }
     78                ImageMarker imageMarker = (ImageMarker) marker;
     79                return imageMarker::getRemoteEntry;
    6280            }
    63             p.setCursor(Cursor.getDefaultCursor());
    64         });
    65         scale.setSelected(true);
    66         JOptionPane pane = new JOptionPane(p, JOptionPane.PLAIN_MESSAGE);
    67         if (!GraphicsEnvironment.isHeadless()) {
    68             JDialog dlg = pane.createDialog(MainApplication.getMainFrame(), imageUrl.toString());
    69             dlg.setModal(false);
    70             dlg.toFront();
    71             dlg.setVisible(true);
    7281        }
     82        return () -> null;
    7383    }
    7484
    75     private static Icon loadScaledImage(URL u, int maxSize) {
    76         Image img = new ImageIcon(u).getImage();
    77         int w = img.getWidth(null);
    78         int h = img.getHeight(null);
    79         if (w > h) {
    80             h = (int) Math.round(maxSize*((double) h/w));
    81             w = maxSize;
    82         } else {
    83             w = (int) Math.round(maxSize*((double) w/h));
    84             h = maxSize;
     85    private Supplier<RemoteEntry> getPreviousImage() {
     86        int index = this.parentLayer.data.indexOf(this);
     87        for (int i = index - 1; i >= 0; i--) {
     88            Marker marker = this.parentLayer.data.get(i);
     89            if (marker instanceof ImageMarker) {
     90                ImageMarker imageMarker = (ImageMarker) marker;
     91                return imageMarker::getRemoteEntry;
     92            }
    8593        }
    86         return new ImageIcon(img.getScaledInstance(w, h, Image.SCALE_SMOOTH));
     94        return () -> null;
    8795    }
     96    private Supplier<RemoteEntry> getNextImage() {
     97        int index = this.parentLayer.data.indexOf(this);
     98        for (int i = index + 1; i < this.parentLayer.data.size(); i++) {
     99            Marker marker = this.parentLayer.data.get(i);
     100            if (marker instanceof ImageMarker) {
     101                ImageMarker imageMarker = (ImageMarker) marker;
     102                return imageMarker::getRemoteEntry;
     103            }
     104        }
     105        return () -> null;
     106    }
     107    private Supplier<RemoteEntry> getLastImage() {
     108        int index = this.parentLayer.data.indexOf(this);
     109        for (int i = this.parentLayer.data.size() - 1; i >= index; i--) {
     110            Marker marker = this.parentLayer.data.get(i);
     111            if (marker instanceof ImageMarker) {
     112                if (marker == this) {
     113                    break;
     114                }
     115                ImageMarker imageMarker = (ImageMarker) marker;
     116                return imageMarker::getRemoteEntry;
     117            }
     118        }
     119        return () -> null;
     120    }
    88121
    89122    @Override
    90123    public WayPoint convertToWayPoint() {