Index: 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/src/org/openstreetmap/josm/data/gpx/GpxImageEntry.java	(revision 18543)
+++ b/src/org/openstreetmap/josm/data/gpx/GpxImageEntry.java	(date 1661985266563)
@@ -1,52 +1,33 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.data.gpx;
 
-import static org.openstreetmap.josm.tools.I18n.tr;
-
 import java.awt.Dimension;
 import java.awt.image.BufferedImage;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.file.Files;
 import java.time.Instant;
 import java.util.Date;
 import java.util.List;
-import java.util.Locale;
-import java.util.Map;
 import java.util.Objects;
-import java.util.function.Consumer;
-import java.util.stream.Stream;
 
 import javax.imageio.IIOParam;
 
 import org.openstreetmap.josm.data.IQuadBucketType;
 import org.openstreetmap.josm.data.coor.CachedLatLon;
+import org.openstreetmap.josm.data.coor.ILatLon;
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.imagery.street_level.Projections;
 import org.openstreetmap.josm.data.osm.BBox;
-import org.openstreetmap.josm.tools.ExifReader;
-import org.openstreetmap.josm.tools.JosmRuntimeException;
-import org.openstreetmap.josm.tools.Logging;
-
-import com.drew.imaging.jpeg.JpegMetadataReader;
-import com.drew.imaging.jpeg.JpegProcessingException;
-import com.drew.imaging.png.PngMetadataReader;
-import com.drew.imaging.png.PngProcessingException;
-import com.drew.imaging.tiff.TiffMetadataReader;
-import com.drew.imaging.tiff.TiffProcessingException;
-import com.drew.metadata.Directory;
-import com.drew.metadata.Metadata;
-import com.drew.metadata.MetadataException;
-import com.drew.metadata.exif.ExifIFD0Directory;
-import com.drew.metadata.exif.GpsDirectory;
-import com.drew.metadata.iptc.IptcDirectory;
-import com.drew.metadata.jpeg.JpegDirectory;
-import com.drew.metadata.xmp.XmpDirectory;
+import org.openstreetmap.josm.gui.layer.geoimage.ImageMetadata;
 
 /**
  * Stores info about each image
  * @since 14205 (extracted from gui.layer.geoimage.ImageEntry)
  */
-public class GpxImageEntry implements Comparable<GpxImageEntry>, IQuadBucketType {
+public class GpxImageEntry implements Comparable<GpxImageEntry>, IQuadBucketType, ImageMetadata {
     private File file;
     private Integer exifOrientation;
     private LatLon exifCoor;
@@ -127,11 +108,17 @@
         setFile(file);
     }
 
+    @Override
+    public URI getImageURI() {
+        return this.getFile().toURI();
+    }
+
     /**
      * Returns width of the image this GpxImageEntry represents.
      * @return width of the image this GpxImageEntry represents
      * @since 13220
      */
+    @Override
     public int getWidth() {
         return width;
     }
@@ -141,6 +128,7 @@
      * @return height of the image this GpxImageEntry represents
      * @since 13220
      */
+    @Override
     public int getHeight() {
         return height;
     }
@@ -150,6 +138,7 @@
      * is returned if that copy exists.
      * @return the position value
      */
+    @Override
     public CachedLatLon getPos() {
         if (tmp != null)
             return tmp.pos;
@@ -161,6 +150,7 @@
      * returned if that copy exists.
      * @return the speed value
      */
+    @Override
     public Double getSpeed() {
         if (tmp != null)
             return tmp.speed;
@@ -172,6 +162,7 @@
      * copy is returned if that copy exists.
      * @return the elevation value
      */
+    @Override
     public Double getElevation() {
         if (tmp != null)
             return tmp.elevation;
@@ -196,6 +187,7 @@
      * is returned if that copy exists.
      * @return the GPS time value
      */
+    @Override
     public Instant getGpsInstant() {
         return tmp != null ? tmp.gpsTime : gpsTime;
     }
@@ -205,6 +197,7 @@
      * @return {@code true} if this entry has a GPS time
      * @since 6450
      */
+    @Override
     public boolean hasGpsTime() {
         return (tmp != null && tmp.gpsTime != null) || gpsTime != null;
     }
@@ -229,6 +222,7 @@
      * Returns EXIF orientation
      * @return EXIF orientation
      */
+    @Override
     public Integer getExifOrientation() {
         return exifOrientation != null ? exifOrientation : 1;
     }
@@ -238,6 +232,7 @@
      * @return EXIF time
      * @since 17715
      */
+    @Override
     public Instant getExifInstant() {
         return exifTime;
     }
@@ -247,6 +242,7 @@
      * @return {@code true} if this entry has a EXIF time
      * @since 6450
      */
+    @Override
     public boolean hasExifTime() {
         return exifTime != null;
     }
@@ -267,6 +263,7 @@
      * @return the EXIF GPS time
      * @since 17715
      */
+    @Override
     public Instant getExifGpsInstant() {
         return exifGpsTime;
     }
@@ -276,6 +273,7 @@
      * @return {@code true} if this entry has a EXIF GPS time
      * @since 6450
      */
+    @Override
     public boolean hasExifGpsTime() {
         return exifGpsTime != null;
     }
@@ -286,21 +284,29 @@
         return Date.from(date);
     }
 
+    @Override
     public LatLon getExifCoor() {
         return exifCoor;
     }
 
+    @Override
     public Double getExifImgDir() {
         if (tmp != null)
             return tmp.exifImgDir;
         return exifImgDir;
     }
 
+    @Override
+    public Instant getLastModified() {
+        return Instant.ofEpochMilli(this.getFile().lastModified());
+    }
+
     /**
      * Sets the width of this GpxImageEntry.
      * @param width set the width of this GpxImageEntry
      * @since 13220
      */
+    @Override
     public void setWidth(int width) {
         this.width = width;
     }
@@ -310,6 +316,7 @@
      * @param height set the height of this GpxImageEntry
      * @since 13220
      */
+    @Override
     public void setHeight(int height) {
         this.height = height;
     }
@@ -330,10 +337,24 @@
         setPos(pos != null ? new CachedLatLon(pos) : null);
     }
 
+    @Override
+    public void setPos(ILatLon pos) {
+        if (pos instanceof CachedLatLon) {
+            this.setPos((CachedLatLon) pos);
+        } else if (pos instanceof LatLon) {
+            this.setPos((LatLon) pos);
+        } else if (pos != null) {
+            this.setPos(new LatLon(pos));
+        } else {
+            this.setPos(null);
+        }
+    }
+
     /**
      * Sets the speed.
      * @param speed speed
      */
+    @Override
     public void setSpeed(Double speed) {
         this.speed = speed;
     }
@@ -342,6 +363,7 @@
      * Sets the elevation.
      * @param elevation elevation
      */
+    @Override
     public void setElevation(Double elevation) {
         this.elevation = elevation;
     }
@@ -358,6 +380,7 @@
      * Sets EXIF orientation.
      * @param exifOrientation EXIF orientation
      */
+    @Override
     public void setExifOrientation(Integer exifOrientation) {
         this.exifOrientation = exifOrientation;
     }
@@ -367,6 +390,7 @@
      * @param exifTime EXIF time
      * @since 17715
      */
+    @Override
     public void setExifTime(Instant exifTime) {
         this.exifTime = exifTime;
     }
@@ -376,6 +400,7 @@
      * @param exifGpsTime the EXIF GPS time
      * @since 17715
      */
+    @Override
     public void setExifGpsTime(Instant exifGpsTime) {
         this.exifGpsTime = exifGpsTime;
     }
@@ -385,6 +410,7 @@
      * @param gpsTime the GPS time
      * @since 17715
      */
+    @Override
     public void setGpsTime(Instant gpsTime) {
         this.gpsTime = gpsTime;
     }
@@ -393,6 +419,18 @@
         this.exifCoor = exifCoor;
     }
 
+    @Override
+    public void setExifCoor(ILatLon exifCoor) {
+        if (exifCoor instanceof LatLon) {
+            this.exifCoor = (LatLon) exifCoor;
+        } else if (exifCoor != null) {
+            this.exifCoor = new LatLon(exifCoor);
+        } else {
+            this.exifCoor = null;
+        }
+    }
+
+    @Override
     public void setExifImgDir(Double exifDir) {
         this.exifImgDir = exifDir;
     }
@@ -402,6 +440,7 @@
      * @param iptcCaption the IPTC caption
      * @since 15219
      */
+    @Override
     public void setIptcCaption(String iptcCaption) {
         this.iptcCaption = iptcCaption;
     }
@@ -411,6 +450,7 @@
      * @param iptcHeadline the IPTC headline
      * @since 15219
      */
+    @Override
     public void setIptcHeadline(String iptcHeadline) {
         this.iptcHeadline = iptcHeadline;
     }
@@ -420,6 +460,7 @@
      * @param iptcKeywords the IPTC keywords
      * @since 15219
      */
+    @Override
     public void setIptcKeywords(List<String> iptcKeywords) {
         this.iptcKeywords = iptcKeywords;
     }
@@ -429,6 +470,7 @@
      * @param iptcObjectName the IPTC object name
      * @since 15219
      */
+    @Override
     public void setIptcObjectName(String iptcObjectName) {
         this.iptcObjectName = iptcObjectName;
     }
@@ -438,6 +480,7 @@
      * @return the IPTC caption
      * @since 15219
      */
+    @Override
     public String getIptcCaption() {
         return iptcCaption;
     }
@@ -447,6 +490,7 @@
      * @return the IPTC headline
      * @since 15219
      */
+    @Override
     public String getIptcHeadline() {
         return iptcHeadline;
     }
@@ -456,6 +500,7 @@
      * @return the IPTC keywords
      * @since 15219
      */
+    @Override
     public List<String> getIptcKeywords() {
         return iptcKeywords;
     }
@@ -465,6 +510,7 @@
      * @return the IPTC object name
      * @since 15219
      */
+    @Override
     public String getIptcObjectName() {
         return iptcObjectName;
     }
@@ -642,137 +688,14 @@
      * If successful, fills in the LatLon, speed, elevation, image direction, and other attributes
      * @since 9270
      */
+    @Override
     public void extractExif() {
-
-        Metadata metadata;
-
-        if (file == null) {
-            return;
-        }
-
-        String fn = file.getName();
-
-        try {
-            // try to parse metadata according to extension
-            String ext = fn.substring(fn.lastIndexOf('.') + 1).toLowerCase(Locale.US);
-            switch (ext) {
-            case "jpg":
-            case "jpeg":
-                metadata = JpegMetadataReader.readMetadata(file);
-                break;
-            case "tif":
-            case "tiff":
-                metadata = TiffMetadataReader.readMetadata(file);
-                break;
-            case "png":
-                metadata = PngMetadataReader.readMetadata(file);
-                break;
-            default:
-                throw new NoMetadataReaderWarning(ext);
-            }
-        } catch (JpegProcessingException | TiffProcessingException | PngProcessingException | IOException
-                | NoMetadataReaderWarning topException) {
-            //try other formats (e.g. JPEG file with .png extension)
-            try {
-                metadata = JpegMetadataReader.readMetadata(file);
-            } catch (JpegProcessingException | IOException ex1) {
-                try {
-                    metadata = TiffMetadataReader.readMetadata(file);
-                } catch (TiffProcessingException | IOException ex2) {
-                    try {
-                        metadata = PngMetadataReader.readMetadata(file);
-                    } catch (PngProcessingException | IOException ex3) {
-                        Logging.warn(topException);
-                        Logging.info(tr("Can''t parse metadata for file \"{0}\". Using last modified date as timestamp.", fn));
-                        setExifTime(Instant.ofEpochMilli(file.lastModified()));
-                        setExifCoor(null);
-                        setPos(null);
-                        return;
-                    }
-                }
-            }
-        }
-
-        IptcDirectory dirIptc = metadata.getFirstDirectoryOfType(IptcDirectory.class);
-        if (dirIptc != null) {
-            ifNotNull(ExifReader.readCaption(dirIptc), this::setIptcCaption);
-            ifNotNull(ExifReader.readHeadline(dirIptc), this::setIptcHeadline);
-            ifNotNull(ExifReader.readKeywords(dirIptc), this::setIptcKeywords);
-            ifNotNull(ExifReader.readObjectName(dirIptc), this::setIptcObjectName);
-        }
-
-        for (XmpDirectory xmpDirectory : metadata.getDirectoriesOfType(XmpDirectory.class)) {
-            Map<String, String> properties = xmpDirectory.getXmpProperties();
-            final String projectionType = "GPano:ProjectionType";
-            if (properties.containsKey(projectionType)) {
-                Stream.of(Projections.values()).filter(p -> p.name().equalsIgnoreCase(properties.get(projectionType)))
-                        .findFirst().ifPresent(projection -> this.cameraProjection = projection);
-                break;
-            }
-        }
-
-        // Changed to silently cope with no time info in exif. One case
-        // of person having time that couldn't be parsed, but valid GPS info
-        Instant time = null;
-        try {
-            time = ExifReader.readInstant(metadata);
-        } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException ex) {
-            Logging.warn(ex);
-        }
-
-        if (time == null) {
-            Logging.info(tr("No EXIF time in file \"{0}\". Using last modified date as timestamp.", fn));
-            time = Instant.ofEpochMilli(file.lastModified()); //use lastModified time if no EXIF time present
-        }
-        setExifTime(time);
-
-        final Directory dir = metadata.getFirstDirectoryOfType(JpegDirectory.class);
-        final Directory dirExif = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
-        final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
-
-        try {
-            if (dirExif != null && dirExif.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
-                setExifOrientation(dirExif.getInt(ExifIFD0Directory.TAG_ORIENTATION));
-            }
-        } catch (MetadataException ex) {
-            Logging.debug(ex);
-        }
-
-        try {
-            if (dir != null && dir.containsTag(JpegDirectory.TAG_IMAGE_WIDTH) && dir.containsTag(JpegDirectory.TAG_IMAGE_HEIGHT)) {
-                // there are cases where these do not match width and height stored in dirExif
-                setWidth(dir.getInt(JpegDirectory.TAG_IMAGE_WIDTH));
-                setHeight(dir.getInt(JpegDirectory.TAG_IMAGE_HEIGHT));
-            }
-        } catch (MetadataException ex) {
-            Logging.debug(ex);
-        }
-
-        if (dirGps == null || dirGps.getTagCount() <= 1) {
-            setExifCoor(null);
-            setPos(null);
-            return;
-        }
-
-        ifNotNull(ExifReader.readSpeed(dirGps), this::setSpeed);
-        ifNotNull(ExifReader.readElevation(dirGps), this::setElevation);
-
-        try {
-            setExifCoor(ExifReader.readLatLon(dirGps));
-            setPos(getExifCoor());
-        } catch (MetadataException | IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
-            Logging.error("Error reading EXIF from file: " + ex);
-            setExifCoor(null);
-            setPos(null);
-        }
-
-        try {
-            ifNotNull(ExifReader.readDirection(dirGps), this::setExifImgDir);
-        } catch (IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
-            Logging.debug(ex);
-        }
+        ImageMetadata.super.extractExif();
+    }
 
-        ifNotNull(dirGps.getGpsDate(), d -> setExifGpsTime(d.toInstant()));
+    @Override
+    public InputStream getInputStream() throws IOException {
+        return Files.newInputStream(this.getFile().toPath());
     }
 
     /**
@@ -786,27 +709,21 @@
         throw new UnsupportedOperationException("read not implemented for " + this.getClass().getSimpleName());
     }
 
-    private static class NoMetadataReaderWarning extends Exception {
-        NoMetadataReaderWarning(String ext) {
-            super("No metadata reader for format *." + ext);
-        }
-    }
-
-    private static <T> void ifNotNull(T value, Consumer<T> setter) {
-        if (value != null) {
-            setter.accept(value);
-        }
-    }
-
     /**
      * Get the projection type for this entry
      * @return The projection type
      * @since 18246
      */
+    @Override
     public Projections getProjectionType() {
         return this.cameraProjection;
     }
 
+    @Override
+    public void setProjectionType(Projections newProjection) {
+        this.cameraProjection = newProjection;
+    }
+
     /**
      * Returns a {@link WayPoint} representation of this GPX image entry.
      * @return a {@code WayPoint} representation of this GPX image entry (containing position, instant and elevation)
Index: 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/src/org/openstreetmap/josm/data/imagery/street_level/IImageEntry.java	(revision 18543)
+++ b/src/org/openstreetmap/josm/data/imagery/street_level/IImageEntry.java	(date 1662039360546)
@@ -1,6 +1,8 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.data.imagery.street_level;
 
+import static org.openstreetmap.josm.tools.I18n.tr;
+
 import java.awt.Dimension;
 import java.awt.image.BufferedImage;
 import java.io.File;
@@ -12,7 +14,12 @@
 import javax.imageio.IIOParam;
 
 import org.openstreetmap.josm.data.coor.ILatLon;
+import org.openstreetmap.josm.gui.layer.geoimage.ImageMetadata;
+import org.openstreetmap.josm.gui.layer.geoimage.ImageUtils;
 import org.openstreetmap.josm.gui.layer.geoimage.ImageViewerDialog;
+import org.openstreetmap.josm.tools.ExifReader;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.Logging;
 
 /**
  * An interface for image entries that will be shown in {@link org.openstreetmap.josm.gui.layer.geoimage.ImageDisplay}
@@ -137,7 +144,24 @@
      * @return the read image, or {@code null}
      * @throws IOException if any I/O error occurs
      */
-    BufferedImage read(Dimension target) throws IOException;
+    default BufferedImage read(Dimension target) throws IOException {
+        URI imageUrl = getImageURI();
+        Logging.info(tr("Loading {0}", imageUrl));
+        BufferedImage image = ImageProvider.read(imageUrl.toURL(), false, false,
+                r -> target == null ? r.getDefaultReadParam() : ImageUtils.withSubsampling(r, target));
+        if (image == null) {
+            Logging.warn("Unable to load {0}", imageUrl);
+            return null;
+        }
+        if (this instanceof ImageMetadata) {
+            ImageMetadata data = (ImageMetadata) this;
+            Logging.debug("Loaded {0} with dimensions {1}x{2} memoryTaken={3}m exifOrientationSwitchedDimension={4}",
+                    imageUrl, image.getWidth(), image.getHeight(), image.getWidth() * image.getHeight() * 4 / 1024 / 1024,
+                    ExifReader.orientationSwitchesDimensions(data.getExifOrientation()));
+            return ImageUtils.applyExifRotation(image, data.getExifOrientation());
+        }
+        return image;
+    }
 
     /**
      * Sets the width of this ImageEntry.
Index: 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/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java	(revision 18543)
+++ b/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java	(date 1662038935890)
@@ -704,12 +704,6 @@
             }
         };
         MainApplication.getLayerManager().addActiveLayerChangeListener(activeLayerChangeListener);
-
-        MapFrame map = MainApplication.getMap();
-        if (map.getToggleDialog(ImageViewerDialog.class) == null) {
-            ImageViewerDialog.createInstance();
-            map.addToggleDialog(ImageViewerDialog.getInstance());
-        }
     }
 
     @Override
Index: 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/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java	(revision 18543)
+++ b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java	(date 1662041497216)
@@ -1,30 +1,21 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.layer.geoimage;
 
-import static org.openstreetmap.josm.tools.I18n.tr;
-
 import java.awt.Dimension;
-import java.awt.Graphics2D;
 import java.awt.Image;
-import java.awt.geom.AffineTransform;
 import java.awt.image.BufferedImage;
 import java.io.File;
 import java.io.IOException;
-import java.io.UncheckedIOException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.Collections;
 import java.util.Objects;
+
 import javax.imageio.IIOParam;
-import javax.imageio.ImageReadParam;
-import javax.imageio.ImageReader;
 
 import org.openstreetmap.josm.data.ImageData;
 import org.openstreetmap.josm.data.gpx.GpxImageEntry;
 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
-import org.openstreetmap.josm.tools.ExifReader;
-import org.openstreetmap.josm.tools.ImageProvider;
-import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Utils;
 
 /**
@@ -193,53 +184,10 @@
      */
     @Override
     public BufferedImage read(Dimension target) throws IOException {
-        URL imageUrl = getImageUrl();
-        Logging.info(tr("Loading {0}", imageUrl));
-        BufferedImage image = ImageProvider.read(imageUrl, false, false,
-                r -> target == null ? r.getDefaultReadParam() : withSubsampling(r, target));
-        if (image == null) {
-            Logging.warn("Unable to load {0}", imageUrl);
-            return null;
-        }
-        Logging.debug("Loaded {0} with dimensions {1}x{2} memoryTaken={3}m exifOrientationSwitchedDimension={4}",
-                imageUrl, image.getWidth(), image.getHeight(), image.getWidth() * image.getHeight() * 4 / 1024 / 1024,
-                ExifReader.orientationSwitchesDimensions(getExifOrientation()));
-        return applyExifRotation(image);
+        return IImageEntry.super.read(target);
     }
 
     protected URL getImageUrl() throws MalformedURLException {
         return getFile().toURI().toURL();
     }
-
-    private ImageReadParam withSubsampling(ImageReader reader, Dimension target) {
-        try {
-            ImageReadParam param = reader.getDefaultReadParam();
-            Dimension source = new Dimension(reader.getWidth(0), reader.getHeight(0));
-            if (source.getWidth() > target.getWidth() || source.getHeight() > target.getHeight()) {
-                int subsampling = (int) Math.floor(Math.max(
-                        source.getWidth() / target.getWidth(),
-                        source.getHeight() / target.getHeight()));
-                param.setSourceSubsampling(subsampling, subsampling, 0, 0);
-            }
-            return param;
-        } catch (IOException e) {
-            throw new UncheckedIOException(e);
-        }
-    }
-
-    private BufferedImage applyExifRotation(BufferedImage img) {
-        Integer exifOrientation = getExifOrientation();
-        if (!ExifReader.orientationNeedsCorrection(exifOrientation)) {
-            return img;
-        }
-        boolean switchesDimensions = ExifReader.orientationSwitchesDimensions(exifOrientation);
-        int width = switchesDimensions ? img.getHeight() : img.getWidth();
-        int height = switchesDimensions ? img.getWidth() : img.getHeight();
-        BufferedImage rotated = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
-        AffineTransform transform = ExifReader.getRestoreOrientationTransform(exifOrientation, img.getWidth(), img.getHeight());
-        Graphics2D g = rotated.createGraphics();
-        g.drawImage(img, transform, null);
-        g.dispose();
-        return rotated;
-    }
 }
Index: 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
--- /dev/null	(date 1662041479517)
+++ b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageMetadata.java	(date 1662041479517)
@@ -0,0 +1,291 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.layer.geoimage;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.time.Instant;
+import java.util.List;
+
+import org.openstreetmap.josm.data.coor.ILatLon;
+import org.openstreetmap.josm.data.imagery.street_level.Projections;
+
+/**
+ * An interface for images with metadata
+ * @author Taylor Smock
+ * @since xxx
+ */
+public interface ImageMetadata {
+    /**
+     * Get the image location
+     * @return The image location
+     */
+    URI getImageURI();
+
+    /**
+     * Returns width of the image this ImageMetadata represents.
+     * @return width of the image this ImageMetadata represents
+     */
+    int getWidth();
+
+    /**
+     * Returns height of the image this ImageMetadata represents.
+     * @return height of the image this ImageMetadata represents
+     * @since 13220
+     */
+    int getHeight();
+
+    /**
+     * Returns the position value. The position value from the temporary copy
+     * is returned if that copy exists.
+     * @return the position value
+     */
+    ILatLon getPos();
+
+    /**
+     * Returns the speed value. The speed value from the temporary copy is
+     * returned if that copy exists.
+     * @return the speed value
+     */
+    Double getSpeed();
+
+    /**
+     * Returns the elevation value. The elevation value from the temporary
+     * copy is returned if that copy exists.
+     * @return the elevation value
+     */
+    Double getElevation();
+
+    /**
+     * Returns the GPS time value. The GPS time value from the temporary copy
+     * is returned if that copy exists.
+     * @return the GPS time value
+     */
+    Instant getGpsInstant();
+
+    /**
+     * Convenient way to determine if this entry has a GPS time, without the cost of building a defensive copy.
+     * @return {@code true} if this entry has a GPS time
+     * @since 6450
+     */
+    boolean hasGpsTime();
+
+    /**
+     * Returns a display name for this entry
+     * @return a display name for this entry
+     */
+    String getDisplayName();
+
+    /**
+     * Returns EXIF orientation
+     * @return EXIF orientation
+     */
+    default Integer getExifOrientation() {
+        return 1;
+    }
+
+    /**
+     * Returns EXIF time
+     * @return EXIF time
+     * @since 17715
+     */
+    Instant getExifInstant();
+
+    /**
+     * Convenient way to determine if this entry has a EXIF time, without the cost of building a defensive copy.
+     * @return {@code true} if this entry has a EXIF time
+     * @since 6450
+     */
+    boolean hasExifTime();
+
+    /**
+     * Returns the EXIF GPS time.
+     * @return the EXIF GPS time
+     * @since 17715
+     */
+    Instant getExifGpsInstant();
+
+    /**
+     * Convenient way to determine if this entry has a EXIF GPS time, without the cost of building a defensive copy.
+     * @return {@code true} if this entry has a EXIF GPS time
+     * @since 6450
+     */
+    boolean hasExifGpsTime();
+
+    /**
+     * Get the exif coordinates
+     * @return The location of the image
+     */
+    ILatLon getExifCoor();
+
+    /**
+     * Get the exif direction
+     * @return The image direction
+     */
+    Double getExifImgDir();
+
+    /**
+     * Get the last time the source was modified
+     * @return The last time the source was modified
+     */
+    Instant getLastModified();
+
+    /**
+     * Sets the width of this ImageMetadata.
+     * @param width set the width of this ImageMetadata
+     * @since 13220
+     */
+    void setWidth(int width);
+
+    /**
+     * Sets the height of this ImageMetadata.
+     * @param height set the height of this ImageMetadata
+     * @since 13220
+     */
+    void setHeight(int height);
+
+    /**
+     * Sets the position.
+     * @param pos position (will be cached)
+     */
+    void setPos(ILatLon pos);
+
+    /**
+     * Sets the speed.
+     * @param speed speed
+     */
+    void setSpeed(Double speed);
+
+    /**
+     * Sets the elevation.
+     * @param elevation elevation
+     */
+    void setElevation(Double elevation);
+
+    /**
+     * Sets EXIF orientation.
+     * @param exifOrientation EXIF orientation
+     */
+    void setExifOrientation(Integer exifOrientation);
+
+    /**
+     * Sets EXIF time.
+     * @param exifTime EXIF time
+     * @since 17715
+     */
+    void setExifTime(Instant exifTime);
+
+    /**
+     * Sets the EXIF GPS time.
+     * @param exifGpsTime the EXIF GPS time
+     * @since 17715
+     */
+    void setExifGpsTime(Instant exifGpsTime);
+
+    /**
+     * Sets the GPS time.
+     * @param gpsTime the GPS time
+     * @since 17715
+     */
+    void setGpsTime(Instant gpsTime);
+
+    /**
+     * Set the exif coordinates
+     * @param exifCoor The exif coordinates
+     */
+    void setExifCoor(ILatLon exifCoor);
+
+    /**
+     * Set the exif direction
+     * @param exifDir The direction
+     */
+    void setExifImgDir(Double exifDir);
+
+    /**
+     * Sets the IPTC caption.
+     * @param iptcCaption the IPTC caption
+     * @since 15219
+     */
+    void setIptcCaption(String iptcCaption);
+
+    /**
+     * Sets the IPTC headline.
+     * @param iptcHeadline the IPTC headline
+     * @since 15219
+     */
+    void setIptcHeadline(String iptcHeadline);
+
+    /**
+     * Sets the IPTC keywords.
+     * @param iptcKeywords the IPTC keywords
+     * @since 15219
+     */
+    void setIptcKeywords(List<String> iptcKeywords);
+
+    /**
+     * Sets the IPTC object name.
+     * @param iptcObjectName the IPTC object name
+     * @since 15219
+     */
+    void setIptcObjectName(String iptcObjectName);
+
+    /**
+     * Returns the IPTC caption.
+     * @return the IPTC caption
+     * @since 15219
+     */
+    String getIptcCaption();
+
+    /**
+     * Returns the IPTC headline.
+     * @return the IPTC headline
+     * @since 15219
+     */
+    String getIptcHeadline();
+
+    /**
+     * Returns the IPTC keywords.
+     * @return the IPTC keywords
+     * @since 15219
+     */
+    List<String> getIptcKeywords();
+
+    /**
+     * Returns the IPTC object name.
+     * @return the IPTC object name
+     * @since 15219
+     */
+    String getIptcObjectName();
+
+    /**
+     * Extract GPS metadata from image EXIF. Has no effect if the image file is not set
+     *
+     * If successful, fills in the LatLon, speed, elevation, image direction, and other attributes
+     * @since 9270
+     */
+    default void extractExif() {
+        try (InputStream original = getInputStream();
+             BufferedInputStream bufferedInputStream = new BufferedInputStream(original)) {
+            ImageUtils.applyExif(this, bufferedInputStream);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    InputStream getInputStream() throws IOException;
+
+    /**
+     * Get the projection type for this entry
+     * @return The projection type
+     * @since 18246 (extracted in xxx)
+     */
+    Projections getProjectionType();
+
+    /**
+     * Set the new projection type
+     * @param newProjection The new type
+     */
+    void setProjectionType(Projections newProjection);
+}
Index: 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
--- /dev/null	(date 1662039387644)
+++ b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageUtils.java	(date 1662039387644)
@@ -0,0 +1,253 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.layer.geoimage;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Dimension;
+import java.awt.Graphics2D;
+import java.awt.geom.AffineTransform;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.time.Instant;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import javax.imageio.ImageReadParam;
+import javax.imageio.ImageReader;
+
+import com.drew.imaging.jpeg.JpegMetadataReader;
+import com.drew.imaging.jpeg.JpegProcessingException;
+import com.drew.imaging.png.PngMetadataReader;
+import com.drew.imaging.png.PngProcessingException;
+import com.drew.imaging.tiff.TiffMetadataReader;
+import com.drew.imaging.tiff.TiffProcessingException;
+import com.drew.metadata.Directory;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.MetadataException;
+import com.drew.metadata.exif.ExifIFD0Directory;
+import com.drew.metadata.exif.GpsDirectory;
+import com.drew.metadata.iptc.IptcDirectory;
+import com.drew.metadata.jpeg.JpegDirectory;
+import com.drew.metadata.xmp.XmpDirectory;
+import org.openstreetmap.josm.data.imagery.street_level.Projections;
+import org.openstreetmap.josm.tools.ExifReader;
+import org.openstreetmap.josm.tools.JosmRuntimeException;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Image utilities
+ * @since xxx
+ */
+public final class ImageUtils {
+    private ImageUtils() {
+        // Hide constructor
+    }
+
+    /**
+     * Rotate an image, if needed
+     * @param img The image to rotate
+     * @param exifOrientation The exif orientation
+     * @return The rotated image or the original
+     */
+    public static BufferedImage applyExifRotation(BufferedImage img, Integer exifOrientation) {
+        if (exifOrientation == null || !ExifReader.orientationNeedsCorrection(exifOrientation)) {
+            return img;
+        }
+        boolean switchesDimensions = ExifReader.orientationSwitchesDimensions(exifOrientation);
+        int width = switchesDimensions ? img.getHeight() : img.getWidth();
+        int height = switchesDimensions ? img.getWidth() : img.getHeight();
+        BufferedImage rotated = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+        AffineTransform transform = ExifReader.getRestoreOrientationTransform(exifOrientation, img.getWidth(), img.getHeight());
+        Graphics2D g = rotated.createGraphics();
+        g.drawImage(img, transform, null);
+        g.dispose();
+        return rotated;
+    }
+
+    /**
+     * Common subsampling method
+     * @param reader The image reader
+     * @param target The target area
+     * @return The sampling parameters
+     */
+    public static ImageReadParam withSubsampling(ImageReader reader, Dimension target) {
+        try {
+            ImageReadParam param = reader.getDefaultReadParam();
+            Dimension source = new Dimension(reader.getWidth(0), reader.getHeight(0));
+            if (source.getWidth() > target.getWidth() || source.getHeight() > target.getHeight()) {
+                int subsampling = (int) Math.floor(Math.max(
+                        source.getWidth() / target.getWidth(),
+                        source.getHeight() / target.getHeight()));
+                param.setSourceSubsampling(subsampling, subsampling, 0, 0);
+            }
+            return param;
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    /**
+     * Apply exif information from an {@link InputStream}
+     * @param image The image to apply information to
+     * @param inputStream The input stream to read
+     */
+    public static void applyExif(ImageMetadata image, InputStream inputStream) {
+        Metadata metadata;
+
+        if (image == null || inputStream == null) {
+            return;
+        }
+
+        metadata = getMetadata(image.getImageURI(), inputStream);
+        if (metadata == null) {
+            image.setExifTime(image.getLastModified());
+            image.setExifCoor(null);
+            image.setPos(null);
+            return;
+        }
+        final String fn = image.getImageURI().toString();
+
+        IptcDirectory dirIptc = metadata.getFirstDirectoryOfType(IptcDirectory.class);
+        if (dirIptc != null) {
+            ifNotNull(ExifReader.readCaption(dirIptc), image::setIptcCaption);
+            ifNotNull(ExifReader.readHeadline(dirIptc), image::setIptcHeadline);
+            ifNotNull(ExifReader.readKeywords(dirIptc), image::setIptcKeywords);
+            ifNotNull(ExifReader.readObjectName(dirIptc), image::setIptcObjectName);
+        }
+
+        for (XmpDirectory xmpDirectory : metadata.getDirectoriesOfType(XmpDirectory.class)) {
+            Map<String, String> properties = xmpDirectory.getXmpProperties();
+            final String projectionType = "GPano:ProjectionType";
+            if (properties.containsKey(projectionType)) {
+                Stream.of(Projections.values()).filter(p -> p.name().equalsIgnoreCase(properties.get(projectionType)))
+                        .findFirst().ifPresent(image::setProjectionType);
+                break;
+            }
+        }
+
+        // Changed to silently cope with no time info in exif. One case
+        // of person having time that couldn't be parsed, but valid GPS info
+        Instant time = null;
+        try {
+            time = ExifReader.readInstant(metadata);
+        } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException ex) {
+            Logging.warn(ex);
+        }
+
+        if (time == null) {
+            Logging.info(tr("No EXIF time in file \"{0}\". Using last modified date as timestamp.", fn));
+            time = image.getLastModified(); //use lastModified time if no EXIF time present
+        }
+        image.setExifTime(time);
+
+        final Directory dir = metadata.getFirstDirectoryOfType(JpegDirectory.class);
+        final Directory dirExif = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
+        final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
+
+        try {
+            if (dirExif != null && dirExif.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
+                image.setExifOrientation(dirExif.getInt(ExifIFD0Directory.TAG_ORIENTATION));
+            }
+        } catch (MetadataException ex) {
+            Logging.debug(ex);
+        }
+
+        try {
+            if (dir != null && dir.containsTag(JpegDirectory.TAG_IMAGE_WIDTH) && dir.containsTag(JpegDirectory.TAG_IMAGE_HEIGHT)) {
+                // there are cases where these do not match width and height stored in dirExif
+                image.setWidth(dir.getInt(JpegDirectory.TAG_IMAGE_WIDTH));
+                image.setHeight(dir.getInt(JpegDirectory.TAG_IMAGE_HEIGHT));
+            }
+        } catch (MetadataException ex) {
+            Logging.debug(ex);
+        }
+
+        if (dirGps == null || dirGps.getTagCount() <= 1) {
+            image.setExifCoor(null);
+            image.setPos(null);
+            return;
+        }
+
+        ifNotNull(ExifReader.readSpeed(dirGps), image::setSpeed);
+        ifNotNull(ExifReader.readElevation(dirGps), image::setElevation);
+
+        try {
+            image.setExifCoor(ExifReader.readLatLon(dirGps));
+            image.setPos(image.getExifCoor());
+        } catch (MetadataException | IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
+            Logging.error("Error reading EXIF from file: " + ex);
+            image.setExifCoor(null);
+            image.setPos(null);
+        }
+
+        try {
+            ifNotNull(ExifReader.readDirection(dirGps), image::setExifImgDir);
+        } catch (IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
+            Logging.debug(ex);
+        }
+
+        ifNotNull(dirGps.getGpsDate(), d -> image.setExifGpsTime(d.toInstant()));
+    }
+
+    private static Metadata getMetadata(URI uri, InputStream inputStream) {
+        inputStream.mark(32);
+        final Exception topException;
+        final String fn = uri.toString();
+        try {
+            // try to parse metadata according to extension
+            String ext = fn.substring(fn.lastIndexOf('.') + 1).toLowerCase(Locale.US);
+            switch (ext) {
+                case "jpg":
+                case "jpeg":
+                    return JpegMetadataReader.readMetadata(inputStream);
+                case "tif":
+                case "tiff":
+                    return TiffMetadataReader.readMetadata(inputStream);
+                case "png":
+                    return PngMetadataReader.readMetadata(inputStream);
+                default:
+                    throw new NoMetadataReaderWarning(ext);
+            }
+        } catch (JpegProcessingException | TiffProcessingException | PngProcessingException | IOException
+                 | NoMetadataReaderWarning exception) {
+            //try other formats (e.g. JPEG file with .png extension)
+            topException = exception;
+        }
+        try {
+            return JpegMetadataReader.readMetadata(inputStream);
+        } catch (JpegProcessingException | IOException ex1) {
+            Logging.trace(ex1);
+        }
+        try {
+            return TiffMetadataReader.readMetadata(inputStream);
+        } catch (TiffProcessingException | IOException ex2) {
+            Logging.trace(ex2);
+        }
+
+        try {
+            return PngMetadataReader.readMetadata(inputStream);
+        } catch (PngProcessingException | IOException ex3) {
+            Logging.trace(ex3);
+        }
+        Logging.warn(topException);
+        Logging.info(tr("Can''t parse metadata for file \"{0}\". Using last modified date as timestamp.", fn));
+        return null;
+    }
+
+    private static class NoMetadataReaderWarning extends Exception {
+        NoMetadataReaderWarning(String ext) {
+            super("No metadata reader for format *." + ext);
+        }
+    }
+
+    private static <T> void ifNotNull(T value, Consumer<T> setter) {
+        if (value != null) {
+            setter.accept(value);
+        }
+    }
+}
Index: 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/src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java	(revision 18543)
+++ b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java	(date 1662039080332)
@@ -43,6 +43,7 @@
 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
 import org.openstreetmap.josm.gui.ExtendedDialog;
 import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.MapFrame;
 import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.openstreetmap.josm.gui.dialogs.DialogsPanel;
 import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
@@ -105,8 +106,14 @@
      * @return the unique instance
      */
     public static ImageViewerDialog getInstance() {
-        if (dialog == null)
-            throw new AssertionError("a new instance needs to be created first");
+        MapFrame map = MainApplication.getMap();
+        synchronized (ImageViewerDialog.class) {
+            if (dialog == null)
+                createInstance();
+            if (map != null && map.getToggleDialog(ImageViewerDialog.class) == null) {
+                map.addToggleDialog(dialog);
+            }
+        }
         return dialog;
     }
 
Index: 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
--- /dev/null	(date 1662042490517)
+++ b/src/org/openstreetmap/josm/gui/layer/geoimage/RemoteEntry.java	(date 1662042490517)
@@ -0,0 +1,315 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.layer.geoimage;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.time.Instant;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+import org.openstreetmap.josm.data.coor.ILatLon;
+import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
+import org.openstreetmap.josm.data.imagery.street_level.Projections;
+import org.openstreetmap.josm.tools.HttpClient;
+import org.openstreetmap.josm.tools.JosmRuntimeException;
+
+/**
+ * A remote image entry
+ * @since xxx
+ */
+public class RemoteEntry implements IImageEntry<RemoteEntry>, ImageMetadata {
+    private final URI uri;
+    private final Supplier<RemoteEntry> firstImage;
+    private final Supplier<RemoteEntry> nextImage;
+    private final Supplier<RemoteEntry> previousImage;
+    private final Supplier<RemoteEntry> lastImage;
+    private int width;
+    private int height;
+    private ILatLon pos;
+    private Integer exifOrientation;
+    private Double elevation;
+    private Double speed;
+    private Double exifImgDir;
+    private ILatLon exifCoor;
+    private Instant exifTime;
+    private Instant exifGpsTime;
+    private Instant gpsTime;
+    private String iptcObjectName;
+    private List<String> iptcKeywords;
+    private String iptcHeadline;
+    private String iptcCaption;
+    private Projections projection;
+    private String title;
+
+    /**
+     * Create a new remote entry
+     * @param uri The URI to use
+     * @param firstImage first image supplier
+     * @param nextImage next image supplier
+     * @param lastImage last image supplier
+     * @param previousImage previous image supplier
+     */
+    public RemoteEntry(URI uri, Supplier<RemoteEntry> firstImage, Supplier<RemoteEntry> previousImage,
+                       Supplier<RemoteEntry> nextImage, Supplier<RemoteEntry> lastImage) {
+        Objects.requireNonNull(uri);
+        Objects.requireNonNull(firstImage);
+        Objects.requireNonNull(previousImage);
+        Objects.requireNonNull(nextImage);
+        Objects.requireNonNull(lastImage);
+        this.uri = uri;
+        this.firstImage = firstImage;
+        this.previousImage = previousImage;
+        this.nextImage = nextImage;
+        this.lastImage = lastImage;
+    }
+
+    @Override
+    public RemoteEntry getNextImage() {
+        return this.nextImage.get();
+    }
+
+    @Override
+    public RemoteEntry getPreviousImage() {
+        return this.previousImage.get();
+    }
+
+    @Override
+    public RemoteEntry getFirstImage() {
+        return this.firstImage.get();
+    }
+
+    @Override
+    public RemoteEntry getLastImage() {
+        return this.lastImage.get();
+    }
+
+    @Override
+    public String getDisplayName() {
+        return this.title == null ? this.getImageURI().toString() : this.title;
+    }
+
+    @Override
+    public void setWidth(int width) {
+        this.width = width;
+    }
+
+    @Override
+    public void setHeight(int height) {
+        this.height = height;
+    }
+
+    @Override
+    public void setPos(ILatLon pos) {
+        this.pos = pos;
+    }
+
+    @Override
+    public void setSpeed(Double speed) {
+        this.speed = speed;
+    }
+
+    @Override
+    public void setElevation(Double elevation) {
+        this.elevation = elevation;
+    }
+
+    @Override
+    public void setExifOrientation(Integer exifOrientation) {
+        this.exifOrientation = exifOrientation;
+    }
+
+    @Override
+    public void setExifTime(Instant exifTime) {
+        this.exifTime = exifTime;
+    }
+
+    @Override
+    public void setExifGpsTime(Instant exifGpsTime) {
+        this.exifGpsTime = exifGpsTime;
+    }
+
+    @Override
+    public void setGpsTime(Instant gpsTime) {
+        this.gpsTime = gpsTime;
+    }
+
+    @Override
+    public void setExifCoor(ILatLon exifCoor) {
+        this.exifCoor = exifCoor;
+    }
+
+    @Override
+    public void setExifImgDir(Double exifDir) {
+        this.exifImgDir = exifDir;
+    }
+
+    @Override
+    public void setIptcCaption(String iptcCaption) {
+        this.iptcCaption = iptcCaption;
+    }
+
+    @Override
+    public void setIptcHeadline(String iptcHeadline) {
+        this.iptcHeadline = iptcHeadline;
+    }
+
+    @Override
+    public void setIptcKeywords(List<String> iptcKeywords) {
+        this.iptcKeywords = iptcKeywords;
+    }
+
+    @Override
+    public void setIptcObjectName(String iptcObjectName) {
+        this.iptcObjectName = iptcObjectName;
+    }
+
+    @Override
+    public Integer getExifOrientation() {
+        return this.exifOrientation != null ? this.exifOrientation : 1;
+    }
+
+    @Override
+    public File getFile() {
+        return null;
+    }
+
+    @Override
+    public URI getImageURI() {
+        return this.uri;
+    }
+
+    @Override
+    public int getWidth() {
+        return this.width;
+    }
+
+    @Override
+    public int getHeight() {
+        return this.height;
+    }
+
+    @Override
+    public ILatLon getPos() {
+        return this.pos;
+    }
+
+    @Override
+    public Double getSpeed() {
+        return this.speed;
+    }
+
+    @Override
+    public Double getElevation() {
+        return this.elevation;
+    }
+
+    @Override
+    public Double getExifImgDir() {
+        return this.exifImgDir;
+    }
+
+    @Override
+    public Instant getLastModified() {
+        if (this.getImageURI().getScheme().contains("file:")) {
+            try {
+                return Files.getLastModifiedTime(Paths.get(this.getImageURI())).toInstant();
+            } catch (IOException e) {
+                throw new UncheckedIOException(e);
+            }
+        }
+        try {
+            return Instant.ofEpochMilli(HttpClient.create(this.getImageURI().toURL(), "HEAD").getResponse().getLastModified());
+        } catch (MalformedURLException e) {
+            throw new JosmRuntimeException(e);
+        }
+    }
+
+    @Override
+    public boolean hasExifTime() {
+        return this.exifTime != null;
+    }
+
+    @Override
+    public Instant getExifGpsInstant() {
+        return this.exifGpsTime;
+    }
+
+    @Override
+    public boolean hasExifGpsTime() {
+        return this.exifGpsTime != null;
+    }
+
+    @Override
+    public ILatLon getExifCoor() {
+        return this.exifCoor;
+    }
+
+    @Override
+    public Instant getExifInstant() {
+        return this.exifTime;
+    }
+
+    @Override
+    public boolean hasGpsTime() {
+        return this.gpsTime != null;
+    }
+
+    @Override
+    public Instant getGpsInstant() {
+        return this.gpsTime;
+    }
+
+    @Override
+    public String getIptcCaption() {
+        return this.iptcCaption;
+    }
+
+    @Override
+    public String getIptcHeadline() {
+        return this.iptcHeadline;
+    }
+
+    @Override
+    public List<String> getIptcKeywords() {
+        return this.iptcKeywords;
+    }
+
+    @Override
+    public String getIptcObjectName() {
+        return this.iptcObjectName;
+    }
+
+    @Override
+    public Projections getProjectionType() {
+        return this.projection;
+    }
+
+    @Override
+    public InputStream getInputStream() throws IOException {
+        URI u = getImageURI();
+        if (u.getScheme().contains("file")) {
+            return Files.newInputStream(Paths.get(u));
+        }
+        return HttpClient.create(u.toURL()).connect().getContent();
+    }
+
+    @Override
+    public void setProjectionType(Projections newProjection) {
+        this.projection = newProjection;
+    }
+
+    /**
+     * Set the display name for this entry
+     * @param text The display name
+     */
+    public void setDisplayName(String text) {
+        this.title = text;
+    }
+}
Index: 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/src/org/openstreetmap/josm/gui/layer/markerlayer/ImageMarker.java	(revision 18543)
+++ b/src/org/openstreetmap/josm/gui/layer/markerlayer/ImageMarker.java	(date 1662043377724)
@@ -1,30 +1,27 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.layer.markerlayer;
 
-import java.awt.BorderLayout;
-import java.awt.Cursor;
-import java.awt.GraphicsEnvironment;
-import java.awt.Image;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
 import java.awt.event.ActionEvent;
+import java.net.URISyntaxException;
 import java.net.URL;
+import java.time.Instant;
 import java.util.Collections;
+import java.util.function.Supplier;
 
-import javax.swing.Icon;
-import javax.swing.ImageIcon;
-import javax.swing.JDialog;
-import javax.swing.JLabel;
 import javax.swing.JOptionPane;
-import javax.swing.JPanel;
-import javax.swing.JScrollPane;
-import javax.swing.JToggleButton;
-import javax.swing.JViewport;
 
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.gpx.GpxConstants;
 import org.openstreetmap.josm.data.gpx.GpxLink;
 import org.openstreetmap.josm.data.gpx.WayPoint;
-import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
+import org.openstreetmap.josm.gui.Notification;
+import org.openstreetmap.josm.gui.layer.geoimage.ImageViewerDialog;
+import org.openstreetmap.josm.gui.layer.geoimage.RemoteEntry;
+import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.Utils;
 
 /**
  * Marker representing an image. Uses a special icon, and when clicked,
@@ -42,49 +39,85 @@
         this.imageUrl = imageUrl;
     }
 
-    @Override public void actionPerformed(ActionEvent ev) {
-        final JPanel p = new JPanel(new BorderLayout());
-        final JScrollPane scroll = new JScrollPane(new JLabel(loadScaledImage(imageUrl, 580)));
-        final JViewport vp = scroll.getViewport();
-        p.add(scroll, BorderLayout.CENTER);
+    @Override
+    public void actionPerformed(ActionEvent ev) {
+        ImageViewerDialog.getInstance().displayImage(getRemoteEntry());
+    }
 
-        final JToggleButton scale = new JToggleButton(ImageProvider.get("misc", "rectangle"));
+    private RemoteEntry getRemoteEntry() {
+        try {
+            final RemoteEntry remoteEntry = new RemoteEntry(imageUrl.toURI(), getFirstImage(), getPreviousImage(),
+                    getNextImage(), getLastImage());
+            // First, extract EXIF data
+            remoteEntry.extractExif();
+            // Then, apply information from this point. This may overwrite details from
+            // the exif, but that will (hopefully) be OK.
+            if (Double.isFinite(this.time)) {
+                remoteEntry.setGpsTime(Instant.ofEpochMilli((long) (this.time * 1000)));
+            }
+            if (this.isLatLonKnown()) {
+                remoteEntry.setPos(this);
+            }
+            if (!Utils.isBlank(this.getText())) {
+                remoteEntry.setDisplayName(this.getText());
+            }
+            return remoteEntry;
+        } catch (URISyntaxException e) {
+            Logging.trace(e);
+            new Notification(tr("Malformed URI: ", this.imageUrl.toExternalForm())).setIcon(JOptionPane.WARNING_MESSAGE).show();
+        }
+        return null;
+    }
 
-        JPanel p2 = new JPanel();
-        p2.add(scale);
-        p.add(p2, BorderLayout.SOUTH);
-        scale.addActionListener(ev1 -> {
-            p.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
-            if (scale.getModel().isSelected()) {
-                ((JLabel) vp.getView()).setIcon(loadScaledImage(imageUrl, Math.max(vp.getWidth(), vp.getHeight())));
-            } else {
-                ((JLabel) vp.getView()).setIcon(new ImageIcon(imageUrl));
+    private Supplier<RemoteEntry> getFirstImage() {
+        for (Marker marker : this.parentLayer.data) {
+            if (marker instanceof ImageMarker) {
+                if (marker == this) {
+                    break;
+                }
+                ImageMarker imageMarker = (ImageMarker) marker;
+                return imageMarker::getRemoteEntry;
             }
-            p.setCursor(Cursor.getDefaultCursor());
-        });
-        scale.setSelected(true);
-        JOptionPane pane = new JOptionPane(p, JOptionPane.PLAIN_MESSAGE);
-        if (!GraphicsEnvironment.isHeadless()) {
-            JDialog dlg = pane.createDialog(MainApplication.getMainFrame(), imageUrl.toString());
-            dlg.setModal(false);
-            dlg.toFront();
-            dlg.setVisible(true);
         }
+        return () -> null;
     }
 
-    private static Icon loadScaledImage(URL u, int maxSize) {
-        Image img = new ImageIcon(u).getImage();
-        int w = img.getWidth(null);
-        int h = img.getHeight(null);
-        if (w > h) {
-            h = (int) Math.round(maxSize*((double) h/w));
-            w = maxSize;
-        } else {
-            w = (int) Math.round(maxSize*((double) w/h));
-            h = maxSize;
+    private Supplier<RemoteEntry> getPreviousImage() {
+        int index = this.parentLayer.data.indexOf(this);
+        for (int i = index - 1; i >= 0; i--) {
+            Marker marker = this.parentLayer.data.get(i);
+            if (marker instanceof ImageMarker) {
+                ImageMarker imageMarker = (ImageMarker) marker;
+                return imageMarker::getRemoteEntry;
+            }
         }
-        return new ImageIcon(img.getScaledInstance(w, h, Image.SCALE_SMOOTH));
+        return () -> null;
     }
+    private Supplier<RemoteEntry> getNextImage() {
+        int index = this.parentLayer.data.indexOf(this);
+        for (int i = index + 1; i < this.parentLayer.data.size(); i++) {
+            Marker marker = this.parentLayer.data.get(i);
+            if (marker instanceof ImageMarker) {
+                ImageMarker imageMarker = (ImageMarker) marker;
+                return imageMarker::getRemoteEntry;
+            }
+        }
+        return () -> null;
+    }
+    private Supplier<RemoteEntry> getLastImage() {
+        int index = this.parentLayer.data.indexOf(this);
+        for (int i = this.parentLayer.data.size() - 1; i >= index; i--) {
+            Marker marker = this.parentLayer.data.get(i);
+            if (marker instanceof ImageMarker) {
+                if (marker == this) {
+                    break;
+                }
+                ImageMarker imageMarker = (ImageMarker) marker;
+                return imageMarker::getRemoteEntry;
+            }
+        }
+        return () -> null;
+    }
 
     @Override
     public WayPoint convertToWayPoint() {
