Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/CubeMapTileXY.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/CubeMapTileXY.java	(revision 36228)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/CubeMapTileXY.java	(revision 36228)
@@ -0,0 +1,52 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside;
+
+import org.openstreetmap.josm.plugins.streetside.cubemap.CubemapUtils;
+
+import jakarta.annotation.Nonnull;
+
+/**
+ * A record for keeping track of what URL image goes with what side
+ * @param side The side of the
+ * @param x The x coordinate (top-left is 0)
+ * @param y The y coordinate (top-left is 0)
+ * @see <a href="https://learn.microsoft.com/en-us/bingmaps/articles/getting-streetside-tiles-from-imagery-metadata">
+ *     Getting Streetside Tiles from Imagery Metadata
+ * </a>
+ */
+public record CubeMapTileXY(CubemapUtils.CubemapFaces side, int x, int y) {
+    /**
+     * Get the quadkey given a zoom level
+     * @param zoom The zoom
+     * @return The quad key for this tile
+     */
+    @Nonnull
+    public String getQuadKey(int zoom) {
+        return xyzToQuadKey(x, y, zoom);
+    }
+
+    /**
+     * Convert an x y z coordinate to a quadkey
+     * @param x The x coordinate
+     * @param y The y coordinate
+     * @param z The z coordinate
+     * @return The quadkey
+     */
+    @Nonnull
+    private static String xyzToQuadKey(int x, int y, int z) {
+        final var string = new char[z];
+        for (int i = z; i > 0; i--) {
+            var digit = '0';
+            final int mask = 1 << (i - 1);
+            if ((x & mask) != 0) {
+                digit++;
+            }
+            if ((y & mask) != 0) {
+                digit += 2;
+            }
+            string[z - i] = digit;
+        }
+        return String.valueOf(string);
+    }
+
+}
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideAbstractImage.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideAbstractImage.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideAbstractImage.java	(revision 36228)
@@ -2,9 +2,21 @@
 package org.openstreetmap.josm.plugins.streetside;
 
-import org.openstreetmap.josm.data.coor.LatLon;
+import java.io.Serializable;
+import java.util.List;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.openstreetmap.gui.jmapviewer.TileXY;
+import org.openstreetmap.josm.data.IQuadBucketType;
+import org.openstreetmap.josm.data.coor.ILatLon;
+import org.openstreetmap.josm.data.osm.BBox;
+import org.openstreetmap.josm.plugins.streetside.cubemap.CubemapUtils;
+import org.openstreetmap.josm.tools.Pair;
+
+import jakarta.annotation.Nonnull;
 
 /**
- * Abstract superclass for all image objects. At the moment there are 2,
- * {@link StreetsideImage}, {@link StreetsideCubemap}.
+ * Abstract superclass for all image objects. At the moment there is one,
+ * {@link StreetsideImage}.
  *
  * @author nokutu
@@ -12,304 +24,169 @@
  *
  */
-public abstract class StreetsideAbstractImage implements Comparable<StreetsideAbstractImage> {
-  /**
-   * If two values for field cd differ by less than EPSILON both values are
-   * considered equal.
-   */
-  private static final float EPSILON = 1e-5f;
+public sealed
 
-  protected String id;
-  /**
-   * Position of the picture.
-   */
-  protected LatLon latLon;
-  //Image id of previous image in sequence (decimal)
-  private long pr;
-  /**
-   * Direction of the picture in degrees from true north.
-   */
-  protected double he;
-  /**
-   * When the object direction is being moved in the map, the temporal direction
-   * is stored here
-   */
-  protected double movingHe;
-  // Image id of next image in sequence (decimal)
-  private long ne;
-  /**
-   * Sequence of pictures containing this object.
-   */
-  private StreetsideSequence sequence;
-  /**
-   * Temporal position of the picture until it is uploaded.
-   */
-  private LatLon tempLatLon;
-  /**
-   * When the object is being dragged in the map, the temporal position is stored
-   * here.
-   */
-  private LatLon movingLatLon;
-  /**
-   * Temporal direction of the picture until it is uploaded
-   */
-  private double tempHe;
-  /**
-   * Whether the image must be drown in the map or not
-   */
-  private boolean visible;
+interface StreetsideAbstractImage extends ILatLon, IQuadBucketType, Comparable<StreetsideAbstractImage>, Serializable
+permits StreetsideImage
+{
 
-  /**
-   * Creates a new object in the given position and with the given direction.
-   * {@link LatLon}
-   *
-   * @param id   - the Streetside image id
-   * @param latLon The latitude and longitude of the image.
-   * @param he   The direction of the picture (0 means north im Mapillary
-   *         camera direction is not yet supported in the Streetside plugin).
-   */
-  protected StreetsideAbstractImage(final String id, final LatLon latLon, final double he) {
-    this.id = id;
-    this.latLon = latLon;
-    tempLatLon = this.latLon;
-    movingLatLon = this.latLon;
-    this.he = he;
-    tempHe = he;
-    movingHe = he;
-    visible = true;
-  }
+    /**
+     * Get the ID for this image
+     * @return the id
+     */
+    String id();
 
-  /**
-   * Creates a new object with the given id.
-   *
-   * @param id - the image id (All images require ids in Streetside)
-   */
-  protected StreetsideAbstractImage(final String id) {
-    this.id = id;
+    /**
+     * Get the subdomains that can be used for this image
+     * @return The valid subdomains
+     */
+    List<String> imageUrlSubdomains();
 
-    visible = true;
-  }
+    /**
+     * Returns the original direction towards the image has been taken.
+     *
+     * @return The direction of the image (0 means north and goes clockwise).
+     */
+    double heading();
 
-  /**
-   * @return the id
-   */
-  public String getId() {
-    return id;
-  }
+    /**
+     * The maximum zoom for this image
+     * @return The max zoom
+     */
+    int zoomMax();
 
-  /**
-   * @param id the id to set
-   */
-  public void setId(String id) {
-    this.id = id;
-  }
+    /**
+     * The minimum zoom for this image
+     * @return The min zoom
+     */
+    int zoomMin();
 
-  /**
-   * Returns the original direction towards the image has been taken.
-   *
-   * @return The direction of the image (0 means north and goes clockwise).
-   */
-  public double getHe() {
-    return he;
-  }
+    /**
+     * Get the number of x columns
+     * @param zoom The zoom level
+     * @return The columns for the x-axis
+     */
+    default int xCols(int zoom) {
+        return yCols(zoom);
+    }
 
-  public void setHe(final double he) {
-    this.he = he;
-  }
+    /**
+     * Get the number of y columns
+     * @param zoom The zoom level
+     * @return The columns for the y-axis
+     */
+    default int yCols(int zoom) {
+        return 1 << zoom;
+    }
 
-  /**
-   * Returns a LatLon object containing the original coordinates of the object.
-   *
-   * @return The LatLon object with the position of the object.
-   */
-  public LatLon getLatLon() {
-    return latLon;
-  }
+    /**
+     * Check if the image is visible
+     * @return {@code true} if the image is visible
+     */
+    default boolean visible() {
+        return true;
+    }
 
-  public void setLatLon(final LatLon latLon) {
-    if (latLon != null) {
-      this.latLon = latLon;
+    @Override
+    default BBox getBBox() {
+        return new BBox(this);
     }
-  }
 
-  /**
-   * Returns the direction towards the image has been taken.
-   *
-   * @return The direction of the image (0 means north and goes clockwise).
-   */
-  public double getMovingHe() {
-    return movingHe;
-  }
+    /**
+     * Get a thumbnail for the image
+     * @return The URL for the thumbnail
+     */
+    default String getThumbnail() {
+        // This is the "front" min zoom image
+        return getTile(Integer.toString(0), Integer.toString(1));
+    }
 
-  /**
-   * Returns a LatLon object containing the current coordinates of the object.
-   * When you are dragging the image this changes.
-   *
-   * @return The LatLon object with the position of the object.
-   */
-  public LatLon getMovingLatLon() {
-    return movingLatLon;
-  }
+    /**
+     * Get the tiles for a face
+     * @param face The id of the face
+     * @param zoom The zoom level for the face
+     * @return A stream of tile location + URL pairs
+     */
+    default Stream<Pair<CubeMapTileXY, String>> getFaceTiles(CubemapUtils.CubemapFaces face, int zoom) {
+        if (zoom > this.zoomMax() || zoom < this.zoomMin()) {
+            throw new IndexOutOfBoundsException(zoom);
+        }
+        final var faceId = face.faceId();
+        final var startingTileId = face.startingTileId();
+        // The {tileId} is chainable after the starting tile id
+        // ---------------
+        // | 0, 0 | 1, 0 |
+        // | 0, 1 | 1, 1 |
+        // ---------------
+        // (0, 0) is 0
+        // (1, 0) is 1
+        // (0, 1) is 2
+        // (1, 1) is 3
+        // Zoom starts at 1
+        int currentZoom = this.zoomMin();
+        Stream<String> level = IntStream.range(0, 4).mapToObj(String::valueOf).map(startingTileId::concat);
+        while (currentZoom < zoom) {
+            level = level.flatMap(s -> IntStream.range(0, 4).mapToObj(String::valueOf).map(s::concat));
+            currentZoom++;
+        }
+        return level.map(s -> new Pair<>(mapToTiles(face, zoom, s), getTile(faceId, s)));
+    }
 
-  /**
-   * Returns the sequence which contains this image. Never null.
-   *
-   * @return The StreetsideSequence object that contains this StreetsideImage.
-   */
+    /**
+     * Convert a tileId to a TileXY coordinate
+     * @param face The cubemap face
+     * @param zoom the zoom level
+     * @param tileId The tile id (quadkey)
+     * @return The xy coordinate
+     */
+    private static CubeMapTileXY mapToTiles(CubemapUtils.CubemapFaces face, int zoom, String tileId) {
+        // Given a z level, the quadkey is the last z characters
+        final var xy = quadKeyToTile(tileId.subSequence(tileId.length() - zoom, tileId.length()));
+        return new CubeMapTileXY(face, xy.getXIndex(), xy.getYIndex());
+    }
 
-  public StreetsideSequence getSequence() {
-    synchronized (this) {
-      if (sequence == null) {
-        sequence = new StreetsideSequence();
-        sequence.add(this);
-      }
-      return sequence;
+    /**
+     * Convert a quadkey to a tile
+     * @param quadkey The quadkey to convert
+     * @return The tile for that quadkey
+     */
+    @Nonnull
+    static TileXY quadKeyToTile(@Nonnull CharSequence quadkey) {
+        final int z = quadkey.length();
+        var x = 0;
+        var y = 0;
+        for (int i = z; i > 0; i--) {
+            final var mask = 1 << (i - 1);
+            switch (quadkey.charAt(z - i)) {
+            case '0':
+                break;
+            case '1':
+                x |= mask;
+                break;
+            case '2':
+                y |= mask;
+                break;
+            case '3':
+                x |= mask;
+                y |= mask;
+                break;
+            default:
+                throw new IllegalArgumentException("Bad quadtile character at " + (i - 1) + " for '" + quadkey + "'");
+            }
+        }
+        return new TileXY(x, y);
     }
-  }
 
-  /**
-   * Sets the StreetsideSequence object which contains the StreetsideImage.
-   *
-   * @param sequence
-   *      The StreetsideSequence that contains the StreetsideImage.
-   * @throws IllegalArgumentException
-   *       if the image is not already part of the
-   *       {@link StreetsideSequence}. Call
-   *       {@link StreetsideSequence#add(StreetsideAbstractImage)} first.
-   */
-  public void setSequence(final StreetsideSequence sequence) {
-    synchronized (this) {
-      if (sequence != null && !sequence.getImages().contains(this)) {
-        throw new IllegalArgumentException();
-      }
-      this.sequence = sequence;
+    /**
+     * Get a tile for an image
+     * @param faceId The face id
+     * @param tileId The tile id
+     * @return The URL for the face and tile
+     * @see <a href="https://learn.microsoft.com/en-us/bingmaps/articles/getting-streetside-tiles-from-imagery-metadata">
+     *     Getting Streetside Tiles from Imagery Metadata
+     * </a>
+     */
+    default String getTile(String faceId, String tileId) {
+        return this.id().replace("{subdomain}", this.imageUrlSubdomains().get(0)).replace("{faceId}", faceId)
+                .replace("{tileId}", tileId);
     }
-  }
-
-  /**
-   * Returns the last fixed direction of the object.
-   *
-   * @return The last fixed direction of the object. 0 means north.
-   */
-  public double getTempHe() {
-    return tempHe;
-  }
-
-  /**
-   * Returns the last fixed coordinates of the object.
-   *
-   * @return A LatLon object containing.
-   */
-  public LatLon getTempLatLon() {
-    return tempLatLon;
-  }
-
-  /**
-   * Returns whether the object has been modified or not.
-   *
-   * @return true if the object has been modified; false otherwise.
-   */
-  public boolean isModified() {
-    return !getMovingLatLon().equals(latLon) || Math.abs(getMovingHe() - he) > EPSILON;
-  }
-
-  /**
-   * Returns whether the image is visible on the map or not.
-   *
-   * @return True if the image is visible; false otherwise.
-   */
-  public boolean isVisible() {
-    return visible;
-  }
-
-  /**
-   * Set's whether the image should be visible on the map or not.
-   *
-   * @param visible
-   *      true if the image is set to be visible; false otherwise.
-   */
-  public void setVisible(final boolean visible) {
-    this.visible = visible;
-  }
-
-  /**
-   * Moves the image temporally to another position
-   *
-   * @param x The movement of the image in longitude units.
-   * @param y The movement of the image in latitude units.
-   */
-  public void move(final double x, final double y) {
-    movingLatLon = new LatLon(tempLatLon.getY() + y, tempLatLon.getX() + x);
-  }
-
-  /**
-   * If the StreetsideImage belongs to a StreetsideSequence, returns the next
-   * image in the sequence.
-   *
-   * @return The following StreetsideImage, or null if there is none.
-   */
-  public StreetsideAbstractImage next() {
-    synchronized (this) {
-      return getSequence().next(this);
-    }
-  }
-
-  /**
-   * If the StreetsideImage belongs to a StreetsideSequence, returns the previous
-   * image in the sequence.
-   *
-   * @return The previous StreetsideImage, or null if there is none.
-   */
-  public StreetsideAbstractImage previous() {
-    synchronized (this) {
-      return getSequence().previous(this);
-    }
-  }
-
-  /**
-   * Called when the mouse button is released, meaning that the picture has
-   * stopped being dragged, so the temporal values are saved.
-   */
-  public void stopMoving() {
-    tempLatLon = movingLatLon;
-    tempHe = movingHe;
-  }
-
-  /**
-   * Turns the image direction.
-   *
-   * @param he
-   *      The angle the image is moving.
-   */
-  public void turn(final double he) {
-    movingHe = tempHe + he;
-  }
-
-  /**
-   * @return the ne
-   */
-  public long getNe() {
-    return ne;
-  }
-
-  /**
-   * @param ne the ne to set
-   */
-  public void setNe(long ne) {
-    this.ne = ne;
-  }
-
-  /**
-   * @return the pr
-   */
-  public long getPr() {
-    return pr;
-  }
-
-  /**
-   * @param pr the pr to set
-   */
-  public void setPr(long pr) {
-    this.pr = pr;
-  }
-
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideCubemap.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideCubemap.java	(revision 36194)
+++ 	(revision )
@@ -1,96 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.streetside;
-
-import org.openstreetmap.josm.data.coor.LatLon;
-import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
-
-/**
- * @author renerr18
- *
- *
- */
-public class StreetsideCubemap extends StreetsideAbstractImage {
-
-  /**
-   * If two values for field cd differ by less than EPSILON both values are considered equal.
-   */
-  @SuppressWarnings("unused")
-  private static final float EPSILON = 1e-5f;
-
-  /**
-   * Main constructor of the class StreetsideCubemap
-   *
-   * @param quadId The Streetside id of the base frontal image of the cubemap
-   *         in quternary
-   * @param latLon The latitude and longitude where it is positioned.
-   * @param he   The direction of the images in degrees, meaning 0 north (camera
-   *         direction is not yet supported in the Streetside plugin).
-   */
-  public StreetsideCubemap(String quadId, LatLon latLon, double he) {
-    super(quadId, latLon, he);
-  }
-
-  /**
-   * Comparison method for the StreetsideCubemap object.
-   *
-   * @param image
-   *      - a StreetsideAbstract image object
-   *
-   *      StreetsideCubemaps are considered equal if they are associated
-   *      with the same image id - only one cubemap may be displayed at a
-   *      time. If the image selection changes, the cubemap changes.
-   *
-   * @return result of the hashcode comparison.
-   * @see org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage
-   */
-  @Override
-  public int compareTo(StreetsideAbstractImage image) {
-    if (image instanceof StreetsideImage) {
-      return id.compareTo(image.getId());
-    }
-    return hashCode() - image.hashCode();
-  }
-
-  /**
-   * HashCode StreetsideCubemap object.
-   *
-   * @return int hashCode
-   * @see org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage
-   */
-  @Override
-  public int hashCode() {
-    return id.hashCode();
-  }
-
-  /**
-   * stops ImageDisplay WalkAction (not currently supported by Streetside)
-   *
-   * @see org.openstreetmap.josm.plugins.streetside.actions.StreetsideWalkAction
-   */
-  @Override
-  public void stopMoving() {
-    super.stopMoving();
-  }
-
-  /**
-   * turns ImageDisplay WalkAction (not currently supported by Streetside)
-   *
-   * @param he - the direction the camera is facing (heading)
-   *
-   * @see org.openstreetmap.josm.plugins.streetside.actions.StreetsideWalkAction
-   */
-  @Override
-  public void turn(double he) {
-    super.turn(he);
-  }
-
-  /**
-   * @return the height of an assembled cubemap face for 16-tiled or 4-tiled imagery
-   *
-   * @see org.openstreetmap.josm.plugins.streetside.actions.StreetsideWalkAction
-   */
-  public int getHeight() {
-    return Boolean.TRUE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get()) ? 1016 : 510;
-  }
-
-}
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideData.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideData.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideData.java	(revision 36228)
@@ -2,13 +2,14 @@
 package org.openstreetmap.josm.plugins.streetside;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.stream.Collectors;
+import java.util.function.Consumer;
 
 import org.apache.commons.jcs3.access.CacheAccess;
@@ -17,6 +18,8 @@
 import org.openstreetmap.josm.data.DataSource;
 import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.BBox;
+import org.openstreetmap.josm.data.osm.QuadBuckets;
 import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.plugins.streetside.cache.CacheUtils;
 import org.openstreetmap.josm.plugins.streetside.cache.Caches;
@@ -32,392 +35,365 @@
  * @author renerr18 (extended for Streetside)
  * @see StreetsideAbstractImage
- * @see StreetsideSequence
  */
 public class StreetsideData implements Data {
-  private final Set<StreetsideAbstractImage> images = ConcurrentHashMap.newKeySet();
-  /**
-   * All the images selected, can be more than one.
-   */
-  private final Set<StreetsideAbstractImage> multiSelectedImages = ConcurrentHashMap.newKeySet();
-  /**
-   * Listeners of the class.
-   */
-  private final List<StreetsideDataListener> listeners = new CopyOnWriteArrayList<>();
-  /**
-   * The bounds of the areas for which the pictures have been downloaded.
-   */
-  private final List<Bounds> bounds;
-  /**
-   * The image currently selected, this is the one being shown.
-   */
-  private StreetsideAbstractImage selectedImage;
-  /**
-   * The image under the cursor.
-   */
-  private StreetsideAbstractImage highlightedImage;
-
-  /**
-   * Creates a new object and adds the initial set of listeners.
-   */
-  protected StreetsideData() {
-    selectedImage = null;
-    bounds = new CopyOnWriteArrayList<>();
-
-    // Adds the basic set of listeners.
-    Arrays.stream(StreetsidePlugin.getStreetsideDataListeners()).forEach(this::addListener);
-    addListener(StreetsideViewerDialog.getInstance().getStreetsideViewerPanel());
-    addListener(StreetsideMainDialog.getInstance());
-    addListener(ImageInfoPanel.getInstance());
-  }
-
-  /**
-   * Downloads surrounding images of this mapillary image in background threads
-   *
-   * @param streetsideImage the image for which the surrounding images should be downloaded
-   */
-  private static void downloadSurroundingImages(StreetsideImage streetsideImage) {
-    MainApplication.worker.execute(() -> {
-      final int prefetchCount = StreetsideProperties.PRE_FETCH_IMAGE_COUNT.get();
-      CacheAccess<String, BufferedImageCacheEntry> imageCache = Caches.ImageCache.getInstance().getCache();
-
-      StreetsideAbstractImage nextImage = streetsideImage.next();
-      StreetsideAbstractImage prevImage = streetsideImage.previous();
-
-      for (int i = 0; i < prefetchCount; i++) {
-        if (nextImage != null) {
-          if (nextImage instanceof StreetsideImage && imageCache.get(nextImage.getId()) == null) {
-            CacheUtils.downloadPicture((StreetsideImage) nextImage);
-          }
-          nextImage = nextImage.next();
-        }
-        if (prevImage != null) {
-          if (prevImage instanceof StreetsideImage && imageCache.get(prevImage.getId()) == null) {
-            CacheUtils.downloadPicture((StreetsideImage) prevImage);
-          }
-          prevImage = prevImage.previous();
-        }
-      }
-    });
-  }
-
-  /**
-   * Downloads surrounding images of this mapillary image in background threads
-   *
-   * @param streetsideImage the image for which the surrounding images should be downloaded
-   */
-  public static void downloadSurroundingCubemaps(StreetsideImage streetsideImage) {
-    MainApplication.worker.execute(() -> {
-      final int prefetchCount = StreetsideProperties.PRE_FETCH_IMAGE_COUNT.get();
-      CacheAccess<String, BufferedImageCacheEntry> imageCache = Caches.ImageCache.getInstance().getCache();
-
-      StreetsideAbstractImage nextImage = streetsideImage.next();
-      StreetsideAbstractImage prevImage = streetsideImage.previous();
-
-      for (int i = 0; i < prefetchCount; i++) {
-        if (nextImage != null) {
-          if (nextImage instanceof StreetsideImage && imageCache.get(nextImage.getId()) == null) {
-            CacheUtils.downloadCubemap((StreetsideImage) nextImage);
-          }
-          nextImage = nextImage.next();
-        }
-        if (prevImage != null) {
-          if (prevImage instanceof StreetsideImage && imageCache.get(prevImage.getId()) == null) {
-            CacheUtils.downloadCubemap((StreetsideImage) prevImage);
-          }
-          prevImage = prevImage.previous();
-        }
-      }
-    });
-  }
-
-  /**
-   * Adds an StreetsideImage to the object, and then repaints mapView.
-   *
-   * @param image The image to be added.
-   */
-  public void add(StreetsideAbstractImage image) {
-    add(image, true);
-  }
-
-  /**
-   * Adds a StreetsideImage to the object, but doesn't repaint mapView. This is
-   * needed for concurrency.
-   *
-   * @param image  The image to be added.
-   * @param update Whether the map must be updated or not
-   *         (updates are currently unsupported by Streetside).
-   */
-  public void add(StreetsideAbstractImage image, boolean update) {
-    images.add(image);
-    if (update) {
-      StreetsideLayer.invalidateInstance();
-    }
-    fireImagesAdded();
-  }
-
-  /**
-   * Adds a set of StreetsideImages to the object, and then repaints mapView.
-   *
-   * @param images The set of images to be added.
-   */
-  public void addAll(Collection<? extends StreetsideAbstractImage> images) {
-    addAll(images, true);
-  }
-
-  /**
-   * Adds a set of {link StreetsideAbstractImage} objects to this object.
-   *
-   * @param newImages The set of images to be added.
-   * @param update  Whether the map must be updated or not.
-   */
-  public void addAll(Collection<? extends StreetsideAbstractImage> newImages, boolean update) {
-    images.addAll(newImages);
-    if (update) {
-      StreetsideLayer.invalidateInstance();
-    }
-    fireImagesAdded();
-  }
-
-  /**
-   * Adds a new listener.
-   *
-   * @param lis Listener to be added.
-   */
-  public final void addListener(final StreetsideDataListener lis) {
-    listeners.add(lis);
-  }
-
-  /**
-   * Adds a {@link StreetsideImage} object to the list of selected images, (when
-   * ctrl + click)
-   *
-   * @param image The {@link StreetsideImage} object to be added.
-   */
-  public void addMultiSelectedImage(final StreetsideAbstractImage image) {
-    if (!multiSelectedImages.contains(image)) {
-      if (getSelectedImage() == null) {
-        this.setSelectedImage(image);
-      } else {
-        multiSelectedImages.add(image);
-      }
-    }
-    StreetsideLayer.invalidateInstance();
-  }
-
-  /**
-   * Adds a set of {@code StreetsideAbstractImage} objects to the list of
-   * selected images.
-   *
-   * @param images A {@link Collection} object containing the set of images to be added.
-   */
-  public void addMultiSelectedImage(Collection<StreetsideAbstractImage> images) {
-    images.stream().filter(image -> !multiSelectedImages.contains(image)).forEach(image -> {
-      if (getSelectedImage() == null) {
-        this.setSelectedImage(image);
-      } else {
-        multiSelectedImages.add(image);
-      }
-    });
-    StreetsideLayer.invalidateInstance();
-  }
-
-  public List<Bounds> getBounds() {
-    return bounds;
-  }
-
-  /**
-   * Removes a listener.
-   *
-   * @param lis Listener to be removed.
-   */
-  public void removeListener(StreetsideDataListener lis) {
-    listeners.remove(lis);
-  }
-
-  /**
-   * Returns the image under the mouse cursor.
-   *
-   * @return The image under the mouse cursor.
-   */
-  public StreetsideAbstractImage getHighlightedImage() {
-    return highlightedImage;
-  }
-
-  /**
-   * Highlights the image under the cursor.
-   *
-   * @param image The image under the cursor.
-   */
-  public void setHighlightedImage(StreetsideAbstractImage image) {
-    highlightedImage = image;
-  }
-
-  /**
-   * Returns a Set containing all images.
-   *
-   * @return A Set object containing all images.
-   */
-  public Set<StreetsideAbstractImage> getImages() {
-    return images;
-  }
-
-  /**
-   * Sets a new {@link Collection} object as the used set of images.
-   * Any images that are already present, are removed.
-   *
-   * @param newImages the new image list (previously set images are completely replaced)
-   */
-  public void setImages(Collection<StreetsideAbstractImage> newImages) {
-    synchronized (this) {
-      images.clear();
-      images.addAll(newImages);
-    }
-  }
-
-  /**
-   * Returns a Set of all sequences, that the images are part of.
-   *
-   * @return all sequences that are contained in the Streetside data
-   */
-  public Set<StreetsideSequence> getSequences() {
-    return images.stream().map(StreetsideAbstractImage::getSequence).collect(Collectors.toSet());
-  }
-
-  /**
-   * Returns the StreetsideImage object that is currently selected.
-   *
-   * @return The selected StreetsideImage object.
-   */
-  public StreetsideAbstractImage getSelectedImage() {
-    return selectedImage;
-  }
-
-  /**
-   * Selects a new image.If the user does ctrl + click, this isn't triggered.
-   *
-   * @param image The StreetsideImage which is going to be selected
-   */
-  public void setSelectedImage(StreetsideAbstractImage image) {
-    setSelectedImage(image, false);
-  }
-
-  private void fireImagesAdded() {
-    listeners.stream().filter(Objects::nonNull).forEach(StreetsideDataListener::imagesAdded);
-  }
-
-  /**
-   * If the selected StreetsideImage is part of a StreetsideSequence then the
-   * following visible StreetsideImage is selected. In case there is none, does
-   * nothing.
-   *
-   * @throws IllegalStateException if the selected image is null or the selected image doesn't
-   *                 belong to a sequence.
-   */
-  public void selectNext() {
-    selectNext(StreetsideProperties.MOVE_TO_IMG.get());
-  }
-
-  /**
-   * If the selected StreetsideImage is part of a StreetsideSequence then the
-   * following visible StreetsideImage is selected. In case there is none, does
-   * nothing.
-   *
-   * @param moveToPicture True if the view must me moved to the next picture.
-   * @throws IllegalStateException if the selected image is null or the selected image doesn't
-   *                 belong to a sequence.
-   */
-  public void selectNext(boolean moveToPicture) {
-    StreetsideAbstractImage tempImage = selectedImage;
-    if (selectedImage != null && selectedImage.getSequence() != null) {
-      while (tempImage.next() != null) {
-        tempImage = tempImage.next();
-        if (tempImage.isVisible()) {
-          setSelectedImage(tempImage, moveToPicture);
-          break;
-        }
-      }
-    }
-  }
-
-  /**
-   * If the selected StreetsideImage is part of a StreetsideSequence then the
-   * previous visible StreetsideImage is selected. In case there is none, does
-   * nothing.
-   *
-   * @throws IllegalStateException if the selected image is null or the selected image doesn't
-   *                 belong to a sequence.
-   */
-  public void selectPrevious() {
-    selectPrevious(StreetsideProperties.MOVE_TO_IMG.get());
-  }
-
-  /**
-   * If the selected StreetsideImage is part of a StreetsideSequence then the
-   * previous visible StreetsideImage is selected. In case there is none, does
-   * nothing. * @throws IllegalStateException if the selected image is null or
-   * the selected image doesn't belong to a sequence.
-   *
-   * @param moveToPicture True if the view must me moved to the previous picture.
-   * @throws IllegalStateException if the selected image is null or the selected image doesn't
-   *                 belong to a sequence.
-   */
-  public void selectPrevious(boolean moveToPicture) {
-    if (selectedImage != null && selectedImage.getSequence() != null) {
-      StreetsideAbstractImage tempImage = selectedImage;
-      while (tempImage.previous() != null) {
-        tempImage = tempImage.previous();
-        if (tempImage.isVisible()) {
-          setSelectedImage(tempImage, moveToPicture);
-          break;
-        }
-      }
-    }
-  }
-
-  /**
-   * Selects a new image.If the user does ctrl+click, this isn't triggered. You
-   * can choose whether to center the view on the new image or not.
-   *
-   * @param image The {@link StreetsideImage} which is going to be selected.
-   * @param zoom  True if the view must be centered on the image; false otherwise.
-   */
-  public void setSelectedImage(StreetsideAbstractImage image, boolean zoom) {
-    StreetsideAbstractImage oldImage = selectedImage;
-    selectedImage = image;
-    multiSelectedImages.clear();
-    final MapView mv = StreetsidePlugin.getMapView();
-    if (image != null) {
-      multiSelectedImages.add(image);
-      if (mv != null && image instanceof StreetsideImage) {
-        StreetsideImage streetsideImage = (StreetsideImage) image;
-
-        // Downloading thumbnails of surrounding pictures.
-        downloadSurroundingImages(streetsideImage);
-      }
-    }
-    if (mv != null && zoom && selectedImage != null) {
-      mv.zoomTo(selectedImage.getMovingLatLon());
-    }
-    fireSelectedImageChanged(oldImage, selectedImage);
-    StreetsideLayer.invalidateInstance();
-  }
-
-  private void fireSelectedImageChanged(StreetsideAbstractImage oldImage, StreetsideAbstractImage newImage) {
-    listeners.stream().filter(Objects::nonNull).forEach(lis -> lis.selectedImageChanged(oldImage, newImage));
-  }
-
-  /**
-   * Returns a List containing all {@code StreetsideAbstractImage} objects
-   * selected with ctrl + click.
-   *
-   * @return A List object containing all the images selected.
-   */
-  public Set<StreetsideAbstractImage> getMultiSelectedImages() {
-    return multiSelectedImages;
-  }
-
-  @Override
-  public Collection<DataSource> getDataSources() {
-    return Collections.emptyList();
-  }
+    private final QuadBuckets<StreetsideImage> images = new QuadBuckets<>();
+    private final List<StreetsideImage> sortedImages = new ArrayList<>();
+    /**
+     * Listeners of the class.
+     */
+    private final List<StreetsideDataListener> listeners = new CopyOnWriteArrayList<>();
+    /**
+     * The bounds of the areas for which the pictures have been downloaded.
+     */
+    private final List<Bounds> bounds;
+    /**
+     * The image currently selected, this is the one being shown.
+     */
+    private StreetsideImage selectedImage;
+    /**
+     * The image under the cursor.
+     */
+    private StreetsideImage highlightedImage;
+
+    /**
+     * Creates a new object and adds the initial set of listeners.
+     */
+    protected StreetsideData() {
+        selectedImage = null;
+        bounds = new CopyOnWriteArrayList<>();
+
+        // Adds the basic set of listeners.
+        Arrays.stream(StreetsidePlugin.getStreetsideDataListeners()).forEach(this::addListener);
+        addListener(StreetsideViewerDialog.getInstance().getStreetsideViewerPanel());
+        addListener(StreetsideMainDialog.getInstance());
+        addListener(ImageInfoPanel.getInstance());
+    }
+
+    /**
+     * Downloads surrounding images of this mapillary image in background threads
+     *
+     * @param streetsideImage the image for which the surrounding images should be downloaded
+     */
+    private void downloadSurroundingImages(StreetsideImage streetsideImage) {
+        MainApplication.worker.execute(() -> downloadSurrounding(streetsideImage, CacheUtils::downloadPicture));
+    }
+
+    /**
+     * Downloads surrounding images of this mapillary image in background threads
+     *
+     * @param streetsideImage the image for which the surrounding images should be downloaded
+     */
+    public void downloadSurroundingCubemaps(StreetsideImage streetsideImage) {
+        MainApplication.worker.execute(() -> downloadSurrounding(streetsideImage, CacheUtils::downloadCubemap));
+    }
+
+    private void downloadSurrounding(StreetsideImage streetsideImage, Consumer<StreetsideImage> imageDownloader) {
+        final int prefetchCount = StreetsideProperties.PRE_FETCH_IMAGE_COUNT.get();
+        CacheAccess<String, BufferedImageCacheEntry> imageCache = Caches.ImageCache.getInstance().getCache();
+
+        StreetsideImage nextImage = next(streetsideImage);
+        StreetsideImage prevImage = previous(streetsideImage);
+
+        for (var i = 0; i < prefetchCount; i++) {
+            if (nextImage != null) {
+                if (imageCache.get(nextImage.id()) == null) {
+                    imageDownloader.accept(nextImage);
+                }
+                nextImage = next(nextImage);
+            }
+            if (prevImage != null) {
+                if (imageCache.get(prevImage.id()) == null) {
+                    imageDownloader.accept(prevImage);
+                }
+                prevImage = previous(prevImage);
+            }
+        }
+    }
+
+    /**
+     * Adds an StreetsideImage to the object, and then repaints mapView.
+     *
+     * @param image The image to be added.
+     */
+    public void add(StreetsideImage image) {
+        add(image, true);
+    }
+
+    /**
+     * Adds a StreetsideImage to the object, but doesn't repaint mapView. This is
+     * needed for concurrency.
+     *
+     * @param image  The image to be added.
+     * @param update Whether the map must be updated or not
+     *         (updates are currently unsupported by Streetside).
+     */
+    public void add(StreetsideImage image, boolean update) {
+        if (this.images.contains(image)) {
+            return;
+        }
+        this.images.add(image);
+        if (update) {
+            StreetsideLayer.invalidateInstance();
+        }
+        fireImagesAdded();
+    }
+
+    /**
+     * Adds a set of StreetsideImages to the object, and then repaints mapView.
+     *
+     * @param images The set of images to be added.
+     */
+    public void addAll(Collection<StreetsideImage> images) {
+        addAll(images, true);
+    }
+
+    /**
+     * Adds a set of {link StreetsideAbstractImage} objects to this object.
+     *
+     * @param newImages The set of images to be added.
+     * @param update  Whether the map must be updated or not.
+     */
+    public void addAll(Collection<StreetsideImage> newImages, boolean update) {
+        newImages = new HashSet<>(newImages);
+        newImages.removeIf(this.images::contains);
+        images.addAll(newImages);
+        sortedImages.addAll(newImages);
+        sortedImages.sort(Comparator.naturalOrder());
+        if (update) {
+            StreetsideLayer.invalidateInstance();
+        }
+        fireImagesAdded();
+    }
+
+    /**
+     * Adds a new listener.
+     *
+     * @param lis Listener to be added.
+     */
+    public final void addListener(final StreetsideDataListener lis) {
+        listeners.add(lis);
+    }
+
+    public List<Bounds> getBounds() {
+        return bounds;
+    }
+
+    /**
+     * Removes a listener.
+     *
+     * @param lis Listener to be removed.
+     */
+    public void removeListener(StreetsideDataListener lis) {
+        listeners.remove(lis);
+    }
+
+    /**
+     * Returns the image under the mouse cursor.
+     *
+     * @return The image under the mouse cursor.
+     */
+    public StreetsideImage getHighlightedImage() {
+        return highlightedImage;
+    }
+
+    /**
+     * Highlights the image under the cursor.
+     *
+     * @param image The image under the cursor.
+     */
+    public void setHighlightedImage(StreetsideImage image) {
+        highlightedImage = image;
+    }
+
+    /**
+     * Returns a Set containing all images.
+     *
+     * @return A Set object containing all images.
+     */
+    public Collection<StreetsideImage> getImages() {
+        return images;
+    }
+
+    /**
+     * Sets a new {@link Collection} object as the used set of images.
+     * Any images that are already present, are removed.
+     *
+     * @param newImages the new image list (previously set images are completely replaced)
+     */
+    public void setImages(Collection<StreetsideImage> newImages) {
+        synchronized (this) {
+            this.images.clear();
+            this.sortedImages.clear();
+            this.addAll(newImages);
+        }
+    }
+
+    /**
+     * Returns the StreetsideImage object that is currently selected.
+     *
+     * @return The selected StreetsideImage object.
+     */
+    public StreetsideImage getSelectedImage() {
+        return selectedImage;
+    }
+
+    /**
+     * Selects a new image.If the user does ctrl + click, this isn't triggered.
+     *
+     * @param image The StreetsideImage which is going to be selected
+     */
+    public void setSelectedImage(StreetsideImage image) {
+        setSelectedImage(image, false);
+    }
+
+    private void fireImagesAdded() {
+        listeners.stream().filter(Objects::nonNull).forEach(StreetsideDataListener::imagesAdded);
+    }
+
+    /**
+     * If the selected StreetsideImage is part of a StreetsideSequence then the
+     * following visible StreetsideImage is selected. In case there is none, does
+     * nothing.
+     *
+     * @throws IllegalStateException if the selected image is null or the selected image doesn't
+     *                 belong to a sequence.
+     */
+    public void selectNext() {
+        selectNext(StreetsideProperties.MOVE_TO_IMG.get());
+    }
+
+    /**
+     * If the selected StreetsideImage is part of a StreetsideSequence then the
+     * following visible StreetsideImage is selected. In case there is none, does
+     * nothing.
+     *
+     * @param moveToPicture True if the view must me moved to the next picture.
+     * @throws IllegalStateException if the selected image is null or the selected image doesn't
+     *                 belong to a sequence.
+     */
+    public void selectNext(boolean moveToPicture) {
+        StreetsideImage tempImage = selectedImage;
+        if (selectedImage != null) {
+            while (next(tempImage) != null) {
+                tempImage = next(tempImage);
+                if (tempImage != null && tempImage.visible()) {
+                    setSelectedImage(tempImage, moveToPicture);
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * If the selected StreetsideImage is part of a StreetsideSequence then the
+     * previous visible StreetsideImage is selected. In case there is none, does
+     * nothing.
+     *
+     * @throws IllegalStateException if the selected image is null or the selected image doesn't
+     *                 belong to a sequence.
+     */
+    public void selectPrevious() {
+        selectPrevious(StreetsideProperties.MOVE_TO_IMG.get());
+    }
+
+    /**
+     * If the selected StreetsideImage is part of a StreetsideSequence then the
+     * previous visible StreetsideImage is selected. In case there is none, does
+     * nothing. * @throws IllegalStateException if the selected image is null or
+     * the selected image doesn't belong to a sequence.
+     *
+     * @param moveToPicture True if the view must me moved to the previous picture.
+     * @throws IllegalStateException if the selected image is null or the selected image doesn't
+     *                 belong to a sequence.
+     */
+    public void selectPrevious(boolean moveToPicture) {
+        if (selectedImage != null) {
+            StreetsideImage tempImage = selectedImage;
+            while (previous(tempImage) != null) {
+                tempImage = previous(tempImage);
+                if (tempImage != null && tempImage.visible()) {
+                    setSelectedImage(tempImage, moveToPicture);
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Selects a new image.If the user does ctrl+click, this isn't triggered. You
+     * can choose whether to center the view on the new image or not.
+     *
+     * @param image The {@link StreetsideImage} which is going to be selected.
+     * @param zoom  True if the view must be centered on the image; false otherwise.
+     */
+    public void setSelectedImage(StreetsideImage image, boolean zoom) {
+        StreetsideImage oldImage = selectedImage;
+        selectedImage = image;
+        final var mv = StreetsidePlugin.getMapView();
+        if (image != null && mv != null) {
+            // Downloading thumbnails of surrounding pictures.
+            downloadSurroundingImages(image);
+        }
+        if (mv != null && zoom && selectedImage != null) {
+            mv.zoomTo(selectedImage);
+        }
+        fireSelectedImageChanged(oldImage, selectedImage);
+        StreetsideLayer.invalidateInstance();
+    }
+
+    private void fireSelectedImageChanged(StreetsideImage oldImage, StreetsideImage newImage) {
+        listeners.stream().filter(Objects::nonNull).forEach(lis -> lis.selectedImageChanged(oldImage, newImage));
+    }
+
+    @Override
+    public Collection<DataSource> getDataSources() {
+        return Collections.emptyList();
+    }
+
+    /**
+     * Get the next image
+     * @param current The current image
+     * @return The next image, if available
+     */
+    public StreetsideImage next(StreetsideImage current) {
+        final int currentIndex = sortedImages.indexOf(current);
+        if (currentIndex + 1 >= sortedImages.size()) {
+            return null;
+        }
+        return sortedImages.get(currentIndex + 1);
+    }
+
+    /**
+     * Get the previous image
+     * @param current The current image
+     * @return The previous image, if available
+     */
+    public StreetsideImage previous(StreetsideImage current) {
+        final int currentIndex = sortedImages.indexOf(current);
+        if (currentIndex - 1 < 0) {
+            return null;
+        }
+        return sortedImages.get(currentIndex - 1);
+    }
+
+    /**
+     * Search for images
+     * @param target The image to look around
+     * @param v The {@link LatLon} degrees to search around
+     * @return The images found
+     */
+    public Collection<StreetsideImage> search(StreetsideImage target, double v) {
+        final var searchBox = new BBox(target);
+        searchBox.addLatLon(new LatLon(target.lat(), target.lon()), v);
+        return this.images.search(searchBox);
+    }
+
+    /**
+     * Search for images
+     * @param searchBox The box to search images in
+     * @return The images found
+     */
+    public Collection<StreetsideImage> search(BBox searchBox) {
+        return this.images.search(searchBox);
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideDataListener.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideDataListener.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideDataListener.java	(revision 36228)
@@ -10,16 +10,16 @@
 public interface StreetsideDataListener {
 
-  /**
-   * Fired when any image is added to the database.
-   */
-  void imagesAdded();
+    /**
+     * Fired when any image is added to the database.
+     */
+    void imagesAdded();
 
-  /**
-   * Fired when the selected image is changed by something different from
-   * manually clicking on the icon.
-   *
-   * @param oldImage Old selected {@link StreetsideAbstractImage}
-   * @param newImage New selected {@link StreetsideAbstractImage}
-   */
-  void selectedImageChanged(StreetsideAbstractImage oldImage, StreetsideAbstractImage newImage);
+    /**
+     * Fired when the selected image is changed by something different from
+     * manually clicking on the icon.
+     *
+     * @param oldImage Old selected {@link StreetsideImage}
+     * @param newImage New selected {@link StreetsideImage}
+     */
+    void selectedImageChanged(StreetsideImage oldImage, StreetsideImage newImage);
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideImage.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideImage.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideImage.java	(revision 36228)
@@ -2,286 +2,61 @@
 package org.openstreetmap.josm.plugins.streetside;
 
+import java.time.Instant;
 import java.util.List;
 
-import org.openstreetmap.josm.data.coor.LatLon;
-import org.openstreetmap.josm.plugins.streetside.cubemap.CubemapUtils;
-import org.openstreetmap.josm.plugins.streetside.model.UserProfile;
+import jakarta.annotation.Nonnull;
 
 /**
  * A StreetsideImage object represents each of the images stored in Streetside.
  *
+ * @param id The unique id for the cubemap
+ * @param lat The latitude of the image
+ * @param lon The longitude of the image
+ * @param heading The direction of the images in degrees, meaning 0 north (not yet supported)
+ * @param pitch The pitch of the image
+ * @param roll The roll of the image
+ * @param vintageEnd The date/time that the image was taken
+ * @param vintageStart The date/time that the image was taken
+ * @param logo The logo to show for this image
+ * @param copyright The copyright to show for this image
+ * @param zoomMax The maximum zoom for this image
+ * @param zoomMin The minimum zoom for this image
+ * @param imageHeight The height for this image tiles
+ * @param imageWidth The width for this image tiles
+ * @param imageUrlSubdomains The subdomains for this image
+ *
  * @author nokutu
  * @author renerr18
  *
- * @see StreetsideSequence
  * @see StreetsideData
  */
-public class StreetsideImage extends StreetsideAbstractImage {
-  // latitude of the Streetside image
-  private double la;
-  //longitude of the Streetside image
-  private double lo;
-  // The bubble altitude, in meters above the WGS84 ellipsoid
-  private double al;
-  // Roll
-  private double ro;
-  // Pitch
-  private double pi;
-  // Blurring instructions - not currently used by the plugin
-  private String bl;
-  // Undocumented Attributes
-  private int ml;
-  private List<String> nbn;
-  private List<String> pbn;
-  private int ad;
-  private Rn rn;
+public record StreetsideImage(String id, double lat, double lon, double heading, double pitch, double roll,
+                              Instant vintageStart, Instant vintageEnd, String logo, String copyright,
+                              int zoomMin, int zoomMax, int imageHeight, int imageWidth,
+                              List<String> imageUrlSubdomains) implements StreetsideAbstractImage {
+    public StreetsideImage {
+        if (lat > 90 || lat < -90) throw new IllegalArgumentException("Invalid latitude: " + lat);
+        if (lon > 180 || lon < -180) throw new IllegalArgumentException("Invalid longitude: " + lon);
+        if (pitch > 360 || pitch < -360) throw new IllegalArgumentException("Invalid pitch: " + pitch); // Is this radians or degrees?
+        if (roll > 360 || roll < -360) throw new IllegalArgumentException("Invalid roll: " + roll); // Is this radians or degrees?
+    }
 
-  /**
-   * Main constructor of the class StreetsideImage
-   *
-   * @param id   The unique identifier of the image.
-   * @param latLon The latitude and longitude where it is positioned.
-   * @param he   The direction of the images in degrees, meaning 0 north.
-   */
-  public StreetsideImage(String id, LatLon latLon, double he) {
-    super(id, latLon, he);
-  }
-
-  public StreetsideImage(String id, double la, double lo) {
-    super(id, new LatLon(la, lo), 0.0);
-  }
-
-  public StreetsideImage(String id) {
-    super(id);
-  }
-
-  // Default constructor for Jackson/JSON Deserializattion
-  public StreetsideImage() {
-    super(CubemapUtils.TEST_IMAGE_ID, null, 0.0);
-  }
-
-  /**
-   * Returns the unique identifier of the object.
-   *
-   * @return A {@code String} containing the unique identifier of the object.
-   */
-  @Override
-  public String getId() {
-    return String.valueOf(id);
-  }
-
-  /**
-   * @param id the id to set
-   */
-  @Override
-  public void setId(String id) {
-    this.id = id;
-  }
-
-  public UserProfile getUser() {
-    return getSequence().getUser();
-  }
-
-  @Override
-  public String toString() {
-    return String.format("Image[id=%s,lat=%f,lon=%f,he=%f,user=%s]", id, latLon.lat(), latLon.lon(), he, "null"//, cd
-    );
-  }
-
-  @Override
-  public boolean equals(Object object) {
-    return object instanceof StreetsideImage && id.equals(((StreetsideImage) object).getId());
-  }
-
-  @Override
-  public int compareTo(StreetsideAbstractImage image) {
-    if (image instanceof StreetsideImage) {
-      return id.compareTo(image.getId());
+    @Override
+    public int compareTo(@Nonnull StreetsideAbstractImage o) {
+        if (o instanceof StreetsideImage other) {
+            if (this.vintageStart.compareTo(other.vintageStart) != 0) {
+                return this.vintageStart.compareTo(other.vintageStart);
+            }
+            if (this.vintageEnd.compareTo(other.vintageEnd) != 0) {
+                return this.vintageEnd.compareTo(other.vintageEnd);
+            }
+            if (this.id.compareTo(o.id()) != 0) {
+                return this.id.compareTo(o.id());
+            }
+            // Fine. Fall back to all the doubles.
+            return (int) Math.round((this.lat + this.lon + this.heading + this.pitch + this.roll) -
+                    (other.lat + other.lon + other.heading + other.pitch + other.roll));
+        }
+        return this.hashCode() - o.hashCode();
     }
-    return hashCode() - image.hashCode();
-  }
-
-  @Override
-  public int hashCode() {
-    return id.hashCode();
-  }
-
-  @Override
-  public void stopMoving() {
-    super.stopMoving();
-    checkModified();
-  }
-
-  private void checkModified() {
-    // modifications not currently supported in Streetside
-  }
-
-  @Override
-  public void turn(double ca) {
-    super.turn(ca);
-    checkModified();
-  }
-
-  /**
-   * @return the altitude
-   */
-  public double getAl() {
-    return al;
-  }
-
-  /**
-   * @param altitude the altitude to set
-   */
-  public void setAl(double altitude) {
-    al = altitude;
-  }
-
-  /**
-   * @return the roll
-   */
-  public double getRo() {
-    return ro;
-  }
-
-  /**
-   * @param roll the roll to set
-   */
-  public void setRo(double roll) {
-    ro = roll;
-  }
-
-  /**
-   * @return the pi
-   */
-  public double getPi() {
-    return pi;
-  }
-
-  /**
-   * @param pitch the pi to set
-   */
-  public void setPi(double pitch) {
-    pi = pitch;
-  }
-
-  /**
-   * @return the burringl
-   */
-  public String getBl() {
-    return bl;
-  }
-
-  /**
-   * @param blurring the blurring to set
-   */
-  public void setBl(String blurring) {
-    bl = blurring;
-  }
-
-  /**
-   * @return the ml
-   */
-  public int getMl() {
-    return ml;
-  }
-
-  /**
-   * @param ml the ml to set
-   */
-  public void setMl(int ml) {
-    this.ml = ml;
-  }
-
-  /**
-   * @return the nbn
-   */
-  public List<String> getNbn() {
-    return nbn;
-  }
-
-  /**
-   * @param nbn the nbn to set
-   */
-  public void setNbn(List<String> nbn) {
-    this.nbn = nbn;
-  }
-
-  /**
-   * @return the pbn
-   */
-  public List<String> getPbn() {
-    return pbn;
-  }
-
-  /**
-   * @param pbn the pbn to set
-   */
-  public void setPbn(List<String> pbn) {
-    this.pbn = pbn;
-  }
-
-  /**
-   * @return the ad
-   */
-  public int getAd() {
-    return ad;
-  }
-
-  /**
-   * @param ad the ad to set
-   */
-  public void setAd(int ad) {
-    this.ad = ad;
-  }
-
-  /**
-   * @return the la
-   */
-  public double getLa() {
-    return la;
-  }
-
-  /**
-   * @param la the la to set
-   */
-  public void setLa(double la) {
-    this.la = la;
-  }
-
-  /**
-   * @return the lo
-   */
-  public double getLo() {
-    return lo;
-  }
-
-  /**
-   * @param lo the lo to set
-   */
-  public void setLo(double lo) {
-    this.lo = lo;
-  }
-
-  /**
-   * @return the rn
-   */
-  public Rn getRn() {
-    return rn;
-  }
-
-  /**
-   * @param rn the rn to set
-   */
-  public void setRn(Rn rn) {
-    this.rn = rn;
-  }
-
-  /**
-   * Rn is a Bing Streetside image attribute - currently not
-   * used, mapped or supported in the Streetside plugin -
-   * left out initially because it's an unrequired complex object.
-   */
-  public static class Rn {
-    // placeholder for Rn attribute (undocumented streetside complex inner type)
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideLayer.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideLayer.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideLayer.java	(revision 36228)
@@ -5,8 +5,6 @@
 import java.awt.BasicStroke;
 import java.awt.Color;
-import java.awt.Composite;
 import java.awt.Graphics2D;
 import java.awt.GraphicsEnvironment;
-import java.awt.Point;
 import java.awt.Rectangle;
 import java.awt.RenderingHints;
@@ -15,6 +13,5 @@
 import java.awt.image.BufferedImage;
 import java.util.Comparator;
-import java.util.IntSummaryStatistics;
-import java.util.Optional;
+import java.util.Objects;
 import java.util.logging.Logger;
 
@@ -23,6 +20,4 @@
 
 import org.openstreetmap.josm.data.Bounds;
-import org.openstreetmap.josm.data.coor.ILatLon;
-import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
 import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
@@ -39,5 +34,4 @@
 import org.openstreetmap.josm.plugins.streetside.cache.CacheUtils;
 import org.openstreetmap.josm.plugins.streetside.gui.StreetsideMainDialog;
-import org.openstreetmap.josm.plugins.streetside.history.StreetsideRecord;
 import org.openstreetmap.josm.plugins.streetside.io.download.StreetsideDownloader;
 import org.openstreetmap.josm.plugins.streetside.io.download.StreetsideDownloader.DOWNLOAD_MODE;
@@ -59,444 +53,396 @@
  */
 public final class StreetsideLayer extends AbstractModifiableLayer
-    implements ActiveLayerChangeListener, StreetsideDataListener {
-
-  private static final Logger LOGGER = Logger.getLogger(StreetsideLayer.class.getCanonicalName());
-
-  /**
-   * The radius of the image marker
-   */
-  private static final int IMG_MARKER_RADIUS = 7;
-  /**
-   * The radius of the circular sector that indicates the camera angle
-   */
-  private static final int CA_INDICATOR_RADIUS = 15;
-  /**
-   * The angle of the circular sector that indicates the camera angle
-   */
-  private static final int CA_INDICATOR_ANGLE = 40;
-  /**
-   * Unique instance of the class.
-   */
-  private static StreetsideLayer instance;
-  private static final DataSetListenerAdapter DATASET_LISTENER = new DataSetListenerAdapter(e -> {
-    if (e instanceof DataChangedEvent && StreetsideDownloader.getMode() == DOWNLOAD_MODE.OSM_AREA) {
-      // When more data is downloaded, a delayed update is thrown, in order to
-      // wait for the data bounds to be set.
-      MainApplication.worker.execute(StreetsideDownloader::downloadOSMArea);
-    }
-  });
-  /**
-   * {@link StreetsideData} object that stores the database.
-   */
-  private final StreetsideData data;
-  /**
-   * Mode of the layer.
-   */
-  public AbstractMode mode;
-  /**
-   * The nearest images to the selected image from different sequences sorted by distance from selection.
-   */
-  private StreetsideImage[] nearestImages = {};
-  private volatile TexturePaint hatched;
-
-  private StreetsideLayer() {
-    super(I18n.tr("Microsoft Streetside Images"));
-    data = new StreetsideData();
-    data.addListener(this);
-  }
-
-  public static void invalidateInstance() {
-    if (hasInstance()) {
-      getInstance().invalidate();
-    }
-  }
-
-  private static synchronized void clearInstance() {
-    instance = null;
-  }
-
-  /**
-   * Returns the unique instance of this class.
-   *
-   * @return The unique instance of this class.
-   */
-  public static synchronized StreetsideLayer getInstance() {
-    if (instance != null) {
-      return instance;
-    }
-    final StreetsideLayer layer = new StreetsideLayer();
-    layer.init();
-    instance = layer; // Only set instance field after initialization is complete
-    return instance;
-  }
-
-  /**
-   * @return if the unique instance of this layer is currently instantiated
-   */
-  public static boolean hasInstance() {
-    return instance != null;
-  }
-
-  /**
-   * Initializes the Layer.
-   */
-  private void init() {
-    final DataSet ds = MainApplication.getLayerManager().getEditDataSet();
-    if (ds != null) {
-      ds.addDataSetListener(DATASET_LISTENER);
-    }
-    MainApplication.getLayerManager().addActiveLayerChangeListener(this);
-    if (!GraphicsEnvironment.isHeadless()) {
-      setMode(new SelectMode());
-      if (StreetsideDownloader.getMode() == DOWNLOAD_MODE.OSM_AREA) {
-        MainApplication.worker.execute(StreetsideDownloader::downloadOSMArea);
-      }
-      if (StreetsideDownloader.getMode() == DOWNLOAD_MODE.VISIBLE_AREA) {
-        mode.zoomChanged();
-      }
-    }
-    // Does not execute when in headless mode
-    if (!StreetsideMainDialog.getInstance().isShowing()) {
-      StreetsideMainDialog.getInstance().showDialog();
-    }
-    if (StreetsidePlugin.getMapView() != null) {
-      StreetsideMainDialog.getInstance().streetsideImageDisplay.repaint();
-    }
-    createHatchTexture();
-    invalidate();
-  }
-
-  /**
-   * Changes the mode the the given one.
-   *
-   * @param mode The mode that is going to be activated.
-   */
-  public void setMode(AbstractMode mode) {
-    final MapView mv = StreetsidePlugin.getMapView();
-    if (this.mode != null && mv != null) {
-      mv.removeMouseListener(this.mode);
-      mv.removeMouseMotionListener(this.mode);
-      NavigatableComponent.removeZoomChangeListener(this.mode);
-    }
-    this.mode = mode;
-    if (mode != null && mv != null) {
-      mv.setNewCursor(mode.cursor, this);
-      mv.addMouseListener(mode);
-      mv.addMouseMotionListener(mode);
-      NavigatableComponent.addZoomChangeListener(mode);
-      StreetsideUtils.updateHelpText();
-    }
-  }
-
-  /**
-   * Returns the {@link StreetsideData} object, which acts as the database of the
-   * Layer.
-   *
-   * @return The {@link StreetsideData} object that stores the database.
-   */
-  @Override
-  public StreetsideData getData() {
-    return data;
-  }
-
-  /**
-   * Returns the n-nearest image, for n=1 the nearest one is returned, for n=2 the second nearest one and so on.
-   * The "n-nearest image" is picked from the list of one image from every sequence that is nearest to the currently
-   * selected image, excluding the sequence to which the selected image belongs.
-   *
-   * @param n the index for picking from the list of "nearest images", beginning from 1
-   * @return the n-nearest image to the currently selected image
-   */
-  public synchronized StreetsideImage getNNearestImage(final int n) {
-    return n >= 1 && n <= nearestImages.length ? nearestImages[n - 1] : null;
-  }
-
-  @Override
-  public synchronized void destroy() {
-    clearInstance();
-    setMode(null);
-    StreetsideRecord.getInstance().reset();
-    AbstractMode.resetThread();
-    StreetsideDownloader.stopAll();
-    if (StreetsideMainDialog.hasInstance()) {
-      StreetsideMainDialog.getInstance().setImage(null);
-      StreetsideMainDialog.getInstance().updateImage();
-    }
-    final MapView mv = StreetsidePlugin.getMapView();
-    if (mv != null) {
-      mv.removeMouseListener(mode);
-      mv.removeMouseMotionListener(mode);
-    }
-    try {
-      MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
-      if (MainApplication.getLayerManager().getEditDataSet() != null) {
-        MainApplication.getLayerManager().getEditDataSet().removeDataSetListener(DATASET_LISTENER);
-      }
-    } catch (IllegalArgumentException e) {
-      Logging.trace(e);
-      // TODO: It would be ideal, to fix this properly. But for the moment let's catch this, for when a listener has already been removed.
-    }
-    super.destroy();
-  }
-
-  @Override
-  public boolean isModified() {
-    return data.getImages().parallelStream().anyMatch(StreetsideAbstractImage::isModified);
-  }
-
-  @Override
-  public void setVisible(boolean visible) {
-    super.setVisible(visible);
-    getData().getImages().parallelStream().forEach(img -> img.setVisible(visible));
-  }
-
-  /**
-   * Initialize the hatch pattern used to paint the non-downloaded area.
-   */
-  private void createHatchTexture() {
-    BufferedImage bi = new BufferedImage(15, 15, BufferedImage.TYPE_INT_ARGB);
-    Graphics2D big = bi.createGraphics();
-    big.setColor(StreetsideProperties.BACKGROUND.get());
-    Composite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f);
-    big.setComposite(comp);
-    big.fillRect(0, 0, 15, 15);
-    big.setColor(StreetsideProperties.OUTSIDE_DOWNLOADED_AREA.get());
-    big.drawLine(0, 15, 15, 0);
-    Rectangle r = new Rectangle(0, 0, 15, 15);
-    hatched = new TexturePaint(bi, r);
-  }
-
-  @Override
-  public synchronized void paint(final Graphics2D g, final MapView mv, final Bounds box) {
-    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
-    if (MainApplication.getLayerManager().getActiveLayer() == this) {
-      // paint remainder
-      g.setPaint(hatched);
-      g.fill(MapViewGeometryUtil.getNonDownloadedArea(mv, data.getBounds()));
-    }
-
-    // Draw the blue and red line
-    synchronized (StreetsideLayer.class) {
-      final StreetsideAbstractImage selectedImg = data.getSelectedImage();
-      for (int i = 0; i < nearestImages.length && selectedImg != null; i++) {
-        if (i == 0) {
-          g.setColor(Color.RED);
+        implements ActiveLayerChangeListener, StreetsideDataListener {
+
+    private static final Logger LOGGER = Logger.getLogger(StreetsideLayer.class.getCanonicalName());
+
+    /**
+     * The radius of the image marker
+     */
+    private static final int IMG_MARKER_RADIUS = 7;
+    /**
+     * The radius of the circular sector that indicates the camera angle
+     */
+    private static final int CA_INDICATOR_RADIUS = 15;
+    /**
+     * The angle of the circular sector that indicates the camera angle
+     */
+    private static final int CA_INDICATOR_ANGLE = 40;
+    /**
+     * Unique instance of the class.
+     */
+    private static StreetsideLayer instance;
+    private static final DataSetListenerAdapter DATASET_LISTENER = new DataSetListenerAdapter(e -> {
+        if (e instanceof DataChangedEvent && StreetsideDownloader.getMode() == DOWNLOAD_MODE.OSM_AREA) {
+            // When more data is downloaded, a delayed update is thrown, in order to
+            // wait for the data bounds to be set.
+            MainApplication.worker.execute(StreetsideDownloader::downloadOSMArea);
+        }
+    });
+    /**
+     * {@link StreetsideData} object that stores the database.
+     */
+    private final StreetsideData data;
+    /**
+     * Mode of the layer.
+     */
+    public AbstractMode mode;
+    /**
+     * The nearest images to the selected image from different sequences sorted by distance from selection.
+     */
+    private StreetsideImage[] nearestImages = {};
+    private volatile TexturePaint hatched;
+
+    private StreetsideLayer() {
+        super(I18n.tr("Microsoft Streetside Images"));
+        data = new StreetsideData();
+        data.addListener(this);
+    }
+
+    public static void invalidateInstance() {
+        if (hasInstance()) {
+            getInstance().invalidate();
+        }
+    }
+
+    private static synchronized void clearInstance() {
+        instance = null;
+    }
+
+    /**
+     * Returns the unique instance of this class.
+     *
+     * @return The unique instance of this class.
+     */
+    public static synchronized StreetsideLayer getInstance() {
+        if (instance != null) {
+            return instance;
+        }
+        final var layer = new StreetsideLayer();
+        layer.init();
+        instance = layer; // Only set instance field after initialization is complete
+        return instance;
+    }
+
+    /**
+     * Check if there is a Microsoft Streetside layer
+     * @return if the unique instance of this layer is currently instantiated
+     */
+    public static boolean hasInstance() {
+        return instance != null;
+    }
+
+    /**
+     * Initializes the Layer.
+     */
+    private void init() {
+        final var ds = MainApplication.getLayerManager().getEditDataSet();
+        if (ds != null) {
+            ds.addDataSetListener(DATASET_LISTENER);
+        }
+        MainApplication.getLayerManager().addActiveLayerChangeListener(this);
+        if (!GraphicsEnvironment.isHeadless()) {
+            setMode(new SelectMode());
+            if (StreetsideDownloader.getMode() == DOWNLOAD_MODE.OSM_AREA) {
+                MainApplication.worker.execute(StreetsideDownloader::downloadOSMArea);
+            }
+            if (StreetsideDownloader.getMode() == DOWNLOAD_MODE.VISIBLE_AREA) {
+                mode.zoomChanged();
+            }
+        }
+        // Does not execute when in headless mode
+        if (!StreetsideMainDialog.getInstance().isShowing()) {
+            StreetsideMainDialog.getInstance().showDialog();
+        }
+        if (StreetsidePlugin.getMapView() != null) {
+            StreetsideMainDialog.getInstance().streetsideImageDisplay.repaint();
+        }
+        createHatchTexture();
+        invalidate();
+    }
+
+    /**
+     * Changes the mode the given one.
+     *
+     * @param mode The mode that is going to be activated.
+     */
+    public void setMode(AbstractMode mode) {
+        final var mv = StreetsidePlugin.getMapView();
+        if (this.mode != null && mv != null) {
+            mv.removeMouseListener(this.mode);
+            mv.removeMouseMotionListener(this.mode);
+            NavigatableComponent.removeZoomChangeListener(this.mode);
+        }
+        this.mode = mode;
+        if (mode != null && mv != null) {
+            mv.setNewCursor(mode.cursor, this);
+            mv.addMouseListener(mode);
+            mv.addMouseMotionListener(mode);
+            NavigatableComponent.addZoomChangeListener(mode);
+            StreetsideUtils.updateHelpText();
+        }
+    }
+
+    @Override
+    public boolean isModified() {
+        return false;
+    }
+
+    /**
+     * Returns the {@link StreetsideData} object, which acts as the database of the
+     * Layer.
+     *
+     * @return The {@link StreetsideData} object that stores the database.
+     */
+    @Override
+    public StreetsideData getData() {
+        return data;
+    }
+
+    /**
+     * Returns the n-nearest image, for n=1 the nearest one is returned, for n=2 the second nearest one and so on.
+     * The "n-nearest image" is picked from the list of one image from every sequence that is nearest to the currently
+     * selected image, excluding the sequence to which the selected image belongs.
+     *
+     * @param n the index for picking from the list of "nearest images", beginning from 1
+     * @return the n-nearest image to the currently selected image
+     */
+    public synchronized StreetsideImage getNNearestImage(final int n) {
+        return n >= 1 && n <= nearestImages.length ? nearestImages[n - 1] : null;
+    }
+
+    @Override
+    public synchronized void destroy() {
+        clearInstance();
+        setMode(null);
+        AbstractMode.resetThread();
+        StreetsideDownloader.stopAll();
+        if (StreetsideMainDialog.hasInstance()) {
+            StreetsideMainDialog.getInstance().setImage(null);
+            StreetsideMainDialog.getInstance().updateImage();
+        }
+        final var mv = StreetsidePlugin.getMapView();
+        if (mv != null) {
+            mv.removeMouseListener(mode);
+            mv.removeMouseMotionListener(mode);
+        }
+        try {
+            MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
+            if (MainApplication.getLayerManager().getEditDataSet() != null) {
+                MainApplication.getLayerManager().getEditDataSet().removeDataSetListener(DATASET_LISTENER);
+            }
+        } catch (IllegalArgumentException e) {
+            Logging.trace(e);
+            // TODO: It would be ideal, to fix this properly. But for the moment let's catch this, for when a listener has already been removed.
+        }
+        super.destroy();
+    }
+
+    /**
+     * Initialize the hatch pattern used to paint the non-downloaded area.
+     */
+    private void createHatchTexture() {
+        final var bufferedImage = new BufferedImage(15, 15, BufferedImage.TYPE_INT_ARGB);
+        final var g2d = bufferedImage.createGraphics();
+        g2d.setColor(StreetsideProperties.BACKGROUND.get());
+        final var composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f);
+        g2d.setComposite(composite);
+        g2d.fillRect(0, 0, 15, 15);
+        g2d.setColor(StreetsideProperties.OUTSIDE_DOWNLOADED_AREA.get());
+        g2d.drawLine(0, 15, 15, 0);
+        final var rectangle = new Rectangle(0, 0, 15, 15);
+        hatched = new TexturePaint(bufferedImage, rectangle);
+    }
+
+    @Override
+    public synchronized void paint(final Graphics2D g, final MapView mv, final Bounds box) {
+        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+        if (MainApplication.getLayerManager().getActiveLayer() == this) {
+            // paint remainder
+            g.setPaint(hatched);
+            g.fill(MapViewGeometryUtil.getNonDownloadedArea(mv, data.getBounds()));
+        }
+
+        // Draw the blue and red line
+        synchronized (StreetsideLayer.class) {
+            final var selectedImg = data.getSelectedImage();
+            for (var i = 0; i < nearestImages.length && selectedImg != null; i++) {
+                if (i == 0) {
+                    g.setColor(Color.RED);
+                } else {
+                    g.setColor(Color.BLUE);
+                }
+                final var selected = mv.getPoint(selectedImg);
+                final var point = mv.getPoint(nearestImages[i]);
+                g.draw(new Line2D.Double(point.getX(), point.getY(), selected.getX(), selected.getY()));
+            }
+        }
+
+        for (var imageAbs : data.getImages()) {
+            if (imageAbs.visible() && mv != null && mv.contains(mv.getPoint(imageAbs))) {
+                drawImageMarker(g, imageAbs);
+            }
+        }
+    }
+
+    /**
+     * Draws an image marker onto the given Graphics context.
+     *
+     * @param g   the Graphics context
+     * @param img the image to be drawn onto the Graphics context
+     */
+    private void drawImageMarker(final Graphics2D g, final StreetsideAbstractImage img) {
+        if (img == null || Double.isNaN(img.lat()) || Double.isNaN(img.lon())) {
+            LOGGER.warning("An image is not painted, because it is null or has no LatLon!");
+            return;
+        }
+        final var selectedImg = getData().getSelectedImage();
+        final var point = MainApplication.getMap().mapView.getPoint(img);
+
+        // Determine colors
+        final Color markerC;
+        final Color directionC;
+        if (img.equals(selectedImg)) {
+            markerC = StreetsideColorScheme.SEQ_HIGHLIGHTED;
+            directionC = StreetsideColorScheme.SEQ_HIGHLIGHTED_CA;
         } else {
-          g.setColor(Color.BLUE);
-        }
-        final Point selected = mv.getPoint(selectedImg.getMovingLatLon());
-        final Point p = mv.getPoint(nearestImages[i].getMovingLatLon());
-        g.draw(new Line2D.Double(p.getX(), p.getY(), selected.getX(), selected.getY()));
-      }
-    }
-
-    // TODO: Sequence lines removed because Streetside imagery is organized
-    // such that the images are sorted by the distance from the center of
-    // the bounding box - Redefine sequences?
-
-    // Draw sequence line
-    /*g.setStroke(new BasicStroke(2));
-    final StreetsideAbstractImage selectedImage = getData().getSelectedImage();
-    for (StreetsideSequence seq : getData().getSequences()) {
-      if (seq.getImages().contains(selectedImage)) {
-    g.setColor(
-      seq.getId() == null ? StreetsideColorScheme.SEQ_IMPORTED_SELECTED : StreetsideColorScheme.SEQ_SELECTED
-    );
-      } else {
-    g.setColor(
-      seq.getId() == null ? StreetsideColorScheme.SEQ_IMPORTED_UNSELECTED : StreetsideColorScheme.SEQ_UNSELECTED
-    );
-      }
-      g.draw(MapViewGeometryUtil.getSequencePath(mv, seq));
-    }*/
-    for (StreetsideAbstractImage imageAbs : data.getImages()) {
-      if (imageAbs.isVisible() && mv != null && mv.contains(mv.getPoint(imageAbs.getMovingLatLon()))) {
-        drawImageMarker(g, imageAbs);
-      }
-    }
-  }
-
-  /**
-   * Draws an image marker onto the given Graphics context.
-   *
-   * @param g   the Graphics context
-   * @param img the image to be drawn onto the Graphics context
-   */
-  private void drawImageMarker(final Graphics2D g, final StreetsideAbstractImage img) {
-    if (img == null || img.getLatLon() == null) {
-      LOGGER.warning("An image is not painted, because it is null or has no LatLon!");
-      return;
-    }
-    final StreetsideAbstractImage selectedImg = getData().getSelectedImage();
-    final Point p = MainApplication.getMap().mapView.getPoint(img.getMovingLatLon());
-
-    // Determine colors
-    final Color markerC;
-    final Color directionC;
-    if (selectedImg != null && getData().getMultiSelectedImages().contains(img)) {
-      markerC = StreetsideColorScheme.SEQ_HIGHLIGHTED;
-      directionC = StreetsideColorScheme.SEQ_HIGHLIGHTED_CA;
-    } else if (selectedImg != null && selectedImg.getSequence() != null
-        && selectedImg.getSequence().equals(img.getSequence())) {
-      markerC = StreetsideColorScheme.SEQ_SELECTED;
-      directionC = StreetsideColorScheme.SEQ_SELECTED_CA;
-    } else {
-      markerC = StreetsideColorScheme.SEQ_UNSELECTED;
-      directionC = StreetsideColorScheme.SEQ_UNSELECTED_CA;
-    }
-
-    // Paint direction indicator
-    float alpha = 0.75f;
-    int type = AlphaComposite.SRC_OVER;
-    AlphaComposite composite = AlphaComposite.getInstance(type, alpha);
-    g.setComposite(composite);
-    g.setColor(directionC);
-    g.fillArc(p.x - CA_INDICATOR_RADIUS, p.y - CA_INDICATOR_RADIUS, 2 * CA_INDICATOR_RADIUS,
-        2 * CA_INDICATOR_RADIUS, (int) (90 - /*img.getMovingHe()*/img.getHe() - CA_INDICATOR_ANGLE / 2d),
-        CA_INDICATOR_ANGLE);
-    // Paint image marker
-    g.setColor(markerC);
-    g.fillOval(p.x - IMG_MARKER_RADIUS, p.y - IMG_MARKER_RADIUS, 2 * IMG_MARKER_RADIUS, 2 * IMG_MARKER_RADIUS);
-
-    // Paint highlight for selected or highlighted images
-    if (img.equals(getData().getHighlightedImage()) || getData().getMultiSelectedImages().contains(img)) {
-      g.setColor(Color.WHITE);
-      g.setStroke(new BasicStroke(2));
-      g.drawOval(p.x - IMG_MARKER_RADIUS, p.y - IMG_MARKER_RADIUS, 2 * IMG_MARKER_RADIUS, 2 * IMG_MARKER_RADIUS);
-    }
-  }
-
-  @Override
-  public Icon getIcon() {
-    return StreetsidePlugin.LOGO.setSize(ImageSizes.LAYER).get();
-  }
-
-  @Override
-  public boolean isMergable(Layer other) {
-    return false;
-  }
-
-  @Override
-  public void mergeFrom(Layer from) {
-    throw new UnsupportedOperationException("This layer does not support merging yet");
-  }
-
-  @Override
-  public Action[] getMenuEntries() {
-    return new Action[] { LayerListDialog.getInstance().createShowHideLayerAction(),
-        LayerListDialog.getInstance().createDeleteLayerAction(), new LayerListPopup.InfoAction(this) };
-  }
-
-  @Override
-  public Object getInfoComponent() {
-    IntSummaryStatistics seqSizeStats = getData().getSequences().stream().mapToInt(seq -> seq.getImages().size())
-        .summaryStatistics();
-    return I18n.tr("Streetside layer") + '\n'
-        + I18n.tr("{0} sequences, each containing between {1} and {2} images (ø {3})",
-            getData().getSequences().size(), seqSizeStats.getCount() <= 0 ? 0 : seqSizeStats.getMin(),
-            seqSizeStats.getCount() <= 0 ? 0 : seqSizeStats.getMax(), seqSizeStats.getAverage())
-        + "\n\n" + "\n+ "
-        + I18n.tr("{0} downloaded images",
-            getData().getImages().stream().filter(i -> i instanceof StreetsideImage).count())
-        + "\n= " + I18n.tr("{0} images in total", getData().getImages().size());
-  }
-
-  @Override
-  public String getToolTipText() {
-    return I18n.tr("{0} images in {1} sequences", getData().getImages().size(), getData().getSequences().size());
-  }
-
-  @Override
-  public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
-    if (MainApplication.getLayerManager().getActiveLayer() == this) {
-      StreetsideUtils.updateHelpText();
-    }
-
-    if (MainApplication.getLayerManager().getEditLayer() != e.getPreviousDataLayer()) {
-      if (MainApplication.getLayerManager().getEditLayer() != null) {
-        MainApplication.getLayerManager().getEditLayer().getDataSet().addDataSetListener(DATASET_LISTENER);
-      }
-      if (e.getPreviousDataLayer() != null) {
-        e.getPreviousDataLayer().getDataSet().removeDataSetListener(DATASET_LISTENER);
-      }
-    }
-  }
-
-  @Override
-  public void visitBoundingBox(BoundingXYVisitor v) {
-  }
-
-  /* (non-Javadoc)
-   * @see org.openstreetmap.josm.plugins.streetside.StreetsideDataListener#imagesAdded()
-   */
-  @Override
-  public void imagesAdded() {
-    updateNearestImages();
-  }
-
-  /* (non-Javadoc)
-   * @see org.openstreetmap.josm.plugins.streetside.StreetsideDataListener#selectedImageChanged(org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage, org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage)
-   */
-  @Override
-  public void selectedImageChanged(StreetsideAbstractImage oldImage, StreetsideAbstractImage newImage) {
-    updateNearestImages();
-  }
-
-  /**
-   * Returns the closest images belonging to a different sequence and
-   * different from the specified target image.
-   *
-   * @param target the image for which you want to find the nearest other images
-   * @param limit  the maximum length of the returned array
-   * @return An array containing the closest images belonging to different sequences sorted by distance from target.
-   */
-  private StreetsideImage[] getNearestImagesFromDifferentSequences(StreetsideAbstractImage target, int limit) {
-    return data.getSequences().parallelStream()
-        .filter(seq -> seq.getId() != null && !seq.getId().equals(target.getSequence().getId())).map(seq -> { // Maps sequence to image from sequence that is nearest to target
-          Optional<StreetsideAbstractImage> resImg = seq.getImages().parallelStream()
-              .filter(img -> img instanceof StreetsideImage && img.isVisible())
-              .min(new NearestImgToTargetComparator(target));
-          return resImg.orElse(null);
-        }).filter(img -> // Filters out images too far away from target
-        img != null && img.getMovingLatLon().greatCircleDistance(
-            (ILatLon) target.getMovingLatLon()) < StreetsideProperties.SEQUENCE_MAX_JUMP_DISTANCE.get())
-        .sorted(new NearestImgToTargetComparator(target)).limit(limit).toArray(StreetsideImage[]::new);
-  }
-
-  private synchronized void updateNearestImages() {
-    final StreetsideAbstractImage selected = data.getSelectedImage();
-    if (selected != null) {
-      nearestImages = getNearestImagesFromDifferentSequences(selected, 2);
-    } else {
-      nearestImages = new StreetsideImage[0];
-    }
-    if (MainApplication.isDisplayingMapView()) {
-      StreetsideMainDialog.getInstance().redButton.setEnabled(nearestImages.length >= 1);
-      StreetsideMainDialog.getInstance().blueButton.setEnabled(nearestImages.length >= 2);
-    }
-    if (nearestImages.length >= 1) {
-      CacheUtils.downloadPicture(nearestImages[0]);
-      if (nearestImages.length >= 2) {
-        CacheUtils.downloadPicture(nearestImages[1]);
-      }
-    }
-  }
-
-  private static class NearestImgToTargetComparator implements Comparator<StreetsideAbstractImage> {
-    private final StreetsideAbstractImage target;
-
-    public NearestImgToTargetComparator(StreetsideAbstractImage target) {
-      this.target = target;
+            markerC = StreetsideColorScheme.SEQ_UNSELECTED;
+            directionC = StreetsideColorScheme.SEQ_UNSELECTED_CA;
+        }
+
+        // Paint direction indicator
+        final var alpha = 0.75f;
+        final int type = AlphaComposite.SRC_OVER;
+        final var composite = AlphaComposite.getInstance(type, alpha);
+        g.setComposite(composite);
+        g.setColor(directionC);
+        g.fillArc(point.x - CA_INDICATOR_RADIUS, point.y - CA_INDICATOR_RADIUS, 2 * CA_INDICATOR_RADIUS,
+                2 * CA_INDICATOR_RADIUS, (int) (90 - /*img.getMovingHe()*/img.heading() - CA_INDICATOR_ANGLE / 2d),
+                CA_INDICATOR_ANGLE);
+        // Paint image marker
+        g.setColor(markerC);
+        g.fillOval(point.x - IMG_MARKER_RADIUS, point.y - IMG_MARKER_RADIUS, 2 * IMG_MARKER_RADIUS, 2 * IMG_MARKER_RADIUS);
+
+        // Paint highlight for selected or highlighted images
+        if (img.equals(getData().getHighlightedImage())) {
+            g.setColor(Color.WHITE);
+            g.setStroke(new BasicStroke(2));
+            g.drawOval(point.x - IMG_MARKER_RADIUS, point.y - IMG_MARKER_RADIUS, 2 * IMG_MARKER_RADIUS, 2 * IMG_MARKER_RADIUS);
+        }
+    }
+
+    @Override
+    public Icon getIcon() {
+        return StreetsidePlugin.LOGO.setSize(ImageSizes.LAYER).get();
+    }
+
+    @Override
+    public boolean isMergable(Layer other) {
+        return false;
+    }
+
+    @Override
+    public void mergeFrom(Layer from) {
+        throw new UnsupportedOperationException("This layer does not support merging yet");
+    }
+
+    @Override
+    public Action[] getMenuEntries() {
+        return new Action[] { LayerListDialog.getInstance().createShowHideLayerAction(),
+                LayerListDialog.getInstance().createDeleteLayerAction(), new LayerListPopup.InfoAction(this) };
+    }
+
+    @Override
+    public Object getInfoComponent() {
+        return I18n.tr("Streetside layer") + '\n'
+                + I18n.tr("{0} downloaded images", getData().getImages().stream().filter(Objects::nonNull).count())
+                + "\n= " + I18n.tr("{0} images in total", getData().getImages().size());
+    }
+
+    @Override
+    public String getToolTipText() {
+        return I18n.tr("{0} images", getData().getImages().size());
+    }
+
+    @Override
+    public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
+        if (MainApplication.getLayerManager().getActiveLayer() == this) {
+            StreetsideUtils.updateHelpText();
+        }
+
+        if (MainApplication.getLayerManager().getEditLayer() != e.getPreviousDataLayer()) {
+            if (MainApplication.getLayerManager().getEditLayer() != null) {
+                MainApplication.getLayerManager().getEditLayer().getDataSet().addDataSetListener(DATASET_LISTENER);
+            }
+            if (e.getPreviousDataLayer() != null) {
+                e.getPreviousDataLayer().getDataSet().removeDataSetListener(DATASET_LISTENER);
+            }
+        }
+    }
+
+    @Override
+    public void visitBoundingBox(BoundingXYVisitor v) {
+        // Streetside currently doesn't care about this
     }
 
     /* (non-Javadoc)
-     * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
-     */
-    @Override
-    public int compare(StreetsideAbstractImage img1, StreetsideAbstractImage img2) {
-      return (int) Math.signum(img1.getMovingLatLon().greatCircleDistance((ILatLon) target.getMovingLatLon())
-          - img2.getMovingLatLon().greatCircleDistance((ILatLon) target.getMovingLatLon()));
-    }
-  }
-
+     * @see org.openstreetmap.josm.plugins.streetside.StreetsideDataListener#imagesAdded()
+     */
+    @Override
+    public void imagesAdded() {
+        updateNearestImages();
+    }
+
+    /* (non-Javadoc)
+     * @see org.openstreetmap.josm.plugins.streetside.StreetsideDataListener#selectedImageChanged(
+     *      org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage,
+     *      org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage)
+     */
+    @Override
+    public void selectedImageChanged(StreetsideImage oldImage, StreetsideImage newImage) {
+        updateNearestImages();
+    }
+
+    /**
+     * Returns the closest images belonging to a different sequence and
+     * different from the specified target image.
+     *
+     * @param target the image for which you want to find the nearest other images
+     * @param limit  the maximum length of the returned array
+     * @return An array containing the closest images belonging to different sequences sorted by distance from target.
+     */
+    private StreetsideImage[] getNearestImagesFromDifferentSequences(StreetsideImage target, int limit) {
+        return data.search(target, 0.01).parallelStream().filter(i -> !target.equals(i))
+                // Filters out images too far away from target
+                .filter(img -> img != null
+                        && img.greatCircleDistance(target) < StreetsideProperties.SEQUENCE_MAX_JUMP_DISTANCE.get())
+                .sorted(new NearestImgToTargetComparator(target)).map(StreetsideImage.class::cast).limit(limit)
+                .toArray(StreetsideImage[]::new);
+    }
+
+    private synchronized void updateNearestImages() {
+        final StreetsideImage selected = data.getSelectedImage();
+        if (selected != null) {
+            nearestImages = getNearestImagesFromDifferentSequences(selected, 2);
+        } else {
+            nearestImages = new StreetsideImage[0];
+        }
+        if (MainApplication.isDisplayingMapView()) {
+            StreetsideMainDialog.getInstance().redButton.setEnabled(nearestImages.length >= 1);
+            StreetsideMainDialog.getInstance().blueButton.setEnabled(nearestImages.length >= 2);
+        }
+        if (nearestImages.length >= 1) {
+            CacheUtils.downloadPicture(nearestImages[0]);
+            if (nearestImages.length >= 2) {
+                CacheUtils.downloadPicture(nearestImages[1]);
+            }
+        }
+    }
+
+    private record NearestImgToTargetComparator(StreetsideAbstractImage target) implements Comparator<StreetsideAbstractImage> {
+        @Override
+        public int compare(StreetsideAbstractImage img1, StreetsideAbstractImage img2) {
+            return (int) Math.signum(img1.greatCircleDistance(target) - img2.greatCircleDistance(target));
+        }
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsidePlugin.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsidePlugin.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsidePlugin.java	(revision 36228)
@@ -11,5 +11,4 @@
 import org.openstreetmap.josm.plugins.streetside.actions.StreetsideDownloadAction;
 import org.openstreetmap.josm.plugins.streetside.actions.StreetsideDownloadViewAction;
-import org.openstreetmap.josm.plugins.streetside.actions.StreetsideExportAction;
 import org.openstreetmap.josm.plugins.streetside.actions.StreetsideWalkAction;
 import org.openstreetmap.josm.plugins.streetside.actions.StreetsideZoomAction;
@@ -20,6 +19,4 @@
 import org.openstreetmap.josm.plugins.streetside.gui.imageinfo.ImageInfoHelpPopup;
 import org.openstreetmap.josm.plugins.streetside.gui.imageinfo.ImageInfoPanel;
-import org.openstreetmap.josm.plugins.streetside.oauth.StreetsideUser;
-import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
 import org.openstreetmap.josm.tools.ImageProvider;
 
@@ -29,77 +26,75 @@
 public class StreetsidePlugin extends Plugin {
 
-  public static final ImageProvider LOGO = new ImageProvider("streetside-logo");
+    public static final ImageProvider LOGO = new ImageProvider("streetside-logo");
 
-  /**
-   * Zoom action
-   */
-  private static final StreetsideZoomAction ZOOM_ACTION = new StreetsideZoomAction();
-  /**
-   * Walk action
-   */
-  private static final StreetsideWalkAction WALK_ACTION = new StreetsideWalkAction();
+    /**
+     * Zoom action
+     */
+    private static final StreetsideZoomAction ZOOM_ACTION = new StreetsideZoomAction();
+    /**
+     * Walk action
+     */
+    private static final StreetsideWalkAction WALK_ACTION = new StreetsideWalkAction();
 
-  /**
-   * Main constructor.
-   *
-   * @param info Required information of the plugin. Obtained from the jar file.
-   */
-  public StreetsidePlugin(PluginInformation info) {
-    super(info);
+    /**
+     * Main constructor.
+     *
+     * @param info Required information of the plugin. Obtained from the jar file.
+     */
+    public StreetsidePlugin(PluginInformation info) {
+        super(info);
 
-    if (StreetsideProperties.ACCESS_TOKEN.get() == null) {
-      StreetsideUser.setTokenValid(false);
+        MainMenu.add(MainApplication.getMenu().imagerySubMenu, new StreetsideDownloadAction(), false);
+        MainMenu.add(MainApplication.getMenu().viewMenu, ZOOM_ACTION, false, 15);
+        MainMenu.add(MainApplication.getMenu().fileMenu, new StreetsideDownloadViewAction(), false, 14);
+        MainMenu.add(MainApplication.getMenu().moreToolsMenu, WALK_ACTION, false);
     }
-    MainMenu.add(MainApplication.getMenu().fileMenu, new StreetsideExportAction(), false, 14);
-    MainMenu.add(MainApplication.getMenu().imagerySubMenu, new StreetsideDownloadAction(), false);
-    MainMenu.add(MainApplication.getMenu().viewMenu, ZOOM_ACTION, false, 15);
-    MainMenu.add(MainApplication.getMenu().fileMenu, new StreetsideDownloadViewAction(), false, 14);
-    MainMenu.add(MainApplication.getMenu().moreToolsMenu, WALK_ACTION, false);
-  }
 
-  static StreetsideDataListener[] getStreetsideDataListeners() {
-    return new StreetsideDataListener[] { WALK_ACTION, ZOOM_ACTION, CubemapBuilder.getInstance() };
-  }
+    static StreetsideDataListener[] getStreetsideDataListeners() {
+        return new StreetsideDataListener[] { WALK_ACTION, ZOOM_ACTION, CubemapBuilder.getInstance() };
+    }
 
-  /**
-   * @return the {@link StreetsideWalkAction} for the plugin
-   */
-  public static StreetsideWalkAction getStreetsideWalkAction() {
-    return WALK_ACTION;
-  }
+    /**
+     * Get the walk action for the plugin
+     * @return the {@link StreetsideWalkAction} for the plugin
+     */
+    public static StreetsideWalkAction getStreetsideWalkAction() {
+        return WALK_ACTION;
+    }
 
-  /**
-   * @return the current {@link MapView} without throwing a {@link NullPointerException}
-   */
-  public static MapView getMapView() {
-    final MapFrame mf = MainApplication.getMap();
-    if (mf != null) {
-      return mf.mapView;
+    /**
+     * Get the current mapview
+     * @return the current {@link MapView} without throwing a {@link NullPointerException}
+     */
+    public static MapView getMapView() {
+        final MapFrame mf = MainApplication.getMap();
+        if (mf != null) {
+            return mf.mapView;
+        }
+        return null;
     }
-    return null;
-  }
 
-  /**
-   * Called when the JOSM map frame is created or destroyed.
-   */
-  @Override
-  public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
-    if (oldFrame == null && newFrame != null) { // map frame added
-      MainApplication.getMap().addToggleDialog(StreetsideMainDialog.getInstance(), false);
-      StreetsideMainDialog.getInstance().setImageInfoHelp(new ImageInfoHelpPopup(
-          MainApplication.getMap().addToggleDialog(ImageInfoPanel.getInstance(), false)));
-      MainApplication.getMap().addToggleDialog(StreetsideViewerDialog.getInstance(), false);
+    /**
+     * Called when the JOSM map frame is created or destroyed.
+     */
+    @Override
+    public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
+        if (oldFrame == null && newFrame != null) { // map frame added
+            MainApplication.getMap().addToggleDialog(StreetsideMainDialog.getInstance(), false);
+            StreetsideMainDialog.getInstance().setImageInfoHelp(new ImageInfoHelpPopup(
+                    MainApplication.getMap().addToggleDialog(ImageInfoPanel.getInstance(), false)));
+            MainApplication.getMap().addToggleDialog(StreetsideViewerDialog.getInstance(), false);
+        }
+        if (oldFrame != null && newFrame == null) { // map frame destroyed
+            StreetsideMainDialog.destroyInstance();
+            ImageInfoPanel.destroyInstance();
+            CubemapBuilder.destroyInstance();
+            StreetsideViewerDialog.destroyInstance();
+        }
     }
-    if (oldFrame != null && newFrame == null) { // map frame destroyed
-      StreetsideMainDialog.destroyInstance();
-      ImageInfoPanel.destroyInstance();
-      CubemapBuilder.destroyInstance();
-      StreetsideViewerDialog.destroyInstance();
+
+    @Override
+    public PreferenceSetting getPreferenceSetting() {
+        return new StreetsidePreferenceSetting();
     }
-  }
-
-  @Override
-  public PreferenceSetting getPreferenceSetting() {
-    return new StreetsidePreferenceSetting();
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideSequence.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/StreetsideSequence.java	(revision 36194)
+++ 	(revision )
@@ -1,211 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.streetside;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-import org.openstreetmap.josm.plugins.streetside.model.UserProfile;
-
-/**
- * Class that stores a sequence of {@link StreetsideAbstractImage} objects.
- *
- * @author nokutu
- * @see StreetsideAbstractImage
- */
-
-public class StreetsideSequence {
-
-  /**
-   * The images in the sequence.
-   */
-  private final List<StreetsideAbstractImage> images;
-  /**
-   * Unique identifier. Used only for {@link StreetsideImage} sequences.
-   */
-  private String id;
-  private UserProfile user;
-  private double la;
-  private double lo;
-  /**
-   * Epoch time when the sequence was created
-   */
-  private long cd;
-
-  public StreetsideSequence(String id, Long ca) {
-    this.id = id;
-    cd = ca;
-    images = new CopyOnWriteArrayList<>();
-  }
-
-  public StreetsideSequence(String id, double la, double lo) {
-    this.id = id;
-    this.la = la;
-    this.lo = lo;
-    images = new CopyOnWriteArrayList<>();
-  }
-
-  /**
-   * No argument constructor for StreetsideSequence - necessary for JSON serialization
-   */
-  public StreetsideSequence() {
-    images = new CopyOnWriteArrayList<>();
-  }
-
-  public StreetsideSequence(String id, double la, double lo, long ca) {
-    this.id = id;
-    this.la = la;
-    this.lo = lo;
-    cd = ca;
-    images = new CopyOnWriteArrayList<>();
-  }
-
-  public StreetsideSequence(String id) {
-    this.id = id;
-    images = new CopyOnWriteArrayList<>();
-  }
-
-  /**
-   * Adds a new {@link StreetsideAbstractImage} object to the database.
-   *
-   * @param image The {@link StreetsideAbstractImage} object to be added
-   */
-  public synchronized void add(StreetsideAbstractImage image) {
-    images.add(image);
-    image.setSequence(this);
-  }
-
-  /**
-   * Adds a set of {@link StreetsideAbstractImage} objects to the database.
-   *
-   * @param images The set of {@link StreetsideAbstractImage} objects to be added.
-   */
-  public synchronized void add(final Collection<? extends StreetsideAbstractImage> images) {
-    this.images.addAll(images);
-    images.forEach(img -> img.setSequence(this));
-  }
-
-  /**
-   * Returns the next {@link StreetsideAbstractImage} in the sequence of a given
-   * {@link StreetsideAbstractImage} object.
-   *
-   * @param image The {@link StreetsideAbstractImage} object whose next image is
-   *        going to be returned.
-   * @return The next {@link StreetsideAbstractImage} object in the sequence.
-   * @throws IllegalArgumentException if the given {@link StreetsideAbstractImage} object doesn't belong
-   *                  in this sequence.
-   */
-  public StreetsideAbstractImage next(StreetsideAbstractImage image) {
-    int i = images.indexOf(image);
-    if (i == -1) {
-      throw new IllegalArgumentException();
-    }
-    if (i == images.size() - 1) {
-      return null;
-    }
-    return images.get(i + 1);
-  }
-
-  /**
-   * Returns the previous {@link StreetsideAbstractImage} in the sequence of a
-   * given {@link StreetsideAbstractImage} object.
-   *
-   * @param image The {@link StreetsideAbstractImage} object whose previous image is
-   *        going to be returned.
-   * @return The previous {@link StreetsideAbstractImage} object in the sequence.
-   * @throws IllegalArgumentException if the given {@link StreetsideAbstractImage} object doesn't belong
-   *                  the this sequence.
-   */
-  public StreetsideAbstractImage previous(StreetsideAbstractImage image) {
-    int i = images.indexOf(image);
-    if (i < 0) {
-      throw new IllegalArgumentException();
-    }
-    if (i == 0) {
-      return null;
-    }
-    return images.get(i - 1);
-  }
-
-  /**
-   * Removes a {@link StreetsideAbstractImage} object from the database.
-   *
-   * @param image The {@link StreetsideAbstractImage} object to be removed.
-   */
-  public void remove(StreetsideAbstractImage image) {
-    images.remove(image);
-  }
-
-  /**
-   * @return the la
-   */
-  public double getLa() {
-    return la;
-  }
-
-  /**
-   * @param la the la to set
-   */
-  public void setLa(double la) {
-    this.la = la;
-  }
-
-  /**
-   * @return the lo
-   */
-  public double getLo() {
-    return lo;
-  }
-
-  /**
-   * @param lo the lo to set
-   */
-  public void setLo(double lo) {
-    this.lo = lo;
-  }
-
-  /**
-   * Returns the Epoch time when the sequence was captured.
-   *
-   * Negative values mean, no value is set.
-   *
-   * @return A long containing the Epoch time when the sequence was captured.
-   */
-  public long getCd() {
-    return cd;
-  }
-
-  /**
-   * Returns all {@link StreetsideAbstractImage} objects contained by this
-   * object.
-   *
-   * @return A {@link List} object containing all the
-   * {@link StreetsideAbstractImage} objects that are part of the
-   * sequence.
-   */
-  public List<StreetsideAbstractImage> getImages() {
-    return images;
-  }
-
-  /**
-   * Returns the unique identifier of the sequence.
-   *
-   * @return A {@code String} containing the unique identifier of the sequence.
-   * null means that the sequence has been created locally for imported
-   * images.
-   */
-  public String getId() {
-    return id;
-  }
-
-  /**
-   * @param id the id to set
-   */
-  public void setId(String id) {
-    this.id = id;
-  }
-
-  public UserProfile getUser() {
-    return user;
-  }
-}
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/ImageReloadAction.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/ImageReloadAction.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/ImageReloadAction.java	(revision 36228)
@@ -3,4 +3,5 @@
 
 import java.awt.event.ActionEvent;
+import java.io.Serial;
 
 import javax.swing.AbstractAction;
@@ -13,15 +14,16 @@
 public class ImageReloadAction extends AbstractAction {
 
-  private static final long serialVersionUID = 7987479726049238315L;
+    @Serial
+    private static final long serialVersionUID = 7987479726049238315L;
 
-  public ImageReloadAction(final String name) {
-    super(name, ImageProvider.get("reload", ImageSizes.SMALLICON));
-  }
+    public ImageReloadAction(final String name) {
+        super(name, ImageProvider.get("reload", ImageSizes.SMALLICON));
+    }
 
-  @Override
-  public void actionPerformed(ActionEvent arg0) {
-    if (StreetsideMainDialog.getInstance().getImage() != null) {
-      CubemapBuilder.getInstance().reload(CubemapBuilder.getInstance().getCubemap().getId());
+    @Override
+    public void actionPerformed(ActionEvent arg0) {
+        if (StreetsideMainDialog.getInstance().getImage() != null) {
+            CubemapBuilder.getInstance().reload(CubemapBuilder.getInstance().getCubemap());
+        }
     }
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/StreetsideDownloadAction.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/StreetsideDownloadAction.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/StreetsideDownloadAction.java	(revision 36228)
@@ -6,4 +6,5 @@
 import java.awt.event.ActionEvent;
 import java.awt.event.KeyEvent;
+import java.io.Serial;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -27,37 +28,38 @@
 public class StreetsideDownloadAction extends JosmAction {
 
-  public static final Shortcut SHORTCUT = Shortcut.registerShortcut("Streetside", "Open Streetside layer",
-      KeyEvent.VK_COMMA, Shortcut.SHIFT);
-  private static final long serialVersionUID = 4426446157849005029L;
-  private static final Logger LOGGER = Logger.getLogger(StreetsideDownloadAction.class.getCanonicalName());
+    public static final Shortcut SHORTCUT = Shortcut.registerShortcut("Streetside", "Open Streetside layer",
+            KeyEvent.VK_COMMA, Shortcut.SHIFT);
+    @Serial
+    private static final long serialVersionUID = 4426446157849005029L;
+    private static final Logger LOGGER = Logger.getLogger(StreetsideDownloadAction.class.getCanonicalName());
 
-  /**
-   * Main constructor.
-   */
-  public StreetsideDownloadAction() {
-    super(tr("Streetside"), new ImageProvider(StreetsidePlugin.LOGO).setSize(ImageSizes.DEFAULT),
-        tr("Open Streetside layer"), SHORTCUT, false, "streetsideDownload", false);
-  }
-
-  @Override
-  public void actionPerformed(ActionEvent ae) {
-    if (!StreetsideLayer.hasInstance()
-        || !MainApplication.getLayerManager().containsLayer(StreetsideLayer.getInstance())) {
-      MainApplication.getLayerManager().addLayer(StreetsideLayer.getInstance());
-      return;
+    /**
+     * Main constructor.
+     */
+    public StreetsideDownloadAction() {
+        super(tr("Streetside"), new ImageProvider(StreetsidePlugin.LOGO).setSize(ImageSizes.DEFAULT),
+                tr("Open Streetside layer"), SHORTCUT, false, "streetsideDownload", false);
     }
 
-    try {
-      // Successive calls to this action toggle the active layer between the OSM data layer and the streetside layer
-      OsmDataLayer editLayer = MainApplication.getLayerManager().getEditLayer();
-      if (MainApplication.getLayerManager().getActiveLayer() != StreetsideLayer.getInstance()) {
-        MainApplication.getLayerManager().setActiveLayer(StreetsideLayer.getInstance());
-      } else if (editLayer != null) {
-        MainApplication.getLayerManager().setActiveLayer(editLayer);
-      }
-    } catch (IllegalArgumentException e) {
-      // If the StreetsideLayer is not managed by LayerManager but you try to set it as active layer
-      LOGGER.log(Level.WARNING, e.getMessage(), e);
+    @Override
+    public void actionPerformed(ActionEvent ae) {
+        if (!StreetsideLayer.hasInstance()
+                || !MainApplication.getLayerManager().containsLayer(StreetsideLayer.getInstance())) {
+            MainApplication.getLayerManager().addLayer(StreetsideLayer.getInstance());
+            return;
+        }
+
+        try {
+            // Successive calls to this action toggle the active layer between the OSM data layer and the streetside layer
+            OsmDataLayer editLayer = MainApplication.getLayerManager().getEditLayer();
+            if (MainApplication.getLayerManager().getActiveLayer() != StreetsideLayer.getInstance()) {
+                MainApplication.getLayerManager().setActiveLayer(StreetsideLayer.getInstance());
+            } else if (editLayer != null) {
+                MainApplication.getLayerManager().setActiveLayer(editLayer);
+            }
+        } catch (IllegalArgumentException e) {
+            // If the StreetsideLayer is not managed by LayerManager, but you try to set it as active layer
+            LOGGER.log(Level.WARNING, e.getMessage(), e);
+        }
     }
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/StreetsideDownloadViewAction.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/StreetsideDownloadViewAction.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/StreetsideDownloadViewAction.java	(revision 36228)
@@ -4,4 +4,5 @@
 import java.awt.event.ActionEvent;
 import java.awt.event.KeyEvent;
+import java.io.Serial;
 
 import org.openstreetmap.josm.actions.JosmAction;
@@ -25,44 +26,45 @@
 public class StreetsideDownloadViewAction extends JosmAction implements ValueChangeListener<String> {
 
-  private static final long serialVersionUID = 6738276777802831669L;
+    @Serial
+    private static final long serialVersionUID = 6738276777802831669L;
 
-  private static final String DESCRIPTION = I18n.marktr("Download Streetside images in current view");
+    private static final String DESCRIPTION = I18n.marktr("Download Streetside images in current view");
 
-  /**
-   * Main constructor.
-   */
-  public StreetsideDownloadViewAction() {
-    super(I18n.tr(DESCRIPTION), new ImageProvider(StreetsidePlugin.LOGO).setSize(ImageSizes.DEFAULT),
-        I18n.tr(DESCRIPTION),
-        Shortcut.registerShortcut("Streetside area", I18n.tr(DESCRIPTION), KeyEvent.VK_PERIOD, Shortcut.SHIFT),
-        false, "streetsideArea", true);
-    StreetsideProperties.DOWNLOAD_MODE.addListener(this);
-    initEnabledState();
-  }
+    /**
+     * Main constructor.
+     */
+    public StreetsideDownloadViewAction() {
+        super(I18n.tr(DESCRIPTION), new ImageProvider(StreetsidePlugin.LOGO).setSize(ImageSizes.DEFAULT),
+                I18n.tr(DESCRIPTION),
+                Shortcut.registerShortcut("Streetside area", I18n.tr(DESCRIPTION), KeyEvent.VK_PERIOD, Shortcut.SHIFT),
+                false, "streetsideArea", true);
+        StreetsideProperties.DOWNLOAD_MODE.addListener(this);
+        initEnabledState();
+    }
 
-  @Override
-  public void actionPerformed(ActionEvent arg0) {
-    StreetsideDownloader.downloadVisibleArea();
-  }
+    @Override
+    public void actionPerformed(ActionEvent arg0) {
+        StreetsideDownloader.downloadVisibleArea();
+    }
 
-  @Override
-  protected boolean listenToSelectionChange() {
-    return false;
-  }
+    @Override
+    protected boolean listenToSelectionChange() {
+        return false;
+    }
 
-  /**
-   * Enabled when the Streetside layer is instantiated and download mode is either "osm area" or "manual".
-   */
-  @Override
-  protected void updateEnabledState() {
-    super.updateEnabledState();
-    setEnabled(StreetsideLayer.hasInstance()
-        && (StreetsideDownloader.getMode() == StreetsideDownloader.DOWNLOAD_MODE.OSM_AREA
-            || StreetsideDownloader.getMode() == StreetsideDownloader.DOWNLOAD_MODE.MANUAL_ONLY));
-  }
+    /**
+     * Enabled when the Streetside layer is instantiated and download mode is either "osm area" or "manual".
+     */
+    @Override
+    protected void updateEnabledState() {
+        super.updateEnabledState();
+        setEnabled(StreetsideLayer.hasInstance()
+                && (StreetsideDownloader.getMode() == StreetsideDownloader.DOWNLOAD_MODE.OSM_AREA
+                        || StreetsideDownloader.getMode() == StreetsideDownloader.DOWNLOAD_MODE.MANUAL_ONLY));
+    }
 
-  @Override
-  public void valueChanged(ValueChangeEvent<? extends String> e) {
-    updateEnabledState();
-  }
+    @Override
+    public void valueChanged(ValueChangeEvent<? extends String> e) {
+        updateEnabledState();
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/StreetsideExportAction.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/StreetsideExportAction.java	(revision 36194)
+++ 	(revision )
@@ -1,114 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.streetside.actions;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.awt.Dimension;
-import java.awt.event.ActionEvent;
-import java.awt.event.KeyEvent;
-import java.util.Set;
-import java.util.concurrent.ConcurrentSkipListSet;
-
-import javax.swing.JButton;
-import javax.swing.JDialog;
-import javax.swing.JOptionPane;
-
-import org.openstreetmap.josm.actions.JosmAction;
-import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
-import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
-import org.openstreetmap.josm.plugins.streetside.StreetsideLayer;
-import org.openstreetmap.josm.plugins.streetside.StreetsidePlugin;
-import org.openstreetmap.josm.plugins.streetside.gui.StreetsideExportDialog;
-import org.openstreetmap.josm.plugins.streetside.io.export.StreetsideExportManager;
-import org.openstreetmap.josm.tools.ImageProvider;
-import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
-import org.openstreetmap.josm.tools.Shortcut;
-
-/**
- * Action that launches a StreetsideExportDialog and lets you export the images.
- *
- * @author nokutu
- *
- */
-public class StreetsideExportAction extends JosmAction {
-
-  private static final long serialVersionUID = 6131359489725632369L;
-
-  private StreetsideExportDialog dialog;
-
-  /**
-   * Main constructor.
-   */
-  public StreetsideExportAction() {
-    super(tr("Export Streetside images"), new ImageProvider(StreetsidePlugin.LOGO).setSize(ImageSizes.DEFAULT),
-        tr("Export Streetside images"), Shortcut.registerShortcut("Export Streetside",
-            tr("Export Streetside images"), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE),
-        false, "streetsideExport", true);
-    setEnabled(false);
-  }
-
-  @Override
-  public void actionPerformed(ActionEvent ae) {
-    JOptionPane pane = new JOptionPane();
-
-    JButton ok = new JButton("Ok");
-    ok.addActionListener(e -> pane.setValue(JOptionPane.OK_OPTION));
-    JButton cancel = new JButton(tr("Cancel"));
-    cancel.addActionListener(e -> pane.setValue(JOptionPane.CANCEL_OPTION));
-
-    dialog = new StreetsideExportDialog(ok);
-    pane.setMessage(dialog);
-    pane.setOptions(new JButton[] { ok, cancel });
-
-    JDialog dlg = pane.createDialog(MainApplication.getMainFrame(), tr("Export Streetside images"));
-    dlg.setMinimumSize(new Dimension(400, 150));
-    dlg.setVisible(true);
-
-    // Checks if the inputs are correct and starts the export process.
-    if (pane.getValue() != null && (int) pane.getValue() == JOptionPane.OK_OPTION && dialog.chooser != null) {
-      if (dialog.group.isSelected(dialog.all.getModel())) {
-        export(StreetsideLayer.getInstance().getData().getImages());
-      } else if (dialog.group.isSelected(dialog.sequence.getModel())) {
-        Set<StreetsideAbstractImage> images = new ConcurrentSkipListSet<>();
-        for (StreetsideAbstractImage image : StreetsideLayer.getInstance().getData().getMultiSelectedImages()) {
-          if (image instanceof StreetsideImage) {
-            if (!images.contains(image)) {
-              images.addAll(image.getSequence().getImages());
-            }
-          } else {
-            images.add(image);
-          }
-        }
-        export(images);
-      } else if (dialog.group.isSelected(dialog.selected.getModel())) {
-        export(StreetsideLayer.getInstance().getData().getMultiSelectedImages());
-      }
-    }
-    dlg.dispose();
-  }
-
-  /**
-   * Exports the given images from the database.
-   *
-   * @param images The set of images to be exported.
-   */
-  public void export(Set<StreetsideAbstractImage> images) {
-    MainApplication.worker
-        .execute(new StreetsideExportManager(images, dialog.chooser.getSelectedFile().toString()));
-  }
-
-  @Override
-  protected boolean listenToSelectionChange() {
-    return false;
-  }
-
-  /**
-   * Enabled when streetside layer is in layer list
-   */
-  @Override
-  protected void updateEnabledState() {
-    super.updateEnabledState();
-    setEnabled(StreetsideLayer.hasInstance());
-  }
-}
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/StreetsideWalkAction.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/StreetsideWalkAction.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/StreetsideWalkAction.java	(revision 36228)
@@ -6,4 +6,5 @@
 import java.awt.Dimension;
 import java.awt.event.ActionEvent;
+import java.io.Serial;
 import java.util.ArrayList;
 import java.util.List;
@@ -14,6 +15,6 @@
 import org.openstreetmap.josm.actions.JosmAction;
 import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
 import org.openstreetmap.josm.plugins.streetside.StreetsideDataListener;
+import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
 import org.openstreetmap.josm.plugins.streetside.StreetsideLayer;
 import org.openstreetmap.josm.plugins.streetside.StreetsidePlugin;
@@ -31,87 +32,88 @@
 public class StreetsideWalkAction extends JosmAction implements StreetsideDataListener {
 
-  private static final long serialVersionUID = 3454223919402245818L;
-  private final List<WalkListener> listeners = new ArrayList<>();
-  private WalkThread thread;
+    @Serial
+    private static final long serialVersionUID = 3454223919402245818L;
+    private final List<WalkListener> listeners = new ArrayList<>();
+    private WalkThread thread;
 
-  /**
-   *
-   */
-  public StreetsideWalkAction() {
-    super(tr("Walk mode"), new ImageProvider(StreetsidePlugin.LOGO).setSize(ImageSizes.DEFAULT), tr("Walk mode"),
-        null, false, "streetsideWalk", true);
-  }
+    /**
+     * Automatically go through images
+     */
+    public StreetsideWalkAction() {
+        super(tr("Walk mode"), new ImageProvider(StreetsidePlugin.LOGO).setSize(ImageSizes.DEFAULT).setOptional(true), tr("Walk mode"),
+                null, false, "streetsideWalk", true);
+    }
 
-  @Override
-  public void actionPerformed(ActionEvent arg0) {
-    StreetsideWalkDialog dialog = new StreetsideWalkDialog();
-    JOptionPane pane = new JOptionPane(dialog, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION);
-    JDialog dlg = pane.createDialog(MainApplication.getMainFrame(), tr("Walk mode"));
-    dlg.setMinimumSize(new Dimension(400, 150));
-    dlg.setVisible(true);
-    if (pane.getValue() != null && (int) pane.getValue() == JOptionPane.OK_OPTION) {
-      thread = new WalkThread((int) dialog.spin.getValue(), dialog.waitForPicture.isSelected(),
-          dialog.followSelection.isSelected(), dialog.goForward.isSelected());
-      fireWalkStarted();
-      thread.start();
-      StreetsideMainDialog.getInstance().setMode(StreetsideMainDialog.MODE.WALK);
+    @Override
+    public void actionPerformed(ActionEvent arg0) {
+        StreetsideWalkDialog dialog = new StreetsideWalkDialog();
+        JOptionPane pane = new JOptionPane(dialog, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION);
+        JDialog dlg = pane.createDialog(MainApplication.getMainFrame(), tr("Walk mode"));
+        dlg.setMinimumSize(new Dimension(400, 150));
+        dlg.setVisible(true);
+        if (pane.getValue() != null && (int) pane.getValue() == JOptionPane.OK_OPTION) {
+            thread = new WalkThread((int) dialog.spin.getValue(), dialog.waitForPicture.isSelected(),
+                    dialog.followSelection.isSelected(), dialog.goForward.isSelected());
+            fireWalkStarted();
+            thread.start();
+            StreetsideMainDialog.getInstance().setMode(StreetsideMainDialog.MODE.WALK);
+        }
     }
-  }
 
-  @Override
-  public void imagesAdded() {
-    // Nothing
-  }
+    @Override
+    public void imagesAdded() {
+        // Nothing
+    }
 
-  /**
-   * Adds a listener.
-   *
-   * @param lis The listener to be added.
-   */
-  public void addListener(WalkListener lis) {
-    listeners.add(lis);
-  }
+    /**
+     * Adds a listener.
+     *
+     * @param lis The listener to be added.
+     */
+    public void addListener(WalkListener lis) {
+        listeners.add(lis);
+    }
 
-  /**
-   * Removes a listener.
-   *
-   * @param lis
-   *      The listener to be added.
-   */
-  public void removeListener(WalkListener lis) {
-    listeners.remove(lis);
-  }
+    /**
+     * Removes a listener.
+     *
+     * @param lis
+     *      The listener to be added.
+     */
+    public void removeListener(WalkListener lis) {
+        listeners.remove(lis);
+    }
 
-  private void fireWalkStarted() {
-    if (listeners.isEmpty()) {
-      return;
+    private void fireWalkStarted() {
+        if (listeners.isEmpty()) {
+            return;
+        }
+        for (WalkListener lis : listeners) {
+            lis.walkStarted(thread);
+        }
     }
-    for (WalkListener lis : listeners) {
-      lis.walkStarted(thread);
+
+    @Override
+    protected boolean listenToSelectionChange() {
+        return false;
     }
-  }
 
-  @Override
-  protected boolean listenToSelectionChange() {
-    return false;
-  }
+    @Override
+    public void selectedImageChanged(StreetsideImage oldImage, StreetsideImage newImage) {
+        if (oldImage == null && newImage != null) {
+            setEnabled(true);
+        } else if (oldImage != null && newImage == null) {
+            setEnabled(false);
+        }
+    }
 
-  @Override
-  public void selectedImageChanged(StreetsideAbstractImage oldImage, StreetsideAbstractImage newImage) {
-    if (oldImage == null && newImage != null) {
-      setEnabled(true);
-    } else if (oldImage != null && newImage == null) {
-      setEnabled(false);
+    /**
+     * Enabled when a mapillary image is selected.
+     */
+    @Override
+    protected void updateEnabledState() {
+        super.updateEnabledState();
+        setEnabled(StreetsideLayer.hasInstance() && StreetsideLayer.getInstance().getData().getSelectedImage() != null);
     }
-  }
-
-  /**
-   * Enabled when a mapillary image is selected.
-   */
-  @Override
-  protected void updateEnabledState() {
-    super.updateEnabledState();
-    setEnabled(StreetsideLayer.hasInstance() && StreetsideLayer.getInstance().getData().getSelectedImage() != null);
-  }
 
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/StreetsideZoomAction.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/StreetsideZoomAction.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/StreetsideZoomAction.java	(revision 36228)
@@ -5,9 +5,10 @@
 
 import java.awt.event.ActionEvent;
+import java.io.Serial;
 
 import org.openstreetmap.josm.actions.JosmAction;
 import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
 import org.openstreetmap.josm.plugins.streetside.StreetsideDataListener;
+import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
 import org.openstreetmap.josm.plugins.streetside.StreetsideLayer;
 import org.openstreetmap.josm.plugins.streetside.StreetsidePlugin;
@@ -23,46 +24,46 @@
 public class StreetsideZoomAction extends JosmAction implements StreetsideDataListener {
 
-  private static final long serialVersionUID = -5885977359895624233L;
+    @Serial
+    private static final long serialVersionUID = -5885977359895624233L;
 
-  /**
-   * Main constructor.
-   */
-  public StreetsideZoomAction() {
-    super(tr("Zoom to selected image"), new ImageProvider(StreetsidePlugin.LOGO).setSize(ImageSizes.DEFAULT),
-        tr("Zoom to the currently selected Streetside image"), null, false, "mapillaryZoom", true);
-  }
+    /**
+     * Main constructor.
+     */
+    public StreetsideZoomAction() {
+        super(tr("Zoom to selected image"), new ImageProvider(StreetsidePlugin.LOGO).setSize(ImageSizes.DEFAULT).setOptional(true),
+                tr("Zoom to the currently selected Streetside image"), null, false, "mapillaryZoom", true);
+    }
 
-  @Override
-  public void actionPerformed(ActionEvent arg0) {
-    if (StreetsideLayer.getInstance().getData().getSelectedImage() == null) {
-      throw new IllegalStateException();
+    @Override
+    public void actionPerformed(ActionEvent arg0) {
+        if (StreetsideLayer.getInstance().getData().getSelectedImage() == null) {
+            throw new IllegalStateException();
+        }
+        MainApplication.getMap().mapView.zoomTo(StreetsideLayer.getInstance().getData().getSelectedImage());
     }
-    MainApplication.getMap().mapView
-        .zoomTo(StreetsideLayer.getInstance().getData().getSelectedImage().getMovingLatLon());
-  }
 
-  @Override
-  public void imagesAdded() {
-    // Nothing
-  }
+    @Override
+    public void imagesAdded() {
+        // Nothing
+    }
 
-  @Override
-  protected boolean listenToSelectionChange() {
-    return false;
-  }
+    @Override
+    protected boolean listenToSelectionChange() {
+        return false;
+    }
 
-  @Override
-  public void selectedImageChanged(StreetsideAbstractImage oldImage, StreetsideAbstractImage newImage) {
-    if (oldImage == null && newImage != null) {
-      setEnabled(true);
-    } else if (oldImage != null && newImage == null) {
-      setEnabled(false);
+    @Override
+    public void selectedImageChanged(StreetsideImage oldImage, StreetsideImage newImage) {
+        if (oldImage == null && newImage != null) {
+            setEnabled(true);
+        } else if (oldImage != null && newImage == null) {
+            setEnabled(false);
+        }
     }
-  }
 
-  @Override
-  protected void updateEnabledState() {
-    super.updateEnabledState();
-    setEnabled(StreetsideLayer.hasInstance() && StreetsideLayer.getInstance().getData().getSelectedImage() != null);
-  }
+    @Override
+    protected void updateEnabledState() {
+        super.updateEnabledState();
+        setEnabled(StreetsideLayer.hasInstance() && StreetsideLayer.getInstance().getData().getSelectedImage() != null);
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/WalkListener.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/WalkListener.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/WalkListener.java	(revision 36228)
@@ -11,9 +11,9 @@
 public interface WalkListener {
 
-  /**
-   * Called when a new walk thread is started.
-   *
-   * @param thread The thread executing the walk.
-   */
-  void walkStarted(WalkThread thread);
+    /**
+     * Called when a new walk thread is started.
+     *
+     * @param thread The thread executing the walk.
+     */
+    void walkStarted(WalkThread thread);
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/WalkThread.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/WalkThread.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/actions/WalkThread.java	(revision 36228)
@@ -8,5 +8,4 @@
 import javax.swing.SwingUtilities;
 
-import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
 import org.openstreetmap.josm.plugins.streetside.StreetsideData;
 import org.openstreetmap.josm.plugins.streetside.StreetsideDataListener;
@@ -24,172 +23,172 @@
  */
 public class WalkThread extends Thread implements StreetsideDataListener {
-  private static final Logger LOGGER = Logger.getLogger(WalkThread.class.getCanonicalName());
-  private final int interval;
-  private final StreetsideData data;
-  private final boolean waitForFullQuality;
-  private final boolean followSelected;
-  private final boolean goForward;
-  private boolean end;
-  private BufferedImage lastImage;
-  private volatile boolean paused;
+    private static final Logger LOGGER = Logger.getLogger(WalkThread.class.getCanonicalName());
+    private final int interval;
+    private final StreetsideData data;
+    private final boolean waitForFullQuality;
+    private final boolean followSelected;
+    private final boolean goForward;
+    private boolean end;
+    private BufferedImage lastImage;
+    private volatile boolean paused;
 
-  /**
-   * Main constructor.
-   *
-   * @param interval     How often the images switch.
-   * @param waitForPicture If it must wait for the full resolution picture or just the
-   *             thumbnail.
-   * @param followSelected Zoom to each image that is selected.
-   * @param goForward    true to go forward; false to go backwards.
-   */
-  public WalkThread(int interval, boolean waitForPicture, boolean followSelected, boolean goForward) {
-    this.interval = interval;
-    waitForFullQuality = waitForPicture;
-    this.followSelected = followSelected;
-    this.goForward = goForward;
-    data = StreetsideLayer.getInstance().getData();
-    data.addListener(this);
-  }
+    /**
+     * Main constructor.
+     *
+     * @param interval     How often the images switch.
+     * @param waitForPicture If it must wait for the full resolution picture or just the
+     *             thumbnail.
+     * @param followSelected Zoom to each image that is selected.
+     * @param goForward    true to go forward; false to go backwards.
+     */
+    public WalkThread(int interval, boolean waitForPicture, boolean followSelected, boolean goForward) {
+        this.interval = interval;
+        waitForFullQuality = waitForPicture;
+        this.followSelected = followSelected;
+        this.goForward = goForward;
+        data = StreetsideLayer.getInstance().getData();
+        data.addListener(this);
+    }
 
-  /**
-   * Downloads n images into the cache beginning from the supplied start-image (including the start-image itself).
-   *
-   * @param startImage the image to start with (this and the next n-1 images in the same sequence are downloaded)
-   * @param n      the number of images to download
-   * @param type     the quality of the image (full or thumbnail)
-   */
-  private static void preDownloadImages(StreetsideImage startImage, int n, CacheUtils.PICTURE type) {
-    if (n >= 1 && startImage != null) {
-      CacheUtils.downloadPicture(startImage, type);
-      if (startImage.next() instanceof StreetsideImage && n >= 2) {
-        preDownloadImages((StreetsideImage) startImage.next(), n - 1, type);
-      }
+    /**
+     * Downloads n images into the cache beginning from the supplied start-image (including the start-image itself).
+     *
+     * @param startImage the image to start with (this and the next n-1 images in the same sequence are downloaded)
+     * @param n      the number of images to download
+     * @param type     the quality of the image (full or thumbnail)
+     */
+    private void preDownloadImages(StreetsideImage startImage, int n, CacheUtils.PICTURE type) {
+        if (n >= 1 && startImage != null) {
+            CacheUtils.downloadPicture(startImage, type);
+            if (this.data.next(startImage) != null && n >= 2) {
+                preDownloadImages(this.data.next(startImage), n - 1, type);
+            }
+        }
     }
-  }
 
-  @Override
-  public void run() {
-    try {
-      while (!end && data.getSelectedImage().next() != null) {
-        StreetsideAbstractImage image = data.getSelectedImage();
-        if (image != null && image.next() instanceof StreetsideImage) {
-          // Predownload next 10 thumbnails.
-          preDownloadImages((StreetsideImage) image.next(), 10, CacheUtils.PICTURE.THUMBNAIL);
-          if (Boolean.TRUE.equals(StreetsideProperties.PREDOWNLOAD_CUBEMAPS.get())) {
-            preDownloadCubemaps((StreetsideImage) image.next(), 10);
-          }
-          if (waitForFullQuality) {
-            // Start downloading 3 next full images.
-            StreetsideAbstractImage currentImage = image.next();
-            preDownloadImages((StreetsideImage) currentImage, 3, CacheUtils.PICTURE.FULL_IMAGE);
-          }
+    @Override
+    public void run() {
+        try {
+            while (!end && this.data.next(this.data.getSelectedImage()) != null) {
+                StreetsideImage image = data.getSelectedImage();
+                if (image != null && this.data.next(image) != null) {
+                    // Predownload next 10 thumbnails.
+                    preDownloadImages(this.data.next(image), 10, CacheUtils.PICTURE.THUMBNAIL);
+                    if (Boolean.TRUE.equals(StreetsideProperties.PREDOWNLOAD_CUBEMAPS.get())) {
+                        preDownloadCubemaps(this.data.next(image), 10);
+                    }
+                    if (waitForFullQuality) {
+                        // Start downloading 3 next full images.
+                        StreetsideImage currentImage = this.data.next(image);
+                        preDownloadImages(currentImage, 3, CacheUtils.PICTURE.FULL_IMAGE);
+                    }
+                }
+                try {
+                    // Waits for full quality picture.
+                    final BufferedImage displayImage = StreetsideMainDialog.getInstance().getStreetsideImageDisplay()
+                            .getImage();
+                    if (waitForFullQuality && image != null) {
+                        while (displayImage == lastImage || displayImage == null || displayImage.getWidth() < 2048) {
+                            Thread.sleep(100);
+                        }
+                    } else { // Waits for thumbnail.
+                        while (displayImage == lastImage || displayImage == null || displayImage.getWidth() < 320) {
+                            Thread.sleep(100);
+                        }
+                    }
+                    while (paused) {
+                        Thread.sleep(100);
+                    }
+                    wait(interval);
+                    while (paused) {
+                        Thread.sleep(100);
+                    }
+                    lastImage = StreetsideMainDialog.getInstance().getStreetsideImageDisplay().getImage();
+                    if (goForward) {
+                        data.selectNext(followSelected);
+                    } else {
+                        data.selectPrevious(followSelected);
+                    }
+                } catch (InterruptedException e) {
+                    return;
+                }
+            }
+        } catch (NullPointerException e) {
+            if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
+                LOGGER.log(Logging.LEVEL_DEBUG,
+                        MessageFormat.format("Exception thrown in WalkThread: {0}", e.getMessage()), e);
+            }
+            return;
         }
-        try {
-          // Waits for full quality picture.
-          final BufferedImage displayImage = StreetsideMainDialog.getInstance().getStreetsideImageDisplay()
-              .getImage();
-          if (waitForFullQuality && image instanceof StreetsideImage) {
-            while (displayImage == lastImage || displayImage == null || displayImage.getWidth() < 2048) {
-              Thread.sleep(100);
+        end();
+    }
+
+    private void preDownloadCubemaps(StreetsideImage startImage, int n) {
+        if (n >= 1 && startImage != null) {
+
+            for (int i = 0; i < 6; i++) {
+                for (int j = 0; j < 4; j++) {
+                    for (int k = 0; k < 4; k++) {
+
+                        CacheUtils.downloadPicture(startImage, CacheUtils.PICTURE.CUBEMAP);
+                        if (this.data.next(startImage) != null && n >= 2) {
+                            preDownloadCubemaps(this.data.next(startImage), n - 1);
+                        }
+                    }
+                }
             }
-          } else { // Waits for thumbnail.
-            while (displayImage == lastImage || displayImage == null || displayImage.getWidth() < 320) {
-              Thread.sleep(100);
-            }
-          }
-          while (paused) {
-            Thread.sleep(100);
-          }
-          wait(interval);
-          while (paused) {
-            Thread.sleep(100);
-          }
-          lastImage = StreetsideMainDialog.getInstance().getStreetsideImageDisplay().getImage();
-          if (goForward) {
-            data.selectNext(followSelected);
-          } else {
-            data.selectPrevious(followSelected);
-          }
-        } catch (InterruptedException e) {
-          return;
         }
-      }
-    } catch (NullPointerException e) {
-      if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-        LOGGER.log(Logging.LEVEL_DEBUG,
-            MessageFormat.format("Exception thrown in WalkThread: {0}", e.getMessage()), e);
-      }
-      return;
     }
-    end();
-  }
 
-  private void preDownloadCubemaps(StreetsideImage startImage, int n) {
-    if (n >= 1 && startImage != null) {
+    @Override
+    public void imagesAdded() {
+        // Nothing
+    }
 
-      for (int i = 0; i < 6; i++) {
-        for (int j = 0; j < 4; j++) {
-          for (int k = 0; k < 4; k++) {
+    @Override
+    public void selectedImageChanged(StreetsideImage oldImage, StreetsideImage newImage) {
+        if (newImage != this.data.next(oldImage)) {
+            end();
+            interrupt();
+        }
+    }
 
-            CacheUtils.downloadPicture(startImage, CacheUtils.PICTURE.CUBEMAP);
-            if (startImage.next() instanceof StreetsideImage && n >= 2) {
-              preDownloadCubemaps((StreetsideImage) startImage.next(), n - 1);
-            }
-          }
+    /**
+     * Continues with the execution if paused.
+     */
+    public void play() {
+        paused = false;
+    }
+
+    /**
+     * Pauses the execution.
+     */
+    public void pause() {
+        paused = true;
+    }
+
+    /**
+     * Stops the execution.
+     */
+    public void stopWalk() {
+        if (SwingUtilities.isEventDispatchThread()) {
+            end();
+            interrupt();
+        } else {
+            SwingUtilities.invokeLater(this::stopWalk);
         }
-      }
     }
-  }
 
-  @Override
-  public void imagesAdded() {
-    // Nothing
-  }
-
-  @Override
-  public void selectedImageChanged(StreetsideAbstractImage oldImage, StreetsideAbstractImage newImage) {
-    if (newImage != oldImage.next()) {
-      end();
-      interrupt();
+    /**
+     * Called when the walk stops by itself of forcefully.
+     */
+    public void end() {
+        if (SwingUtilities.isEventDispatchThread()) {
+            end = true;
+            data.removeListener(this);
+            StreetsideMainDialog.getInstance().setMode(StreetsideMainDialog.MODE.NORMAL);
+        } else {
+            SwingUtilities.invokeLater(this::end);
+        }
     }
-  }
-
-  /**
-   * Continues with the execution if paused.
-   */
-  public void play() {
-    paused = false;
-  }
-
-  /**
-   * Pauses the execution.
-   */
-  public void pause() {
-    paused = true;
-  }
-
-  /**
-   * Stops the execution.
-   */
-  public void stopWalk() {
-    if (SwingUtilities.isEventDispatchThread()) {
-      end();
-      interrupt();
-    } else {
-      SwingUtilities.invokeLater(this::stopWalk);
-    }
-  }
-
-  /**
-   * Called when the walk stops by itself of forcefully.
-   */
-  public void end() {
-    if (SwingUtilities.isEventDispatchThread()) {
-      end = true;
-      data.removeListener(this);
-      StreetsideMainDialog.getInstance().setMode(StreetsideMainDialog.MODE.NORMAL);
-    } else {
-      SwingUtilities.invokeLater(this::end);
-    }
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cache/CacheUtils.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cache/CacheUtils.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cache/CacheUtils.java	(revision 36228)
@@ -3,12 +3,17 @@
 
 import java.io.IOException;
-import java.util.logging.Logger;
+import java.io.UncheckedIOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 import org.openstreetmap.josm.data.cache.CacheEntry;
 import org.openstreetmap.josm.data.cache.CacheEntryAttributes;
 import org.openstreetmap.josm.data.cache.ICachedLoaderListener;
+import org.openstreetmap.josm.plugins.streetside.CubeMapTileXY;
 import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
 import org.openstreetmap.josm.plugins.streetside.cubemap.CubemapBuilder;
-import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.plugins.streetside.cubemap.CubemapUtils;
 
 /**
@@ -20,111 +25,121 @@
 public final class CacheUtils {
 
-  private static final Logger LOGGER = Logger.getLogger(CacheUtils.class.getCanonicalName());
+    private static final IgnoreDownload ignoreDownload = new IgnoreDownload();
 
-  private static final IgnoreDownload ignoreDownload = new IgnoreDownload();
+    private CacheUtils() {
+        // Private constructor to avoid instantiation
+    }
 
-  private CacheUtils() {
-    // Private constructor to avoid instantiation
-  }
+    /**
+     * Downloads the thumbnail and the full resolution picture of the given
+     * image. Does nothing if it is already in cache.
+     *
+     * @param img The image whose picture is going to be downloaded.
+     * @return A map of tiles to cached images (The {@code null} key is the thumbnail, if one was requested)
+     */
+    public static Map<CubeMapTileXY, StreetsideCache> downloadPicture(StreetsideImage img) {
+        return downloadPicture(img, PICTURE.BOTH);
+    }
 
-  /**
-   * Downloads the the thumbnail and the full resolution picture of the given
-   * image. Does nothing if it is already in cache.
-   *
-   * @param img The image whose picture is going to be downloaded.
-   */
-  public static void downloadPicture(StreetsideImage img) {
-    downloadPicture(img, PICTURE.BOTH);
-  }
+    /**
+     * Downloads the thumbnail and the full resolution picture of the given
+     * image. Does nothing if it is already in cache.
+     *
+     * @param cm The image whose picture is going to be downloaded.
+     * @return A map of tiles to cached images (The {@code null} key is the thumbnail, if one was requested)
+     */
+    public static Map<CubeMapTileXY, StreetsideCache> downloadCubemap(StreetsideImage cm) {
+        return downloadPicture(cm, PICTURE.CUBEMAP);
+    }
 
-  /**
-   * Downloads the the thumbnail and the full resolution picture of the given
-   * image. Does nothing if it is already in cache.
-   *
-   * @param cm The image whose picture is going to be downloaded.
-   */
-  public static void downloadCubemap(StreetsideImage cm) {
-    downloadPicture(cm, PICTURE.CUBEMAP);
-  }
+    /**
+     * Downloads the picture of the given image. Does nothing when it is already
+     * in cache.
+     *
+     * @param img The image to be downloaded.
+     * @param pic The picture type to be downloaded (full quality, thumbnail or
+     *      both.)
+     * @return A map of tiles to cached images (The {@code null} key is the thumbnail, if one was requested)
+     */
+    public static Map<CubeMapTileXY, StreetsideCache> downloadPicture(StreetsideImage img, PICTURE pic) {
+        return downloadPicture(img, pic, ignoreDownload);
+    }
 
-  /**
-   * Downloads the picture of the given image. Does nothing when it is already
-   * in cache.
-   *
-   * @param img The image to be downloaded.
-   * @param pic The picture type to be downloaded (full quality, thumbnail or
-   *      both.)
-   */
-  public static void downloadPicture(StreetsideImage img, PICTURE pic) {
-    switch (pic) {
-    case BOTH:
-      if (new StreetsideCache(img.getId(), StreetsideCache.Type.THUMBNAIL).get() == null)
-        submit(img.getId(), StreetsideCache.Type.THUMBNAIL, ignoreDownload);
-      if (new StreetsideCache(img.getId(), StreetsideCache.Type.FULL_IMAGE).get() == null)
-        submit(img.getId(), StreetsideCache.Type.FULL_IMAGE, ignoreDownload);
-      break;
-    case THUMBNAIL:
-      submit(img.getId(), StreetsideCache.Type.THUMBNAIL, ignoreDownload);
-      break;
-    case FULL_IMAGE:
-      // not used (relic from Mapillary)
-      break;
-    case CUBEMAP:
-      if (img.getId() == null) {
-        LOGGER.log(Logging.LEVEL_ERROR, "Download cancelled. Image id is null.");
-      } else {
-        CubemapBuilder.getInstance().downloadCubemapImages(img.getId());
-      }
-      break;
-    default:
-      submit(img.getId(), StreetsideCache.Type.FULL_IMAGE, ignoreDownload);
-      break;
+    /**
+     * Downloads the picture of the given image. Does nothing when it is already
+     * in cache.
+     *
+     * @param img The image to be downloaded.
+     * @param pic The picture type to be downloaded (full quality, thumbnail or
+     *      both.)
+     * @param lis  The listener that is going to receive the picture.
+     * @return A map of tiles to cached images (The {@code null} key is the thumbnail, if one was requested)
+     */
+    public static Map<CubeMapTileXY, StreetsideCache> downloadPicture(StreetsideImage img, PICTURE pic,
+            ICachedLoaderListener lis) {
+        if (img.id() == null) {
+            return Collections.emptyMap();
+        }
+        return switch (pic) {
+        case BOTH -> {
+            final Map<CubeMapTileXY, StreetsideCache> jobs = new HashMap<>(
+                    1 + (int) Math.round(Math.pow(4, img.zoomMax())));
+            jobs.putAll(downloadPicture(img, PICTURE.THUMBNAIL, lis));
+            jobs.putAll(downloadPicture(img, PICTURE.FULL_IMAGE, lis));
+            yield Collections.unmodifiableMap(jobs);
+        }
+        case FULL_IMAGE -> img.getFaceTiles(CubemapUtils.CubemapFaces.FRONT, img.zoomMax())
+                .collect(Collectors.toMap(p -> p.a, p -> submit(p.b, lis)));
+        case CUBEMAP -> CubemapBuilder.getInstance().downloadCubemapImages(img);
+        case THUMBNAIL -> Collections.singletonMap(null, submit(img.getThumbnail(), lis));
+        };
     }
-  }
 
-  /**
-   * Requests the picture with the given key and quality and uses the given
-   * listener.
-   *
-   * @param key  The key of the picture to be requested.
-   * @param type The quality of the picture to be requested.
-   * @param lis  The listener that is going to receive the picture.
-   */
-  public static void submit(String key, StreetsideCache.Type type, ICachedLoaderListener lis) {
-    try {
-      new StreetsideCache(key, type).submit(lis, false);
-    } catch (IOException e) {
-      LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
+    /**
+     * Requests the picture with the given key and quality and uses the given
+     * listener.
+     *
+     * @param key  The key of the picture to be requested.
+     * @param lis  The listener that is going to receive the picture.
+     * @return The cache job
+     */
+    public static StreetsideCache submit(String key, ICachedLoaderListener lis) {
+        try {
+            final var cache = new StreetsideCache(key);
+            cache.submit(lis, false);
+            return cache;
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
     }
-  }
 
-  /**
-   * Picture quality
-   */
-  public enum PICTURE {
     /**
-     * Thumbnail quality picture (320 p)
+     * Picture quality
      */
-    THUMBNAIL,
-    /**
-     * Full quality picture (2048 p)
-     */
-    FULL_IMAGE,
-    /**
-     * Both of them
-     */
-    BOTH,
-    /**
-     * Streetside cubemap
-     */
-    CUBEMAP
-  }
+    public enum PICTURE {
+        /**
+         * Thumbnail quality picture (320 p)
+         */
+        THUMBNAIL,
+        /**
+         * Full quality picture (2048 p)
+         */
+        FULL_IMAGE,
+        /**
+         * Both of them
+         */
+        BOTH,
+        /**
+         * Streetside cubemap
+         */
+        CUBEMAP
+    }
 
-  private static class IgnoreDownload implements ICachedLoaderListener {
+    private static class IgnoreDownload implements ICachedLoaderListener {
 
-    @Override
-    public void loadingFinished(CacheEntry arg0, CacheEntryAttributes arg1, LoadResult arg2) {
-      // Ignore download
+        @Override
+        public void loadingFinished(CacheEntry arg0, CacheEntryAttributes arg1, LoadResult arg2) {
+            // Ignore download
+        }
     }
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cache/Caches.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cache/Caches.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cache/Caches.java	(revision 36228)
@@ -10,151 +10,128 @@
 
 import org.apache.commons.jcs3.access.CacheAccess;
-import org.apache.commons.jcs3.engine.behavior.IElementAttributes;
 import org.openstreetmap.josm.data.Preferences;
 import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
 import org.openstreetmap.josm.data.cache.JCSCacheManager;
-import org.openstreetmap.josm.plugins.streetside.model.UserProfile;
 import org.openstreetmap.josm.tools.Logging;
 
 public final class Caches {
 
-  private static final Logger LOGGER = Logger.getLogger(Caches.class.getCanonicalName());
+    private static final Logger LOGGER = Logger.getLogger(Caches.class.getCanonicalName());
 
-  private Caches() {
-    // Private constructor to avoid instantiation
-  }
-
-  public static File getCacheDirectory() {
-    final File f = new File(Preferences.main().getPluginsDirectory().getPath() + "/MicrosoftStreetside/cache");
-    if (!f.exists()) {
-      f.mkdirs();
-    }
-    return f;
-  }
-
-  public abstract static class CacheProxy<K, V extends Serializable> {
-    private final CacheAccess<K, V> cache;
-
-    protected CacheProxy() {
-      CacheAccess<K, V> c;
-      try {
-        c = createNewCache();
-      } catch (IOException e) {
-        LOGGER.log(Logging.LEVEL_WARN, e, () -> "Could not initialize cache for " + getClass().getName());
-        c = null;
-      }
-      cache = c;
+    private Caches() {
+        // Private constructor to avoid instantiation
     }
 
-    protected abstract CacheAccess<K, V> createNewCache() throws IOException;
-
-    public V get(final K key) {
-      return cache == null ? null : cache.get(key);
+    public static File getCacheDirectory() {
+        final var file = new File(Preferences.main().getPluginsDirectory().getPath() + "/MicrosoftStreetside/cache");
+        if (!file.exists()) {
+            if (!file.mkdirs()) {
+                Logging.error("Failed to make directory: {0}", file);
+            }
+        }
+        return file;
     }
 
-    public void put(final K key, final V value) {
-      if (cache != null) {
-        cache.put(key, value);
-      }
-    }
-  }
+    public abstract static class CacheProxy<K, V extends Serializable> {
+        private final CacheAccess<K, V> cache;
 
-  public static class ImageCache {
-    private static ImageCache instance;
-    private final CacheAccess<String, BufferedImageCacheEntry> cache;
+        protected CacheProxy() {
+            CacheAccess<K, V> c;
+            try {
+                c = createNewCache();
+            } catch (IOException e) {
+                LOGGER.log(Logging.LEVEL_WARN, e, () -> "Could not initialize cache for " + getClass().getName());
+                c = null;
+            }
+            cache = c;
+        }
 
-    public ImageCache() {
-      CacheAccess<String, BufferedImageCacheEntry> c;
-      try {
-        c = JCSCacheManager.getCache("streetside", 10, 10000, Caches.getCacheDirectory().getPath());
-      } catch (Exception e) {
-        Logging.log(Logging.LEVEL_WARN, "Could not initialize the Streetside image cache.", e);
-        c = null;
-      }
-      cache = c;
+        protected abstract CacheAccess<K, V> createNewCache() throws IOException;
+
+        public V get(final K key) {
+            return cache == null ? null : cache.get(key);
+        }
+
+        public void put(final K key, final V value) {
+            if (cache != null) {
+                cache.put(key, value);
+            }
+        }
     }
 
-    public static ImageCache getInstance() {
-      synchronized (ImageCache.class) {
-        if (ImageCache.instance == null) {
-          ImageCache.instance = new ImageCache();
+    public static class ImageCache {
+        private static ImageCache instance;
+        private final CacheAccess<String, BufferedImageCacheEntry> cache;
+
+        public ImageCache() {
+            CacheAccess<String, BufferedImageCacheEntry> c;
+            try {
+                c = JCSCacheManager.getCache("streetside", 10, 10000, Caches.getCacheDirectory().getPath());
+            } catch (Exception e) {
+                Logging.log(Logging.LEVEL_WARN, "Could not initialize the Streetside image cache.", e);
+                c = null;
+            }
+            cache = c;
         }
-        return ImageCache.instance;
-      }
+
+        public static ImageCache getInstance() {
+            synchronized (ImageCache.class) {
+                if (ImageCache.instance == null) {
+                    ImageCache.instance = new ImageCache();
+                }
+                return ImageCache.instance;
+            }
+        }
+
+        public CacheAccess<String, BufferedImageCacheEntry> getCache() {
+            return cache;
+        }
     }
 
-    public CacheAccess<String, BufferedImageCacheEntry> getCache() {
-      return cache;
-    }
-  }
+    public static class CubemapCache {
+        private static CubemapCache instance;
+        private final CacheAccess<String, BufferedImageCacheEntry> cache;
 
-  public static class CubemapCache {
-    private static CubemapCache instance;
-    private final CacheAccess<String, BufferedImageCacheEntry> cache;
+        public CubemapCache() {
+            CacheAccess<String, BufferedImageCacheEntry> c;
+            try {
+                c = JCSCacheManager.getCache("streetside", 10, 10000, Caches.getCacheDirectory().getPath());
+            } catch (Exception e) {
+                LOGGER.log(Logging.LEVEL_WARN, "Could not initialize the Streetside cubemap cache.", e);
+                c = null;
+            }
+            cache = c;
+        }
 
-    public CubemapCache() {
-      CacheAccess<String, BufferedImageCacheEntry> c;
-      try {
-        c = JCSCacheManager.getCache("streetside", 10, 10000, Caches.getCacheDirectory().getPath());
-      } catch (Exception e) {
-        LOGGER.log(Logging.LEVEL_WARN, "Could not initialize the Streetside cubemap cache.", e);
-        c = null;
-      }
-      cache = c;
+        public static CubemapCache getInstance() {
+            synchronized (CubemapCache.class) {
+                if (CubemapCache.instance == null) {
+                    CubemapCache.instance = new CubemapCache();
+                }
+                return CubemapCache.instance;
+            }
+        }
+
+        public CacheAccess<String, BufferedImageCacheEntry> getCache() {
+            return cache;
+        }
     }
 
-    public static CubemapCache getInstance() {
-      synchronized (CubemapCache.class) {
-        if (CubemapCache.instance == null) {
-          CubemapCache.instance = new CubemapCache();
+    public static class MapObjectIconCache extends CacheProxy<String, ImageIcon> {
+        private static CacheProxy<String, ImageIcon> instance;
+
+        public static CacheProxy<String, ImageIcon> getInstance() {
+            synchronized (MapObjectIconCache.class) {
+                if (MapObjectIconCache.instance == null) {
+                    MapObjectIconCache.instance = new MapObjectIconCache();
+                }
+                return MapObjectIconCache.instance;
+            }
         }
-        return CubemapCache.instance;
-      }
+
+        @Override
+        protected CacheAccess<String, ImageIcon> createNewCache() throws IOException {
+            return JCSCacheManager.getCache("streetsideObjectIcons", 100, 1000, Caches.getCacheDirectory().getPath());
+        }
     }
-
-    public CacheAccess<String, BufferedImageCacheEntry> getCache() {
-      return cache;
-    }
-  }
-
-  public static class MapObjectIconCache extends CacheProxy<String, ImageIcon> {
-    private static CacheProxy<String, ImageIcon> instance;
-
-    public static CacheProxy<String, ImageIcon> getInstance() {
-      synchronized (MapObjectIconCache.class) {
-        if (MapObjectIconCache.instance == null) {
-          MapObjectIconCache.instance = new MapObjectIconCache();
-        }
-        return MapObjectIconCache.instance;
-      }
-    }
-
-    @Override
-    protected CacheAccess<String, ImageIcon> createNewCache() throws IOException {
-      return JCSCacheManager.getCache("streetsideObjectIcons", 100, 1000, Caches.getCacheDirectory().getPath());
-    }
-  }
-
-  public static class UserProfileCache extends CacheProxy<String, UserProfile> {
-    private static CacheProxy<String, UserProfile> instance;
-
-    public static CacheProxy<String, UserProfile> getInstance() {
-      synchronized (UserProfileCache.class) {
-        if (UserProfileCache.instance == null) {
-          UserProfileCache.instance = new UserProfileCache();
-        }
-        return UserProfileCache.instance;
-      }
-    }
-
-    @Override
-    protected CacheAccess<String, UserProfile> createNewCache() throws IOException {
-      CacheAccess<String, UserProfile> cache = JCSCacheManager.getCache("userProfile", 100, 1000,
-          Caches.getCacheDirectory().getPath());
-      IElementAttributes atts = cache.getDefaultElementAttributes();
-      atts.setMaxLife(604_800_000); // Sets lifetime to 7 days (604800000=1000*60*60*24*7)
-      cache.setDefaultElementAttributes(atts);
-      return cache;
-    }
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cache/StreetsideCache.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cache/StreetsideCache.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cache/StreetsideCache.java	(revision 36228)
@@ -2,4 +2,7 @@
 package org.openstreetmap.josm.plugins.streetside.cache;
 
+import java.io.UncheckedIOException;
+import java.net.MalformedURLException;
+import java.net.URI;
 import java.net.URL;
 import java.util.HashMap;
@@ -8,5 +11,4 @@
 import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob;
 import org.openstreetmap.josm.data.imagery.TileJobOptions;
-import org.openstreetmap.josm.plugins.streetside.utils.StreetsideURL.VirtualEarth;
 
 /**
@@ -18,64 +20,42 @@
 public class StreetsideCache extends JCSCachedTileLoaderJob<String, BufferedImageCacheEntry> {
 
-  private final URL url;
-  private final String id;
+    private final String url;
 
-  /**
-   * Main constructor.
-   *
-   * @param id   The id of the image.
-   * @param type The type of image that must be downloaded (THUMBNAIL or
-   *       FULL_IMAGE).
-   */
-  public StreetsideCache(final String id, final Type type) {
-    super(Caches.ImageCache.getInstance().getCache(), new TileJobOptions(50000, 50000, new HashMap<>(), 50000L));
+    /**
+     * Main constructor.
+     *
+     * @param url The image URL
+     */
+    public StreetsideCache(final String url) {
+        super(Caches.ImageCache.getInstance().getCache(), new TileJobOptions(50000, 50000, new HashMap<>(), 50000L));
+        this.url = url;
+    }
 
-    if (id == null || type == null) {
-      this.id = null;
-      url = null;
-    } else {
-      this.id = id;
-      url = VirtualEarth.streetsideTile(id, type == Type.THUMBNAIL);
+    @Override
+    public String getCacheKey() {
+        return this.url;
     }
-  }
 
-  @Override
-  public String getCacheKey() {
-    return id;
-  }
+    @Override
+    public URL getUrl() {
+        try {
+            return URI.create(this.url).toURL();
+        } catch (MalformedURLException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
 
-  @Override
-  public URL getUrl() {
-    return url;
-  }
+    @Override
+    protected BufferedImageCacheEntry createCacheEntry(byte[] content) {
+        return new BufferedImageCacheEntry(content);
+    }
 
-  @Override
-  protected BufferedImageCacheEntry createCacheEntry(byte[] content) {
-    return new BufferedImageCacheEntry(content);
-  }
-
-  @Override
-  protected boolean isObjectLoadable() {
-    if (cacheData == null) {
-      return false;
+    @Override
+    protected boolean isObjectLoadable() {
+        if (cacheData == null) {
+            return false;
+        }
+        final byte[] content = cacheData.getContent();
+        return content != null && content.length > 0;
     }
-    final byte[] content = cacheData.getContent();
-    return content != null && content.length > 0;
-  }
-
-  /**
-   * Types of images.
-   *
-   * @author nokutu
-   */
-  public enum Type {
-    /**
-     * Full quality image
-     */
-    FULL_IMAGE,
-    /**
-     * Low quality image
-     */
-    THUMBNAIL
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/CameraTransformer.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/CameraTransformer.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/CameraTransformer.java	(revision 36228)
@@ -7,178 +7,166 @@
 import javafx.scene.transform.Translate;
 
-// necessary because JavaFX is not an official part of Java 8 (access)
-@SuppressWarnings("restriction")
+/**
+ * A transformer for the camera rotations
+ */
 public class CameraTransformer extends Group {
 
-  public Translate t = new Translate();
-  public Translate p = new Translate();
-  public Translate ip = new Translate();
-  public Rotate rx = new Rotate();
-  public Rotate ry = new Rotate();
-  public Rotate rz = new Rotate();
-  public Scale s = new Scale();
+    public final Translate t = new Translate();
+    private final Translate p = new Translate();
+    private final Translate ip = new Translate();
+    public final Rotate rx = new Rotate();
+    public final Rotate ry = new Rotate();
+    private final Rotate rz = new Rotate();
+    private final Scale s = new Scale();
 
-  {
-    rx.setAxis(Rotate.X_AXIS);
-  }
+    /**
+     * Create a new transformer
+     */
+    public CameraTransformer() {
+        super();
+        rx.setAxis(Rotate.X_AXIS);
+        ry.setAxis(Rotate.Y_AXIS);
+        rz.setAxis(Rotate.Z_AXIS);
+        getTransforms().addAll(t, rz, ry, rx, s);
+    }
 
-  {
-    ry.setAxis(Rotate.Y_AXIS);
-  }
+    /**
+     * Create a new transformer with a specific rotation
+     * @param rotateOrder The order in which rotations will occur
+     */
+    public CameraTransformer(CameraTransformer.RotateOrder rotateOrder) {
+        super();
+        switch (rotateOrder) {
+        case XYZ:
+            getTransforms().addAll(t, p, rz, ry, rx, s, ip);
+            break;
+        case XZY:
+            getTransforms().addAll(t, p, ry, rz, rx, s, ip);
+            break;
+        case YXZ:
+            getTransforms().addAll(t, p, rz, rx, ry, s, ip);
+            break;
+        case YZX:
+            getTransforms().addAll(t, p, rx, rz, ry, s, ip); // For Camera
+            break;
+        case ZXY:
+            getTransforms().addAll(t, p, ry, rx, rz, s, ip);
+            break;
+        case ZYX:
+            getTransforms().addAll(t, p, rx, ry, rz, s, ip);
+            break;
+        }
+    }
 
-  {
-    rz.setAxis(Rotate.Z_AXIS);
-  }
+    public void setTranslate(double x, double y, double z) {
+        t.setX(x);
+        t.setY(y);
+        t.setZ(z);
+    }
 
-  public CameraTransformer() {
-    super();
-    getTransforms().addAll(t, rz, ry, rx, s);
-  }
+    public void setTranslate(double x, double y) {
+        t.setX(x);
+        t.setY(y);
+    }
 
-  public CameraTransformer(CameraTransformer.RotateOrder rotateOrder) {
-    super();
-    switch (rotateOrder) {
-    case XYZ:
-      getTransforms().addAll(t, p, rz, ry, rx, s, ip);
-      break;
-    case XZY:
-      getTransforms().addAll(t, p, ry, rz, rx, s, ip);
-      break;
-    case YXZ:
-      getTransforms().addAll(t, p, rz, rx, ry, s, ip);
-      break;
-    case YZX:
-      getTransforms().addAll(t, p, rx, rz, ry, s, ip); // For Camera
-      break;
-    case ZXY:
-      getTransforms().addAll(t, p, ry, rx, rz, s, ip);
-      break;
-    case ZYX:
-      getTransforms().addAll(t, p, rx, ry, rz, s, ip);
-      break;
+    public void setTx(double x) {
+        t.setX(x);
     }
-  }
 
-  public void setTranslate(double x, double y, double z) {
-    t.setX(x);
-    t.setY(y);
-    t.setZ(z);
-  }
+    public void setTy(double y) {
+        t.setY(y);
+    }
 
-  public void setTranslate(double x, double y) {
-    t.setX(x);
-    t.setY(y);
-  }
+    public void setTz(double z) {
+        t.setZ(z);
+    }
 
-  public void setTx(double x) {
-    t.setX(x);
-  }
+    public void setRotate(double x, double y, double z) {
+        rx.setAngle(x);
+        ry.setAngle(y);
+        rz.setAngle(z);
+    }
 
-  public void setTy(double y) {
-    t.setY(y);
-  }
+    public void setRotateX(double x) {
+        rx.setAngle(x);
+    }
 
-  public void setTz(double z) {
-    t.setZ(z);
-  }
+    public void setRotateY(double y) {
+        ry.setAngle(y);
+    }
 
-  public void setRotate(double x, double y, double z) {
-    rx.setAngle(x);
-    ry.setAngle(y);
-    rz.setAngle(z);
-  }
+    public void setRotateZ(double z) {
+        rz.setAngle(z);
+    }
 
-  public void setRotateX(double x) {
-    rx.setAngle(x);
-  }
+    public void setRx(double x) {
+        rx.setAngle(x);
+    }
 
-  public void setRotateY(double y) {
-    ry.setAngle(y);
-  }
+    public void setRy(double y) {
+        ry.setAngle(y);
+    }
 
-  public void setRotateZ(double z) {
-    rz.setAngle(z);
-  }
+    public void setRz(double z) {
+        rz.setAngle(z);
+    }
 
-  public void setRx(double x) {
-    rx.setAngle(x);
-  }
+    public void setScale(double scaleFactor) {
+        s.setX(scaleFactor);
+        s.setY(scaleFactor);
+        s.setZ(scaleFactor);
+    }
 
-  public void setRy(double y) {
-    ry.setAngle(y);
-  }
+    public void setScale(double x, double y, double z) {
+        s.setX(x);
+        s.setY(y);
+        s.setZ(z);
+    }
 
-  public void setRz(double z) {
-    rz.setAngle(z);
-  }
+    public void setSx(double x) {
+        s.setX(x);
+    }
 
-  public void setScale(double scaleFactor) {
-    s.setX(scaleFactor);
-    s.setY(scaleFactor);
-    s.setZ(scaleFactor);
-  }
+    public void setSy(double y) {
+        s.setY(y);
+    }
 
-  public void setScale(double x, double y, double z) {
-    s.setX(x);
-    s.setY(y);
-    s.setZ(z);
-  }
+    public void setSz(double z) {
+        s.setZ(z);
+    }
 
-  public void setSx(double x) {
-    s.setX(x);
-  }
+    public void setPivot(double x, double y, double z) {
+        p.setX(x);
+        p.setY(y);
+        p.setZ(z);
+        ip.setX(-x);
+        ip.setY(-y);
+        ip.setZ(-z);
+    }
 
-  public void setSy(double y) {
-    s.setY(y);
-  }
+    public void reset() {
+        rx.setAngle(0.0);
+        ry.setAngle(0.0);
+        rz.setAngle(0.0);
+        resetTSP();
+    }
 
-  public void setSz(double z) {
-    s.setZ(z);
-  }
+    public void resetTSP() {
+        t.setX(0.0);
+        t.setY(0.0);
+        t.setZ(0.0);
+        s.setX(1.0);
+        s.setY(1.0);
+        s.setZ(1.0);
+        p.setX(0.0);
+        p.setY(0.0);
+        p.setZ(0.0);
+        ip.setX(0.0);
+        ip.setY(0.0);
+        ip.setZ(0.0);
+    }
 
-  public void setPivot(double x, double y, double z) {
-    p.setX(x);
-    p.setY(y);
-    p.setZ(z);
-    ip.setX(-x);
-    ip.setY(-y);
-    ip.setZ(-z);
-  }
-
-  public void reset() {
-    t.setX(0.0);
-    t.setY(0.0);
-    t.setZ(0.0);
-    rx.setAngle(0.0);
-    ry.setAngle(0.0);
-    rz.setAngle(0.0);
-    s.setX(1.0);
-    s.setY(1.0);
-    s.setZ(1.0);
-    p.setX(0.0);
-    p.setY(0.0);
-    p.setZ(0.0);
-    ip.setX(0.0);
-    ip.setY(0.0);
-    ip.setZ(0.0);
-  }
-
-  public void resetTSP() {
-    t.setX(0.0);
-    t.setY(0.0);
-    t.setZ(0.0);
-    s.setX(1.0);
-    s.setY(1.0);
-    s.setZ(1.0);
-    p.setX(0.0);
-    p.setY(0.0);
-    p.setZ(0.0);
-    ip.setX(0.0);
-    ip.setY(0.0);
-    ip.setZ(0.0);
-  }
-
-  public enum RotateOrder {
-    XYZ, XZY, YXZ, YZX, ZXY, ZYX
-  }
+    public enum RotateOrder {
+        XYZ, XZY, YXZ, YZX, ZXY, ZYX
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapBox.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapBox.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapBox.java	(revision 36228)
@@ -1,6 +1,4 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.plugins.streetside.cubemap;
-
-import java.awt.image.BufferedImage;
 
 import org.openstreetmap.josm.plugins.streetside.utils.GraphicsUtils;
@@ -9,5 +7,4 @@
 import javafx.beans.property.DoubleProperty;
 import javafx.beans.property.SimpleDoubleProperty;
-import javafx.geometry.Rectangle2D;
 import javafx.scene.Group;
 import javafx.scene.PerspectiveCamera;
@@ -19,250 +16,164 @@
 
 /**
+ * A box for showing the cubemap images
  * @author renerr18
  */
-@SuppressWarnings("restriction")
 public class CubemapBox extends Group {
 
-  private final Affine affine = new Affine();
-  private final ImageView front = new ImageView();
-  private final ImageView right = new ImageView();
-  private final ImageView back = new ImageView();
-  private final ImageView left = new ImageView();
-  private final ImageView up = new ImageView();
-  private final ImageView down = new ImageView();
-  private final ImageView[] views = new ImageView[] { front, right, back, left, up, down };
-  private final Image frontImg;
-  private final Image rightImg;
-  private final Image backImg;
-  private final Image leftImg;
-  private final Image upImg;
-  private final Image downImg;
-  private final PerspectiveCamera camera;
-  private final CubemapBoxImageType imageType;
-  private Image singleImg;
-  private AnimationTimer timer;
+    private final Affine affine = new Affine();
+    private final ImageView front = new ImageView();
+    private final ImageView right = new ImageView();
+    private final ImageView back = new ImageView();
+    private final ImageView left = new ImageView();
+    private final ImageView up = new ImageView();
+    private final ImageView down = new ImageView();
+    private final ImageView[] views = new ImageView[] { front, right, back, left, up, down };
+    private final Image frontImg;
+    private final Image rightImg;
+    private final Image backImg;
+    private final Image leftImg;
+    private final Image upImg;
+    private final Image downImg;
+    private final PerspectiveCamera camera;
 
-  {
-    front.setId(CubemapUtils.CubemapFaces.FRONT.getValue());
-    right.setId(CubemapUtils.CubemapFaces.RIGHT.getValue());
-    back.setId(CubemapUtils.CubemapFaces.BACK.getValue());
-    left.setId(CubemapUtils.CubemapFaces.LEFT.getValue());
-    up.setId(CubemapUtils.CubemapFaces.UP.getValue());
-    down.setId(CubemapUtils.CubemapFaces.DOWN.getValue());
+    /**
+     * Create a new CubemapBox
+     * @param frontImg The front image
+     * @param rightImg The right image
+     * @param backImg The back image
+     * @param leftImg The left image
+     * @param upImg The up image
+     * @param downImg The down image
+     * @param size The size of each cube side
+     * @param camera The camera to use
+     */
+    public CubemapBox(Image frontImg, Image rightImg, Image backImg, Image leftImg, Image upImg, Image downImg,
+            double size, PerspectiveCamera camera) {
+        super();
 
-  }
+        this.front.setId(CubemapUtils.CubemapFaces.FRONT.getValue());
+        this.right.setId(CubemapUtils.CubemapFaces.RIGHT.getValue());
+        this.back.setId(CubemapUtils.CubemapFaces.BACK.getValue());
+        this.left.setId(CubemapUtils.CubemapFaces.LEFT.getValue());
+        this.up.setId(CubemapUtils.CubemapFaces.UP.getValue());
+        this.down.setId(CubemapUtils.CubemapFaces.DOWN.getValue());
 
-  public CubemapBox(Image frontImg, Image rightImg, Image backImg, Image leftImg, Image upImg, Image downImg,
-      double size, PerspectiveCamera camera) {
+        this.frontImg = frontImg;
+        this.rightImg = rightImg;
+        this.backImg = backImg;
+        this.leftImg = leftImg;
+        this.upImg = upImg;
+        this.downImg = downImg;
+        this.size.set(size);
+        this.camera = camera;
 
-    super();
+        loadImageViews();
 
-    imageType = CubemapBoxImageType.MULTIPLE;
+        getTransforms().add(affine);
 
-    this.frontImg = frontImg;
-    this.rightImg = rightImg;
-    this.backImg = backImg;
-    this.leftImg = leftImg;
-    this.upImg = upImg;
-    this.downImg = downImg;
-    this.size.set(size);
-    this.camera = camera;
+        getChildren().addAll(views);
 
-    loadImageViews();
-
-    getTransforms().add(affine);
-
-    getChildren().addAll(views);
-
-    startTimer();
-  }
-
-  public void loadImageViews() {
-
-    for (ImageView iv : views) {
-      iv.setSmooth(true);
-      iv.setPreserveRatio(true);
+        startTimer();
     }
 
-    validateImageType();
-  }
+    /**
+     * Load the image views
+     */
+    public void loadImageViews() {
 
-  private void layoutViews() {
+        for (ImageView iv : views) {
+            iv.setSmooth(true);
+            iv.setPreserveRatio(true);
+        }
 
-    for (ImageView v : views) {
-      v.setFitWidth(getSize());
-      v.setFitHeight(getSize());
+        validateImageType();
     }
 
-    back.setTranslateX(-0.5 * getSize());
-    back.setTranslateY(-0.5 * getSize());
-    back.setTranslateZ(-0.5 * getSize());
+    private void layoutViews() {
 
-    front.setTranslateX(-0.5 * getSize());
-    front.setTranslateY(-0.5 * getSize());
-    front.setTranslateZ(0.5 * getSize());
-    front.setRotationAxis(Rotate.Z_AXIS);
-    front.setRotate(-180);
-    front.getTransforms().add(new Rotate(180, front.getFitHeight() / 2, 0, 0, Rotate.X_AXIS));
-    front.setTranslateY(front.getTranslateY() - getSize());
+        for (ImageView v : views) {
+            v.setFitWidth(getSize());
+            v.setFitHeight(getSize());
+        }
 
-    up.setTranslateX(-0.5 * getSize());
-    up.setTranslateY(-1 * getSize());
-    up.setRotationAxis(Rotate.X_AXIS);
-    up.setRotate(-90);
+        back.setTranslateX(-0.5 * getSize());
+        back.setTranslateY(-0.5 * getSize());
+        back.setTranslateZ(-0.5 * getSize());
 
-    down.setTranslateX(-0.5 * getSize());
-    down.setTranslateY(0);
-    down.setRotationAxis(Rotate.X_AXIS);
-    down.setRotate(90);
+        front.setTranslateX(-0.5 * getSize());
+        front.setTranslateY(-0.5 * getSize());
+        front.setTranslateZ(0.5 * getSize());
+        front.setRotationAxis(Rotate.Z_AXIS);
+        front.setRotate(-180);
+        front.getTransforms().add(new Rotate(180, front.getFitHeight() / 2, 0, 0, Rotate.X_AXIS));
+        front.setTranslateY(front.getTranslateY() - getSize());
 
-    left.setTranslateX(-1 * getSize());
-    left.setTranslateY(-0.5 * getSize());
-    left.setRotationAxis(Rotate.Y_AXIS);
-    left.setRotate(90);
+        up.setTranslateX(-0.5 * getSize());
+        up.setTranslateY(-1 * getSize());
+        up.setRotationAxis(Rotate.X_AXIS);
+        up.setRotate(-90);
 
-    right.setTranslateX(0);
-    right.setTranslateY(-0.5 * getSize());
-    right.setRotationAxis(Rotate.Y_AXIS);
-    right.setRotate(-90);
+        down.setTranslateX(-0.5 * getSize());
+        down.setTranslateY(0);
+        down.setRotationAxis(Rotate.X_AXIS);
+        down.setRotate(90);
 
-  }
+        left.setTranslateX(-1 * getSize());
+        left.setTranslateY(-0.5 * getSize());
+        left.setRotationAxis(Rotate.Y_AXIS);
+        left.setRotate(90);
 
-  /**
-   * for single image creates viewports and sets all views(image) to singleImg for multiple... sets images per view.
-   */
-  private void validateImageType() {
-    switch (imageType) {
-    case SINGLE:
-      loadSingleImageViewports();
-      break;
-    case MULTIPLE:
-      setMultipleImages();
-      break;
+        right.setTranslateX(0);
+        right.setTranslateY(-0.5 * getSize());
+        right.setRotationAxis(Rotate.Y_AXIS);
+        right.setRotate(-90);
     }
-  }
 
-  private void loadSingleImageViewports() {
-    layoutViews();
-    double width = singleImg.getWidth();
-    double height = singleImg.getHeight();
+    /**
+     * for single image creates viewports and sets all views(image) to singleImg for multiple... sets images per view.
+     */
+    private void validateImageType() {
+        setMultipleImages();
+    }
 
-    // simple check to see if cells will be square
-    if (width / 4 != height / 3) {
-      throw new UnsupportedOperationException("Image does not comply with size constraints");
+    private void setMultipleImages() {
+        GraphicsUtils.PlatformHelper.run(() -> {
+            layoutViews();
+            front.setImage(frontImg);
+            right.setImage(rightImg);
+            back.setImage(backImg);
+            left.setImage(leftImg);
+            up.setImage(upImg);
+            down.setImage(downImg);
+
+        });
     }
-    double cellSize = singleImg.getWidth() - singleImg.getHeight();
 
-    recalculateSize(cellSize);
+    /**
+     * Start the UI timer for updates
+     */
+    public void startTimer() {
+        AnimationTimer timer = new AnimationTimer() {
+            @Override
+            public void handle(long now) {
+                Transform ct = (camera != null) ? camera.getLocalToSceneTransform() : null;
+                if (ct != null) {
+                    affine.setTx(ct.getTx());
+                    affine.setTy(ct.getTy());
+                    affine.setTz(ct.getTz());
+                }
+            }
+        };
+        timer.start();
+    }
 
-    double topx = cellSize;
-    double topy = 0;
-    double botx = cellSize;
-    double boty = cellSize * 2;
-    double leftx = 0;
-    double lefty = cellSize;
-    double rightx = cellSize * 2;
-    double righty = cellSize;
-    double fwdx = cellSize;
-    double fwdy = cellSize;
-    double backx = cellSize * 3;
-    double backy = cellSize;
+    public final double getSize() {
+        return size.get();
+    }
 
-    // add top padding x+, y+, width-, height
-    up.setViewport(new Rectangle2D(topx, topy, cellSize, cellSize));
+    private final DoubleProperty size = new SimpleDoubleProperty();
 
-    // add left padding x, y+, width, height-
-    left.setViewport(new Rectangle2D(leftx, lefty, cellSize - 1, cellSize - 1));
-
-    // add front padding x+, y+, width-, height
-    front.setViewport(new Rectangle2D(fwdx, fwdy, cellSize, cellSize));
-
-    // add right padding x, y+, width, height-
-    right.setViewport(new Rectangle2D(rightx, righty, cellSize, cellSize));
-
-    // add back padding x, y+, width, height-
-    back.setViewport(new Rectangle2D(backx + 1, backy - 1, cellSize - 1, cellSize - 1));
-
-    // add bottom padding x+, y, width-, height-
-    down.setViewport(new Rectangle2D(botx, boty, cellSize, cellSize));
-
-    for (ImageView v : views) {
-      v.setImage(singleImg);
+    public ImageView[] getViews() {
+        return views;
     }
-  }
-
-  private void recalculateSize(double cell) {
-    double factor = Math.floor(getSize() / cell);
-    setSize(cell * factor);
-  }
-
-  public synchronized void setImage(BufferedImage img, int position) {
-    views[position].setImage(GraphicsUtils.convertBufferedImage2JavaFXImage(img));
-
-  }
-
-  private void setMultipleImages() {
-    GraphicsUtils.PlatformHelper.run(() -> {
-      layoutViews();
-      front.setImage(frontImg);
-      right.setImage(rightImg);
-      back.setImage(backImg);
-      left.setImage(leftImg);
-      up.setImage(upImg);
-      down.setImage(downImg);
-
-    });
-  }
-
-  public void startTimer() {
-    timer = new AnimationTimer() {
-      @Override
-      public void handle(long now) {
-        Transform ct = (camera != null) ? camera.getLocalToSceneTransform() : null;
-        if (ct != null) {
-          affine.setTx(ct.getTx());
-          affine.setTy(ct.getTy());
-          affine.setTz(ct.getTz());
-        }
-      }
-    };
-    timer.start();
-  }
-
-  public final double getSize() {
-    return size.get();
-  }
-
-  public final void setSize(double value) {
-    size.set(value);
-  } /*
-    * Properties
-    */
-
-  private final DoubleProperty size = new SimpleDoubleProperty() {
-    @Override
-    protected void invalidated() {
-      switch (imageType) {
-      case SINGLE:
-        layoutViews();
-        break;
-      case MULTIPLE:
-        break;
-      }
-
-    }
-  };
-
-  public DoubleProperty sizeProperty() {
-    return size;
-  }
-
-  public ImageView[] getViews() {
-    return views;
-  }
-
-  public enum CubemapBoxImageType {
-    MULTIPLE, SINGLE
-  }
-
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapBuilder.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapBuilder.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapBuilder.java	(revision 36228)
@@ -2,9 +2,11 @@
 package org.openstreetmap.josm.plugins.streetside.cubemap;
 
+import java.awt.geom.AffineTransform;
+import java.awt.image.AffineTransformOp;
 import java.awt.image.BufferedImage;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.EnumSet;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -15,373 +17,355 @@
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.logging.Logger;
+import java.util.stream.Collectors;
 
 import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.plugins.streetside.CubeMapTileXY;
 import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
-import org.openstreetmap.josm.plugins.streetside.StreetsideCubemap;
 import org.openstreetmap.josm.plugins.streetside.StreetsideDataListener;
+import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
+import org.openstreetmap.josm.plugins.streetside.cache.StreetsideCache;
 import org.openstreetmap.josm.plugins.streetside.gui.StreetsideViewerDialog;
 import org.openstreetmap.josm.plugins.streetside.gui.imageinfo.StreetsideViewerPanel;
-import org.openstreetmap.josm.plugins.streetside.gui.imageinfo.ThreeSixtyDegreeViewerPanel;
 import org.openstreetmap.josm.plugins.streetside.utils.GraphicsUtils;
 import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
 import org.openstreetmap.josm.tools.Logging;
 
+import javafx.embed.swing.SwingFXUtils;
 import javafx.scene.image.Image;
-import javafx.scene.image.ImageView;
-
+
+/**
+ * Build a cubemap
+ */
 // JavaFX access in Java 8
-public class CubemapBuilder implements ITileDownloadingTaskListener, StreetsideDataListener {
-
-  private static final Logger LOGGER = Logger.getLogger(CubemapBuilder.class.getCanonicalName());
-
-  private static CubemapBuilder instance;
-  protected boolean isBuilding;
-  private StreetsideCubemap cubemap;
-  private long startTime;
-
-  private Map<String, BufferedImage> tileImages = new ConcurrentHashMap<>();
-  private ExecutorService pool;
-
-  private int currentTileCount = 0;
-
-  private CubemapBuilder() {
-    // private constructor to avoid instantiation
-  }
-
-  public static CubemapBuilder getInstance() {
-    if (instance == null) {
-      instance = new CubemapBuilder();
-    }
-    return instance;
-  }
-
-  /**
-   * @return true, iff the singleton instance is present
-   */
-  public static boolean hasInstance() {
-    return CubemapBuilder.instance != null;
-  }
-
-  /**
-   * Destroys the unique instance of the class.
-   */
-  public static synchronized void destroyInstance() {
-    CubemapBuilder.instance = null;
-  }
-
-  /**
-   * @return the tileImages
-   */
-  public Map<String, BufferedImage> getTileImages() {
-    return tileImages;
-  }
-
-  /**
-   * @param tileImages the tileImages to set
-   */
-  public void setTileImages(Map<String, BufferedImage> tileImages) {
-    this.tileImages = tileImages;
-  }
-
-  /**
-   * Fired when any image is added to the database.
-   */
-  @Override
-  public void imagesAdded() {
-    // Not implemented by the CubemapBuilder
-  }
-
-  /**
-   * Fired when the selected image is changed by something different from
-   * manually clicking on the icon.
-   *
-   * @param oldImage Old selected {@link StreetsideAbstractImage}
-   * @param newImage New selected {@link StreetsideAbstractImage}
-   * @see StreetsideDataListener
-   */
-  @Override
-  public void selectedImageChanged(StreetsideAbstractImage oldImage, StreetsideAbstractImage newImage) {
-
-    startTime = System.currentTimeMillis();
-
-    if (newImage != null) {
-
-      cubemap = null;
-      cubemap = new StreetsideCubemap(newImage.getId(), newImage.getLatLon(), newImage.getHe());
-      currentTileCount = 0;
-      resetTileImages();
-
-      // download cubemap images in different threads and then subsequently
-      // set the cubeface images in JavaFX
-      downloadCubemapImages(cubemap.getId());
-
-      long runTime = (System.currentTimeMillis() - startTime) / 1000;
-      if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-        LOGGER.log(Logging.LEVEL_DEBUG, MessageFormat
-            .format("Completed downloading tiles for {0} in {1} seconds.", newImage.getId(), runTime));
-      }
-    }
-  }
-
-  public void reload(String imageId) {
-    if (cubemap != null && imageId.equals(cubemap.getId())) {
-      tileImages = new HashMap<>();
-      downloadCubemapImages(imageId);
-    }
-  }
-
-  public void downloadCubemapImages(String imageId) {
-    ThreeSixtyDegreeViewerPanel panel360 = StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel();
-    if (panel360 != null && panel360.getScene() != panel360.getLoadingScene()) {
-      panel360.setScene(panel360.getLoadingScene());
-    }
-
-    final int maxThreadCount = Boolean.TRUE.equals(StreetsideProperties.DOWNLOAD_CUBEFACE_TILES_TOGETHER.get()) ? 6
-        : 6 * CubemapUtils.getMaxCols() * CubemapUtils.getMaxRows();
-
-    int fails = 0;
-
-    // TODO: message for progress bar
-    String[] message = new String[2];
-    message[0] = MessageFormat.format("Downloading Streetside imagery for {0}", imageId);
-    message[1] = "Wait for completion…….";
-
-    long startTime = System.currentTimeMillis();
-
-    if (!CubemapBuilder.getInstance().getTileImages().keySet().isEmpty()) {
-      pool.shutdownNow();
-      CubemapBuilder.getInstance().resetTileImages();
-    }
-
-    try {
-
-      pool = Executors.newFixedThreadPool(maxThreadCount);
-      List<Callable<List<String>>> tasks = new ArrayList<>(maxThreadCount);
-
-      if (Boolean.TRUE.equals(StreetsideProperties.DOWNLOAD_CUBEFACE_TILES_TOGETHER.get())) {
-        EnumSet.allOf(CubemapUtils.CubemapFaces.class).forEach(face -> {
-          String tileId = imageId + face.getValue();
-          tasks.add(new TileDownloadingTask(tileId));
+public final class CubemapBuilder implements ITileDownloadingTaskListener, StreetsideDataListener {
+
+    private static final Logger LOGGER = Logger.getLogger(CubemapBuilder.class.getCanonicalName());
+
+    private static CubemapBuilder instance;
+    boolean isBuilding;
+    private StreetsideAbstractImage cubemap;
+    private long startTime;
+
+    private final Map<CubeMapTileXY, BufferedImage> tileImages = new ConcurrentHashMap<>();
+    private final ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor();
+    private final List<Future<?>> lastFutures = new ArrayList<>();
+
+    private final AtomicInteger currentTileCount = new AtomicInteger();
+
+    private CubemapBuilder() {
+        // private constructor to avoid instantiation
+    }
+
+    public static synchronized CubemapBuilder getInstance() {
+        if (instance == null) {
+            instance = new CubemapBuilder();
+        }
+        return instance;
+    }
+
+    /**
+     * Destroys the unique instance of the class.
+     */
+    public static synchronized void destroyInstance() {
+        CubemapBuilder.instance = null;
+    }
+
+    /**
+     * Get the current tile images
+     * @return the tileImages
+     */
+    public Map<CubeMapTileXY, BufferedImage> getTileImages() {
+        return tileImages;
+    }
+
+    /**
+     * Set the tile images to show
+     * @param tileImages the tileImages to set
+     */
+    public void setTileImages(Map<CubeMapTileXY, BufferedImage> tileImages) {
+        synchronized (this.tileImages) {
+            this.tileImages.clear();
+            this.tileImages.putAll(tileImages);
+        }
+    }
+
+    /**
+     * Add an entry to the tile images
+     * @param key The key to use
+     * @param value The value to use
+     */
+    private void addTileImage(CubeMapTileXY key, BufferedImage value) {
+        synchronized (this.tileImages) {
+            this.tileImages.put(key, value);
+        }
+    }
+
+    /**
+     * Fired when any image is added to the database.
+     */
+    @Override
+    public void imagesAdded() {
+        // Not implemented by the CubemapBuilder
+    }
+
+    /**
+     * Fired when the selected image is changed by something different from
+     * manually clicking on the icon.
+     *
+     * @param oldImage Old selected {@link StreetsideImage}
+     * @param newImage New selected {@link StreetsideImage}
+     * @see StreetsideDataListener
+     */
+    @Override
+    public void selectedImageChanged(StreetsideImage oldImage, StreetsideImage newImage) {
+
+        startTime = System.currentTimeMillis();
+
+        if (newImage != null) {
+
+            cubemap = newImage;
+            currentTileCount.set(0);
+            resetTileImages();
+
+            // download cubemap images in different threads and then subsequently
+            // set the cubeface images in JavaFX
+            downloadCubemapImages(cubemap);
+
+            long runTime = (System.currentTimeMillis() - startTime) / 1000;
+            if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
+                LOGGER.log(Logging.LEVEL_DEBUG, "Completed downloading tiles for {0} in {1} seconds.",
+                        new Object[] { newImage.id(), runTime });
+            }
+        }
+    }
+
+    /**
+     * Reload an image
+     * @param image The image to reload -- nothing happens if it is not the current image
+     */
+    public void reload(StreetsideAbstractImage image) {
+        if (cubemap != null && image.id().equals(cubemap.id())) {
+            this.tileImages.clear();
+            downloadCubemapImages(image);
+        }
+    }
+
+    /**
+     * Download the cubemap images for the specified image
+     * @param image The streetside image to get the cubemap for
+     * @return The images (FIXME: Currently returns an empty map)
+     */
+    public Map<CubeMapTileXY, StreetsideCache> downloadCubemapImages(StreetsideAbstractImage image) {
+        final var panel360 = StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel();
+        if (panel360 != null && panel360.getScene() != panel360.getLoadingScene()) {
+            panel360.setScene(panel360.getLoadingScene());
+        }
+
+        final int maxThreadCount = CubemapUtils.NUM_SIDES * CubemapUtils.getMaxCols(image)
+                * CubemapUtils.getMaxRows(image);
+
+        // TODO: message for progress bar
+        // final var message = new String[2];
+        // message[0] = MessageFormat.format("Downloading Streetside imagery for {0}", image.id());
+        // message[1] = "Wait for completion…….";
+
+        final long startTimeDownloadCubemapImages = System.currentTimeMillis();
+
+        if (!CubemapBuilder.getInstance().getTileImages().keySet().isEmpty()) {
+            CubemapBuilder.getInstance().resetTileImages();
+        }
+
+        List<Callable<List<String>>> tasks = new ArrayList<>(maxThreadCount);
+
+        if (Boolean.TRUE.equals(StreetsideProperties.DOWNLOAD_CUBEFACE_TILES_TOGETHER.get())) {
+            EnumSet.allOf(CubemapUtils.CubemapFaces.class)
+                    .forEach(face -> tasks.add(new TileDownloadingTask(image, face, new CubeMapTileXY(face, 0, 0))));
+        } else {
+            final var zoom = Boolean.TRUE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get())
+                    // launch 16-tiled (high-res) downloading tasks
+                    ? image.zoomMax()
+                    // launch 4-tiled (low-res) downloading tasks . . .
+                    : image.zoomMin();
+            // download all imagery for each cubeface at once
+            for (var face : CubemapUtils.CubemapFaces.values()) {
+                tasks.addAll(
+                        image.getFaceTiles(face, zoom).map(f -> new TileDownloadingTask(image, face, f.a)).toList());
+            }
+        }
+        // finish preparing tasks for invocation
+
+        // execute tasks
+        MainApplication.worker.submit(() -> {
+            try {
+                final List<Future<List<String>>> results;
+                synchronized (lastFutures) {
+                    lastFutures.forEach(f -> f.cancel(true));
+                    lastFutures.clear();
+                    // Timeout after a minute to avoid blocking the worker thread.
+                    results = pool.invokeAll(tasks, 1, TimeUnit.MINUTES);
+                    lastFutures.addAll(results);
+                }
+
+                if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
+                    waitForCompletedTasks(results, startTimeDownloadCubemapImages);
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
+            }
         });
-      } else {
-
-        // launch 4-tiled (low-res) downloading tasks . . .
-        if (Boolean.FALSE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get())) {
-          // download all imagery for each cubeface at once
-
-          for (int i = 0; i < CubemapUtils.NUM_SIDES; i++) {
-            int tileNr = 0;
-            for (int j = 0; j < CubemapUtils.getMaxCols(); j++) {
-              for (int k = 0; k < CubemapUtils.getMaxRows(); k++) {
-
-                String tileId = imageId + CubemapUtils.getFaceNumberForCount(i) + tileNr++;
-                tasks.add(new TileDownloadingTask(tileId));
-              }
-            }
-          }
-          // launch 16-tiled (high-res) downloading tasks
-        } else if (Boolean.TRUE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get())) {
-
-          for (int i = 0; i < CubemapUtils.NUM_SIDES; i++) {
-            for (int j = 0; j < CubemapUtils.getMaxCols(); j++) {
-              for (int k = 0; k < CubemapUtils.getMaxRows(); k++) {
-
-                String tileId = imageId + CubemapUtils.getFaceNumberForCount(i) + j + k;
-                tasks.add(new TileDownloadingTask(tileId));
-              }
-            }
-          }
-        }
-      } // finish preparing tasks for invocation
-
-      // execute tasks
-      MainApplication.worker.submit(() -> {
-        try {
-          List<Future<List<String>>> results = pool.invokeAll(tasks);
-
-          if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get()) && results != null) {
-            for (Future<List<String>> ff : results) {
-              try {
+
+        long stopTime = System.currentTimeMillis();
+        long runTime = stopTime - startTimeDownloadCubemapImages;
+
+        if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
+            LOGGER.log(Logging.LEVEL_DEBUG, "Tile imagery downloading tasks completed in {0} seconds.", runTime / 1000);
+        }
+
+        return Collections.emptyMap(); // FIXME: Actually return something for cancelling
+    }
+
+    /**
+     * Wait for completed tasks and log errors
+     * @param results The list of tasks to wait for
+     * @param startTimeDownloadCubemapImages The time that downloads started
+     * @throws InterruptedException If this thread was interrupted (see {@link Future#get()})
+     */
+    private static void waitForCompletedTasks(List<Future<List<String>>> results, long startTimeDownloadCubemapImages)
+            throws InterruptedException {
+        for (Future<List<String>> ff : results) {
+            try {
+                LOGGER.log(Logging.LEVEL_DEBUG, "Completed tile downloading task {0} in {1} seconds.", new Object[] {
+                        ff.get(), (System.currentTimeMillis() - startTimeDownloadCubemapImages) / 1000 });
+            } catch (ExecutionException e) {
+                LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
+            }
+        }
+    }
+
+    /**
+     * Fired when a TileDownloadingTask has completed downloading an image tile. When all the tiles for the Cubemap
+     * have been downloaded, the CubemapBuilder assembles the cubemap.
+     *
+     * @param image The image that the task has finished downloading for
+     * @param tileXY
+     *      the complete quadKey of the imagery tile, including cubeface and row/column in quaternary.
+     * @param bufferedImage The image for the tile
+     * @see TileDownloadingTask
+     */
+    @Override
+    public void tileAdded(StreetsideAbstractImage image, CubeMapTileXY tileXY, BufferedImage bufferedImage) {
+        // determine whether four tiles have been set for each of the
+        // six cubemap faces. If so, build the images for the faces
+        // and set the views in the cubemap box.
+
+        if (!cubemap.id().equals(image.id())) {
+            return;
+        }
+        this.addTileImage(tileXY, bufferedImage);
+
+        if (currentTileCount.incrementAndGet() == (CubemapUtils.NUM_SIDES * CubemapUtils.getMaxCols(image)
+                * CubemapUtils.getMaxRows(image))) {
+            if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
+                long endTime = System.currentTimeMillis();
+                long runTime = (endTime - startTime) / 1000;
                 LOGGER.log(Logging.LEVEL_DEBUG,
-                    MessageFormat.format("Completed tile downloading task {0} in {1} seconds.",
-                        ff.get().toString(), (System.currentTimeMillis() - startTime) / 1000));
-              } catch (ExecutionException e) {
-                LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
-              }
-            }
-          }
-        } catch (InterruptedException e) {
-          LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
-        }
-      });
-    } catch (Exception ee) {
-      fails++;
-      LOGGER.log(Logging.LEVEL_ERROR, ee, () -> "Error loading tile for image " + imageId);
-    }
-
-    long stopTime = System.currentTimeMillis();
-    long runTime = stopTime - startTime;
-
-    if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-      LOGGER.log(Logging.LEVEL_DEBUG,
-          MessageFormat.format("Tile imagery downloading tasks completed in {0} seconds.", runTime / 1000));
-    }
-
-    if (fails > 0) {
-      LOGGER.log(Logging.LEVEL_ERROR, fails + " downloading tasks failed!");
-    }
-  }
-
-  /**
-   * Fired when a TileDownloadingTask has completed downloading an image tile. When all of the tiles for the Cubemap
-   * have been downloaded, the CubemapBuilder assembles the cubemap.
-   *
-   * @param tileId
-   *      the complete quadKey of the imagery tile, including cubeface and row/column in quaternary.
-   * @see TileDownloadingTask
-   */
-  @Override
-  public void tileAdded(String tileId) {
-    // determine whether four tiles have been set for each of the
-    // six cubemap faces. If so, build the images for the faces
-    // and set the views in the cubemap box.
-
-    if (!tileId.startsWith(cubemap.getId())) {
-      return;
-    }
-
-    currentTileCount++;
-
-    if (currentTileCount == (CubemapUtils.NUM_SIDES * CubemapUtils.getMaxCols() * CubemapUtils.getMaxRows())) {
-      if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
+                        "{0} tile images ready for building cumbemap faces for cubemap {1} in {2} seconds.",
+                        new Object[] { currentTileCount.get(), CubemapBuilder.getInstance().getCubemap().id(),
+                                Long.toString(runTime) });
+            }
+
+            buildCubemapFaces();
+        }
+    }
+
+    /**
+     * Assembles the cubemap once all the tiles have been downloaded.
+     * <p>
+     * The tiles for each cubemap face are cropped and stitched together
+     * then the ImageViews of the cubemap are set with the new imagery.
+     *
+     * @see StreetsideAbstractImage
+     */
+    private void buildCubemapFaces() {
+        StreetsideViewerDialog.getInstance();
+        final var cubemapBox = StreetsideViewerPanel.getCubemapBox();
+        final var views = cubemapBox.getViews();
+
+        final var finalImages = new Image[CubemapUtils.NUM_SIDES];
+
+        // build 4-tiled cubemap faces and crop buffers
+        final int zoom = Boolean.TRUE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get())
+                ? this.cubemap.zoomMax()
+                : this.cubemap.zoomMin();
+        for (var i = 0; i < CubemapUtils.NUM_SIDES; i++) {
+            final var face = CubemapUtils.CubemapFaces.values()[i];
+            final Map<CubeMapTileXY, BufferedImage> tiles;
+            synchronized (this.tileImages) {
+                tiles = this.cubemap.getFaceTiles(face, zoom).filter(p -> tileImages.get(p.a) != null)
+                        .collect(Collectors.toMap(p -> p.a, p -> tileImages.get(p.a)));
+            }
+            if (this.cubemap.getFaceTiles(face, zoom).count() != tiles.size()) {
+                return; // We don't have all the sides yet. FIXME
+            }
+
+            final var tImage = GraphicsUtils.buildMultiTiledCubemapFaceImage(tiles, zoom);
+            // We need to flip the image horizontally (at least with JavaFX).
+            final var finalImg = new BufferedImage(tImage.getWidth(), tImage.getHeight(), tImage.getType());
+            final var g2d = finalImg.createGraphics();
+            final var translate = AffineTransform.getScaleInstance(-1, 1);
+            translate.concatenate(AffineTransform.getTranslateInstance(-finalImg.getWidth(), 0));
+            // rotate top/down cubeface 180 degrees - misalignment workaround (this could probably be worked around in
+            // CubemapBox).
+            g2d.drawImage(face == CubemapUtils.CubemapFaces.DOWN || face == CubemapUtils.CubemapFaces.UP
+                    ? GraphicsUtils.rotateImage(tImage)
+                    : tImage, new AffineTransformOp(translate, AffineTransformOp.TYPE_BILINEAR), 0, 0);
+            g2d.dispose();
+            finalImages[i] = SwingFXUtils.toFXImage(finalImg, null);
+        }
+
+        for (var i = 0; i < CubemapUtils.NUM_SIDES; i++) {
+            views[i].setImage(finalImages[i]);
+        }
+
+        StreetsideViewerDialog.getInstance().getStreetsideViewerPanel().revalidate();
+        StreetsideViewerDialog.getInstance().getStreetsideViewerPanel().repaint();
+
+        StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel()
+                .setScene(StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel().getCubemapScene());
+
+        StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel().revalidate();
+        StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel().repaint();
+
         long endTime = System.currentTimeMillis();
         long runTime = (endTime - startTime) / 1000;
-        LOGGER.log(Logging.LEVEL_DEBUG, MessageFormat.format(
-            "{0} tile images ready for building cumbemap faces for cubemap {1} in {2} seconds.",
-            currentTileCount, CubemapBuilder.getInstance().getCubemap().getId(), Long.toString(runTime)));
-      }
-
-      buildCubemapFaces();
-    }
-  }
-
-  /**
-   * Assembles the cubemap once all of the tiles have been downloaded.
-   * <p>
-   * The tiles for each cubemap face are cropped and stitched together
-   * then the ImageViews of the cubemap are set with the new imagery.
-   *
-   * @see     StreetsideCubemap
-   */
-  private void buildCubemapFaces() {
-    StreetsideViewerDialog.getInstance();
-    CubemapBox cmb = StreetsideViewerPanel.getCubemapBox();
-    ImageView[] views = cmb.getViews();
-
-    Image[] finalImages = new Image[CubemapUtils.NUM_SIDES];
-
-    // build 4-tiled cubemap faces and crop buffers
-    if (Boolean.FALSE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get())) {
-      for (int i = 0; i < CubemapUtils.NUM_SIDES; i++) {
-
-        BufferedImage[] faceTileImages = new BufferedImage[CubemapUtils.getMaxCols()
-            * CubemapUtils.getMaxRows()];
-
-        for (int j = 0; j < (CubemapUtils.getMaxCols() * CubemapUtils.getMaxRows()); j++) {
-          String tileId = getCubemap().getId() + CubemapUtils.getFaceNumberForCount(i) + j;
-          BufferedImage currentTile = tileImages.get(tileId);
-
-          faceTileImages[j] = currentTile;
-        }
-
-        BufferedImage finalImg = GraphicsUtils.buildMultiTiledCubemapFaceImage(faceTileImages);
-
-        // rotate top cubeface 180 degrees - misalignment workaround
-        if (i == 4) {
-          finalImg = GraphicsUtils.rotateImage(finalImg);
-        }
-        finalImages[i] = GraphicsUtils.convertBufferedImage2JavaFXImage(finalImg);
-      }
-      // build 16-tiled cubemap faces and crop buffers
-    } else if (Boolean.TRUE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get())) {
-      for (int i = 0; i < CubemapUtils.NUM_SIDES; i++) {
-
-        int tileCount = 0;
-
-        BufferedImage[] faceTileImages = new BufferedImage[Boolean.TRUE
-            .equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get()) ? 16 : 4];
-
-        for (int j = 0; j < CubemapUtils.getMaxCols(); j++) {
-          for (int k = 0; k < CubemapUtils.getMaxRows(); k++) {
-            String tileId = getCubemap().getId() + CubemapUtils.getFaceNumberForCount(i)
-                + CubemapUtils.convertDoubleCountNrto16TileNr(j + Integer.toString(k));
-            BufferedImage currentTile = tileImages.get(tileId);
-            faceTileImages[tileCount++] = currentTile;
-          }
-        }
-        BufferedImage finalImg = GraphicsUtils.buildMultiTiledCubemapFaceImage(faceTileImages);
-        // rotate top cubeface 180 degrees - misalignment workaround
-        if (i == 4) {
-          finalImg = GraphicsUtils.rotateImage(finalImg);
-        }
-        finalImages[i] = GraphicsUtils.convertBufferedImage2JavaFXImage(finalImg);
-      }
-    }
-
-    for (int i = 0; i < CubemapUtils.NUM_SIDES; i++) {
-      views[i].setImage(finalImages[i]);
-    }
-
-    StreetsideViewerDialog.getInstance().getStreetsideViewerPanel().revalidate();
-    StreetsideViewerDialog.getInstance().getStreetsideViewerPanel().repaint();
-
-    StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel()
-        .setScene(StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel().getCubemapScene());
-
-    StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel().revalidate();
-    StreetsideViewerPanel.getThreeSixtyDegreeViewerPanel().repaint();
-
-    long endTime = System.currentTimeMillis();
-    long runTime = (endTime - startTime) / 1000;
-
-    String message = MessageFormat.format(
-        "Completed downloading, assembling and setting cubemap imagery for cubemap {0} in  {1} seconds.",
-        cubemap.getId(), runTime);
-
-    if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-      LOGGER.log(Logging.LEVEL_DEBUG, message);
-    }
-
-    // reset count and image map after assembly
-    resetTileImages();
-    currentTileCount = 0;
-    isBuilding = false;
-  }
-
-  private void resetTileImages() {
-    tileImages = new HashMap<>();
-  }
-
-  /**
-   * @return the cubemap
-   */
-  public synchronized StreetsideCubemap getCubemap() {
-    return cubemap;
-  }
-
-  /**
-   * @param cubemap
-   *      the cubemap to set
-   */
-  public static void setCubemap(StreetsideCubemap cubemap) {
-    CubemapBuilder.getInstance().cubemap = cubemap;
-  }
-
-  /**
-   * @return the isBuilding
-   */
-  public boolean isBuilding() {
-    return isBuilding;
-  }
+
+        String message = MessageFormat.format(
+                "Completed downloading, assembling and setting cubemap imagery for cubemap {0} in  {1} seconds.",
+                cubemap.id(), runTime);
+
+        if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
+            LOGGER.log(Logging.LEVEL_DEBUG, message);
+        }
+
+        // reset count and image map after assembly
+        resetTileImages();
+        currentTileCount.set(0);
+        isBuilding = false;
+    }
+
+    private void resetTileImages() {
+        tileImages.clear();
+    }
+
+    /**
+     * Get the current image that is providing the cubemap data
+     * @return the cubemap
+     */
+    public synchronized StreetsideAbstractImage getCubemap() {
+        return cubemap;
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapUtils.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapUtils.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapUtils.java	(revision 36228)
@@ -2,306 +2,104 @@
 package org.openstreetmap.josm.plugins.streetside.cubemap;
 
-import java.text.MessageFormat;
-import java.util.HashMap;
-import java.util.Map;
 import java.util.logging.Logger;
-import java.util.stream.Stream;
 
+import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
 import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
 import org.openstreetmap.josm.tools.Logging;
 
-public class CubemapUtils {
+/**
+ * Utils for cubemaps
+ */
+public final class CubemapUtils {
 
-  public static final String TEST_IMAGE_ID = "00000000";
-  public static final int NUM_SIDES = 6;
-  private static final Logger LOGGER = Logger.getLogger(CubemapUtils.class.getCanonicalName());
-  // numerical base for decimal conversion (quaternary in the case of Streetside)
-  private static final int NUM_BASE = 4;
-  public static Map<String[], String> directionConversion = new HashMap<>();
-  public static Map<String, String> rowCol2StreetsideCellAddressMap = null;
+    public static final int NUM_SIDES = 6;
+    private static final Logger LOGGER = Logger.getLogger(CubemapUtils.class.getCanonicalName());
+    // numerical base for decimal conversion (quaternary in the case of Streetside)
+    private static final int NUM_BASE = 4;
 
-  // Intialize utility map for storing row to Streetside cell number conversions
-  static {
-
-    CubemapUtils.rowCol2StreetsideCellAddressMap = new HashMap<>();
-    CubemapUtils.rowCol2StreetsideCellAddressMap.put("00", "00");
-    CubemapUtils.rowCol2StreetsideCellAddressMap.put("01", "01");
-    CubemapUtils.rowCol2StreetsideCellAddressMap.put("02", "10");
-    CubemapUtils.rowCol2StreetsideCellAddressMap.put("03", "11");
-    CubemapUtils.rowCol2StreetsideCellAddressMap.put("10", "02");
-    CubemapUtils.rowCol2StreetsideCellAddressMap.put("11", "03");
-    CubemapUtils.rowCol2StreetsideCellAddressMap.put("12", "12");
-    CubemapUtils.rowCol2StreetsideCellAddressMap.put("13", "13");
-    CubemapUtils.rowCol2StreetsideCellAddressMap.put("20", "20");
-    CubemapUtils.rowCol2StreetsideCellAddressMap.put("21", "21");
-    CubemapUtils.rowCol2StreetsideCellAddressMap.put("22", "30");
-    CubemapUtils.rowCol2StreetsideCellAddressMap.put("23", "31");
-    CubemapUtils.rowCol2StreetsideCellAddressMap.put("30", "22");
-    CubemapUtils.rowCol2StreetsideCellAddressMap.put("31", "23");
-    CubemapUtils.rowCol2StreetsideCellAddressMap.put("32", "32");
-    CubemapUtils.rowCol2StreetsideCellAddressMap.put("33", "33");
-  }
-
-  private CubemapUtils() {
-    // Private constructor to avoid instantiation
-  }
-
-  public static int getMaxCols() {
-    return Boolean.TRUE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get()) ? 4 : 2;
-  }
-
-  public static int getMaxRows() {
-    return getMaxCols();
-  }
-
-  public static String convertDecimal2Quaternary(long inputNum) {
-    String res = null;
-    final StringBuilder sb = new StringBuilder();
-
-    if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-      LOGGER.log(Logging.LEVEL_DEBUG,
-          MessageFormat.format("convertDecimal2Quaternary input: {0}", Long.toString(inputNum)));
+    private CubemapUtils() {
+        // Private constructor to avoid instantiation
     }
 
-    while (inputNum > 0) {
-      sb.append(inputNum % CubemapUtils.NUM_BASE);
-      inputNum /= CubemapUtils.NUM_BASE;
+    /**
+     * Get the maximum columns for the image
+     * @param image The image to get the max columns for
+     * @return The maximum number of columns
+     */
+    public static int getMaxCols(StreetsideAbstractImage image) {
+        if (Boolean.TRUE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get())) {
+            return image.xCols(image.zoomMax());
+        }
+        return image.xCols(image.zoomMin());
     }
 
-    res = sb.reverse().toString();
-
-    if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-      LOGGER.log(Logging.LEVEL_DEBUG, MessageFormat.format("convertDecimal2Quaternary output: {0}", res));
+    /**
+     * Get the maximum rows for the image
+     * @param image The image to get the max rows for
+     * @return The maximum number of rows
+     */
+    public static int getMaxRows(StreetsideAbstractImage image) {
+        return getMaxCols(image);
     }
 
-    return res;
-  }
-
-  public static String convertQuaternary2Decimal(String inputNum) {
-
-    final String res;
-
-    if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-      LOGGER.log(Logging.LEVEL_DEBUG, MessageFormat.format("convertQuaternary2Decimal input: {0}", inputNum));
+    /**
+     * Convert a decimal number to a quaternary (base 4) number
+     * @param inputNum The number to convert
+     * @return The quaternary as a string
+     */
+    public static String convertDecimal2Quaternary(long inputNum) {
+        return Long.toString(inputNum, CubemapUtils.NUM_BASE);
     }
 
-    int len = inputNum.length();
-    int power = 1;
-    int num = 0;
-    int i;
-
-    for (i = len - 1; i >= 0; i--) {
-      if (Integer.parseInt(inputNum.substring(i, i + 1)) >= CubemapUtils.NUM_BASE) {
-        LOGGER.log(Logging.LEVEL_ERROR, "Error converting quadkey " + inputNum + " to decimal.");
-        return "000000000";
-      }
-
-      num += Integer.parseInt(inputNum.substring(i, i + 1)) * power;
-      power = power * CubemapUtils.NUM_BASE;
+    /**
+     * Convert a quaternary number to a standard decimal number
+     * @param inputNum The quaternary input number to convert
+     * @return The standard decimal number
+     */
+    public static String convertQuaternary2Decimal(String inputNum) {
+        try {
+            return Long.toString(Long.valueOf(inputNum, CubemapUtils.NUM_BASE));
+        } catch (NumberFormatException numberFormatException) {
+            Logging.trace(numberFormatException);
+            LOGGER.log(Logging.LEVEL_ERROR, "Error converting quadkey {0} to decimal.", inputNum);
+            return "000000000";
+        }
     }
 
-    res = Integer.toString(num);
+    /**
+     * The faces for a cubemap
+     */
+    public enum CubemapFaces {
+        FRONT("01"), RIGHT("02"), BACK("03"), LEFT("10"), UP("11"), DOWN("12");
 
-    if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-      LOGGER.log(Logging.LEVEL_DEBUG, MessageFormat.format("convertQuaternary2Decimal output: {0}", res));
+        private final String value;
+
+        CubemapFaces(String value) {
+            this.value = value;
+        }
+
+        /**
+         * The base value for the side
+         * @return The base value for the side (top is 1-1 in Streetside API docs, here it is 11)
+         */
+        public String getValue() {
+            return value;
+        }
+
+        /**
+         * The face id
+         * @return The face id
+         */
+        public String faceId() {
+            return this.value.substring(0, 1);
+        }
+
+        /**
+         * The starting tile id
+         * @return The starting tile id
+         */
+        public String startingTileId() {
+            return this.value.substring(1, 2);
+        }
     }
-
-    return res;
-  }
-
-  public static String getFaceNumberForCount(int count) {
-    final String res;
-
-    switch (count) {
-    case 0:
-      res = CubemapFaces.FRONT.getValue();
-      break;
-    case 1:
-      res = CubemapFaces.RIGHT.getValue();
-      break;
-    case 2:
-      res = CubemapFaces.BACK.getValue();
-      break;
-    case 3:
-      res = CubemapFaces.LEFT.getValue();
-      break;
-    case 4:
-      res = CubemapFaces.UP.getValue();
-      break;
-    case 5:
-      res = CubemapFaces.DOWN.getValue();
-      break;
-    default:
-      res = null;
-      break;
-    }
-    return res;
-  }
-
-  public static int getTileWidth() {
-    // 4-tiled cubemap imagery has a 2-pixel overlap; 16-tiled has a 1-pixel
-    // overlap
-    if (Boolean.FALSE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get())) {
-      return 255;
-    } else {
-      return 254;
-    }
-  }
-
-  public static int getTileHeight() {
-    // 4-tiled cubemap imagery has a 2-pixel overlap; 16-tiled has a 1-pixel
-    // overlap
-    if (Boolean.FALSE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get())) {
-      return 255;
-    } else {
-      return 254;
-    }
-  }
-
-  public static int getCount4FaceNumber(String faceString) {
-
-    final int tileAddress;
-
-    switch (faceString) {
-    // back
-    case "03":
-      tileAddress = 0;
-      break;
-    // down
-    case "12":
-      tileAddress = 1;
-      break;
-    // front
-    case "01":
-      tileAddress = 2;
-      break;
-    // left
-    case "10":
-      tileAddress = 3;
-      break;
-    // right
-    case "02":
-      tileAddress = 4;
-      break;
-    // up
-    case "11":
-      tileAddress = 5;
-      break;
-    default:
-      tileAddress = 6;
-      break;
-    }
-
-    return tileAddress;
-  }
-
-  public static String getFaceIdFromTileId(String tileId) {
-    // magic numbers - the face id is contained in the 16th and 17th positions
-    return tileId.substring(16, 18);
-  }
-
-  public static String convertDoubleCountNrto16TileNr(String countNr) {
-    String tileAddress;
-
-    switch (countNr) {
-    case "00":
-      tileAddress = "00";
-      break;
-    case "01":
-      tileAddress = "01";
-      break;
-    case "02":
-      tileAddress = "10";
-      break;
-    case "03":
-      tileAddress = "11";
-      break;
-    case "10":
-      tileAddress = "02";
-      break;
-    case "11":
-      tileAddress = "03";
-      break;
-    case "12":
-      tileAddress = "12";
-      break;
-    case "13":
-      tileAddress = "13";
-      break;
-    case "20":
-      tileAddress = "20";
-      break;
-    case "21":
-      tileAddress = "21";
-      break;
-    case "22":
-      tileAddress = "30";
-      break;
-    case "23":
-      tileAddress = "31";
-      break;
-    case "30":
-      tileAddress = "22";
-      break;
-    case "31":
-      tileAddress = "23";
-      break;
-    case "32":
-      tileAddress = "32";
-      break;
-    case "33":
-      tileAddress = "33";
-      break;
-    // shouldn't happen
-    default:
-      tileAddress = null;
-      break;
-    }
-
-    return tileAddress;
-  }
-
-  public enum CubefaceType {
-    ONE(1), FOUR(4), SIXTEEN(16);
-
-    private static final Map<Integer, CubefaceType> map = new HashMap<>();
-
-    static {
-      for (CubefaceType cubefaceType : CubefaceType.values()) {
-        map.put(cubefaceType.value, cubefaceType);
-      }
-    }
-
-    private final int value;
-
-    CubefaceType(int value) {
-      this.value = value;
-    }
-
-    public static CubefaceType valueOf(int cubefaceType) {
-      return map.get(cubefaceType);
-    }
-
-    public int getValue() {
-      return value;
-    }
-  }
-
-  public enum CubemapFaces {
-    FRONT("01"), RIGHT("02"), BACK("03"), LEFT("10"), UP("11"), DOWN("12");
-
-    private final String value;
-
-    CubemapFaces(String value) {
-      this.value = value;
-    }
-
-    public static Stream<CubemapFaces> stream() {
-      return Stream.of(CubemapFaces.values());
-    }
-
-    public String getValue() {
-      return value;
-    }
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/ITileDownloadingTaskListener.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/ITileDownloadingTaskListener.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/ITileDownloadingTaskListener.java	(revision 36228)
@@ -1,4 +1,9 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.plugins.streetside.cubemap;
+
+import java.awt.image.BufferedImage;
+
+import org.openstreetmap.josm.plugins.streetside.CubeMapTileXY;
+import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
 
 /**
@@ -10,10 +15,12 @@
 public interface ITileDownloadingTaskListener {
 
-  /**
-   * Fired when a cubemap tile image is downloaded by a download worker.
-   *
-   * @param imageId image id
-   */
-  void tileAdded(String imageId);
+    /**
+     * Fired when a cubemap tile image is downloaded by a download worker.
+     *
+     * @param image The image for which we are downloading tiles
+     * @param tile The tile that we downloaded an image for
+     * @param tileImage The image for the tile
+     */
+    void tileAdded(StreetsideAbstractImage image, CubeMapTileXY tile, BufferedImage tileImage);
 
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/TileDownloadingTask.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/TileDownloadingTask.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/cubemap/TileDownloadingTask.java	(revision 36228)
@@ -4,158 +4,137 @@
 import java.awt.image.BufferedImage;
 import java.io.IOException;
+import java.net.URI;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Semaphore;
 import java.util.logging.Logger;
 
 import javax.imageio.ImageIO;
 
-import org.openstreetmap.josm.plugins.streetside.cache.StreetsideCache;
+import org.openstreetmap.josm.plugins.streetside.CubeMapTileXY;
+import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
 import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
-import org.openstreetmap.josm.plugins.streetside.utils.StreetsideURL;
+import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.Logging;
 
+/**
+ * A task for downloading tiles of an image
+ */
 public class TileDownloadingTask implements Callable<List<String>> {
 
-  private static final Logger LOGGER = Logger.getLogger(TileDownloadingTask.class.getCanonicalName());
-  /**
-   * Listeners of the class.
-   */
-  private final List<ITileDownloadingTaskListener> listeners = new CopyOnWriteArrayList<>();
-  protected CubemapBuilder cb;
-  boolean cancelled;
-  private String tileId;
-  private StreetsideCache cache;
+    private static final Logger LOGGER = Logger.getLogger(TileDownloadingTask.class.getCanonicalName());
+    private static final Semaphore limitConnections = new Semaphore(
+            CubemapUtils.NUM_SIDES * Config.getPref().getInt("streetside.download.threads.per.side", 64)); // z3 is 8x8
+    /**
+     * Listeners of the class.
+     */
+    private final List<ITileDownloadingTaskListener> listeners = new CopyOnWriteArrayList<>();
+    private final StreetsideAbstractImage image;
+    private final CubemapUtils.CubemapFaces face;
+    private final CubeMapTileXY tileId;
+    protected final CubemapBuilder cb;
 
-  public TileDownloadingTask(String id) {
-    tileId = id;
-    cb = CubemapBuilder.getInstance();
-    addListener(CubemapBuilder.getInstance());
-  }
+    /**
+     * Download a tile
+     * @param image The image that we are downloading the tile for
+     * @param face The face for the image (since Streetside provides cubemaps)
+     * @param tileId The tile id to download
+     */
+    public TileDownloadingTask(StreetsideAbstractImage image, CubemapUtils.CubemapFaces face, CubeMapTileXY tileId) {
+        this.image = image;
+        this.face = face;
+        this.tileId = tileId;
+        this.cb = CubemapBuilder.getInstance();
+        addListener(this.cb);
+    }
 
-  /**
-   * Adds a new listener.
-   *
-   * @param lis Listener to be added.
-   */
-  public final void addListener(final ITileDownloadingTaskListener lis) {
-    listeners.add(lis);
-  }
+    /**
+     * Adds a new listener.
+     *
+     * @param lis Listener to be added.
+     */
+    public final void addListener(final ITileDownloadingTaskListener lis) {
+        listeners.add(lis);
+    }
 
-  /**
-   * @return the tileId
-   */
-  public String getId() {
-    return tileId;
-  }
+    /**
+     * Get the id for this task
+     * @return the tileId
+     */
+    public String getId() {
+        return tileId.toString();
+    }
 
-  /**
-   * @param id the tileId to set
-   */
-  public void setId(String id) {
-    tileId = id;
-  }
+    @Override
+    public List<String> call() {
+        try {
+            limitConnections.acquire();
+            try {
+                return download();
+            } finally {
+                limitConnections.release();
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            Logging.trace(e);
+            return Collections.emptyList();
+        }
+    }
 
-  /**
-   * @return the cache
-   */
-  public StreetsideCache getCache() {
-    return cache;
-  }
+    private List<String> download() {
+        List<String> res = new ArrayList<>();
 
-  /**
-   * @param cache the cache to set
-   */
-  public void setCache(StreetsideCache cache) {
-    this.cache = cache;
-  }
+        final int zoom = Boolean.TRUE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get())
+                ? this.image.zoomMax()
+                : this.image.zoomMin();
+        if (Boolean.TRUE.equals(StreetsideProperties.DOWNLOAD_CUBEFACE_TILES_TOGETHER.get())) {
+            // download all imagery for each cubeface at once
+            res.addAll(this.image.getFaceTiles(this.face, zoom).map(pair -> downloadTile(pair.a, pair.b)).toList());
+            // task downloads just one tile
+        } else {
+            res.add(downloadTile(tileId, this.image.getTile(this.face.getValue(), tileId.getQuadKey(zoom))));
+        }
+        return res;
+    }
 
-  /**
-   * @return the cb
-   */
-  public CubemapBuilder getCb() {
-    return cb;
-  }
+    private String downloadTile(CubeMapTileXY tile, String url) {
+        BufferedImage img;
 
-  /**
-   * @param cb the cb to set
-   */
-  public void setCb(CubemapBuilder cb) {
-    this.cb = cb;
-  }
+        long startTime = System.currentTimeMillis();
 
-  /**
-   * @param cancelled the cancelled to set
-   */
-  public void setCancelled(boolean cancelled) {
-    this.cancelled = cancelled;
-  }
+        try {
+            img = ImageIO.read(URI.create(url).toURL());
 
-  @Override
-  public List<String> call() throws Exception {
+            if (img == null) {
+                LOGGER.log(Logging.LEVEL_ERROR, "Download of BufferedImage {0} is null!", url);
+            }
 
-    List<String> res = new ArrayList<>();
+            fireTileAdded(this.image, tile, img);
 
-    if (Boolean.TRUE.equals(StreetsideProperties.DOWNLOAD_CUBEFACE_TILES_TOGETHER.get())) {
-      // download all imagery for each cubeface at once
-      if (Boolean.FALSE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get())) {
-        // download low-res imagery
-        int tileNr = 0;
-        for (int j = 0; j < CubemapUtils.getMaxCols(); j++) {
-          for (int k = 0; k < CubemapUtils.getMaxRows(); k++) {
-            String quadKey = tileId + tileNr++;
-            res.add(downloadTile(quadKey));
-          }
+            if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
+                long endTime = System.currentTimeMillis();
+                long runTime = (endTime - startTime) / 1000;
+                LOGGER.log(Logging.LEVEL_DEBUG, "Loaded image for {0} in {1} seconds.", new Object[] { url, runTime });
+            }
+        } catch (IOException e) {
+            LOGGER.log(Logging.LEVEL_ERROR, MessageFormat.format("Error downloading image for tileId {0}", url), e);
+            return null;
         }
-        // download high-res imagery
-      } else {
-        for (int j = 0; j < CubemapUtils.getMaxCols(); j++) {
-          for (int k = 0; k < CubemapUtils.getMaxRows(); k++) {
-            String quadKey = tileId + j + k;
-            res.add(downloadTile(quadKey));
-          }
-        }
-      }
-      // task downloads just one tile
-    } else {
-      res.add(downloadTile(tileId));
+        return url;
     }
-    return res;
-  }
 
-  private String downloadTile(String tileId) {
-    BufferedImage img;
-
-    long startTime = System.currentTimeMillis();
-
-    try {
-      img = ImageIO.read(StreetsideURL.VirtualEarth.streetsideTile(tileId, false));
-
-      if (img == null) {
-        LOGGER.log(Logging.LEVEL_ERROR, "Download of BufferedImage " + tileId + " is null!");
-      }
-
-      CubemapBuilder.getInstance().getTileImages().put(tileId, img);
-
-      fireTileAdded(tileId);
-
-      if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-        long endTime = System.currentTimeMillis();
-        long runTime = (endTime - startTime) / 1000;
-        LOGGER.log(Logging.LEVEL_DEBUG,
-            MessageFormat.format("Loaded image for {0} in {1} seconds.", tileId, runTime));
-      }
-    } catch (IOException e) {
-      LOGGER.log(Logging.LEVEL_ERROR, MessageFormat.format("Error downloading image for tileId {0}", tileId), e);
-      return null;
+    /**
+     * Fire a tile add event
+     * @param image The Streetside image we are getting tiled images for
+     * @param tile The tile in the image
+     * @param tileImage The actual tile image
+     */
+    private void fireTileAdded(StreetsideAbstractImage image, CubeMapTileXY tile, BufferedImage tileImage) {
+        listeners.stream().filter(Objects::nonNull).forEach(lis -> lis.tileAdded(image, tile, tileImage));
     }
-    return tileId;
-  }
-
-  private void fireTileAdded(String id) {
-    listeners.stream().filter(Objects::nonNull).forEach(lis -> lis.tileAdded(id));
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsideExportDialog.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsideExportDialog.java	(revision 36194)
+++ 	(revision )
@@ -1,151 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.streetside.gui;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.awt.Component;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-
-import javax.swing.AbstractAction;
-import javax.swing.BoxLayout;
-import javax.swing.ButtonGroup;
-import javax.swing.JButton;
-import javax.swing.JFileChooser;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JRadioButton;
-
-import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
-import org.openstreetmap.josm.plugins.streetside.StreetsideLayer;
-
-/**
- * GUI for exporting images.
- *
- * @author nokutu
- *
- */
-public class StreetsideExportDialog extends JPanel implements ActionListener {
-
-  private static final long serialVersionUID = -2746815082016025516L;
-  /**
-   * Button to export all downloaded images.
-   */
-  public final JRadioButton all;
-  /**
-   * Button to export all images in the sequence of the selected StreetsideImage.
-   */
-  public final JRadioButton sequence;
-  /**
-   * Button to export all images belonging to the selected
-   * {@link StreetsideImage} objects.
-   */
-  public final JRadioButton selected;
-  /**
-   * Group of button containing all the options.
-   */
-  public final ButtonGroup group;
-  private final JButton choose;
-  private final JLabel path;
-  private final JButton ok;
-  /**
-   * File chooser.
-   */
-  public JFileChooser chooser;
-
-  /**
-   * Main constructor.
-   *
-   * @param ok The button for to OK option.
-   */
-  public StreetsideExportDialog(JButton ok) {
-    this.ok = ok;
-    ok.setEnabled(false);
-
-    setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
-
-    RewriteButtonAction action = new RewriteButtonAction(this);
-    group = new ButtonGroup();
-    all = new JRadioButton(action);
-    all.setText(tr("Export all images"));
-    sequence = new JRadioButton(action);
-    sequence.setText(tr("Export selected sequence"));
-    selected = new JRadioButton(action);
-    selected.setText(tr("Export selected images"));
-    group.add(all);
-    group.add(sequence);
-    group.add(selected);
-    // Some options are disabled depending on the circumstances
-    sequence.setEnabled(StreetsideLayer.getInstance().getData().getSelectedImage() instanceof StreetsideImage);
-    if (StreetsideLayer.getInstance().getData().getMultiSelectedImages().isEmpty()) {
-      selected.setEnabled(false);
-    }
-
-    path = new JLabel(tr("Select a directory"));
-    choose = new JButton(tr("Explore"));
-    choose.addActionListener(this);
-
-    // All options belong to the same JPanel so the are in line.
-    JPanel jpanel = new JPanel();
-    jpanel.setLayout(new BoxLayout(jpanel, BoxLayout.PAGE_AXIS));
-    jpanel.add(all);
-    jpanel.add(sequence);
-    jpanel.add(selected);
-    jpanel.setAlignmentX(Component.CENTER_ALIGNMENT);
-    path.setAlignmentX(Component.CENTER_ALIGNMENT);
-    choose.setAlignmentX(Component.CENTER_ALIGNMENT);
-
-    add(jpanel);
-    add(path);
-    add(choose);
-  }
-
-  /**
-   * Creates the folder chooser GUI.
-   */
-  @Override
-  public void actionPerformed(ActionEvent e) {
-    chooser = new JFileChooser();
-    chooser.setCurrentDirectory(new java.io.File(System.getProperty("user.home")));
-    chooser.setDialogTitle(tr("Select a directory"));
-    chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
-    chooser.setAcceptAllFileFilterUsed(false);
-
-    if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
-      path.setText(chooser.getSelectedFile().toString());
-      updateUI();
-      ok.setEnabled(true);
-    }
-  }
-
-  /**
-   * Enables/disables some parts of the panel depending if the rewrite button is
-   * active.
-   *
-   * @author nokutu
-   */
-  public class RewriteButtonAction extends AbstractAction {
-
-    private static final long serialVersionUID = 1035332841101190301L;
-    private final StreetsideExportDialog dlg;
-    private String lastPath;
-
-    /**
-     * Main constructor.
-     *
-     * @param dlg Parent dialog.
-     */
-    public RewriteButtonAction(StreetsideExportDialog dlg) {
-      this.dlg = dlg;
-    }
-
-    @SuppressWarnings("synthetic-access")
-    @Override
-    public void actionPerformed(ActionEvent arg0) {
-      choose.setEnabled(true);
-      if (lastPath != null) {
-        dlg.path.setText(lastPath);
-      }
-    }
-  }
-}
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsideImageDisplay.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsideImageDisplay.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsideImageDisplay.java	(revision 36228)
@@ -12,5 +12,4 @@
 import java.awt.Point;
 import java.awt.Rectangle;
-import java.awt.Shape;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
@@ -21,6 +20,5 @@
 import java.awt.geom.Rectangle2D;
 import java.awt.image.BufferedImage;
-import java.util.ArrayList;
-import java.util.Collection;
+import java.io.Serial;
 
 import javax.swing.JComponent;
@@ -28,7 +26,4 @@
 import org.openstreetmap.josm.plugins.streetside.StreetsideLayer;
 import org.openstreetmap.josm.plugins.streetside.actions.StreetsideDownloadAction;
-import org.openstreetmap.josm.plugins.streetside.model.ImageDetection;
-import org.openstreetmap.josm.plugins.streetside.model.MapObject;
-import org.openstreetmap.josm.plugins.streetside.utils.StreetsideColorScheme;
 import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
 
@@ -43,492 +38,448 @@
 public class StreetsideImageDisplay extends JComponent {
 
-  private static final long serialVersionUID = -3188274185432686201L;
-
-  private final Collection<ImageDetection> detections = new ArrayList<>();
-
-  /**
-   * The image currently displayed
-   */
-  private volatile BufferedImage image;
-
-  /**
-   * The rectangle (in image coordinates) of the image that is visible. This
-   * rectangle is calculated each time the zoom is modified
-   */
-  private volatile Rectangle visibleRect;
-
-  /**
-   * When a selection is done, the rectangle of the selection (in image
-   * coordinates)
-   */
-  private Rectangle selectedRect;
-
-  /**
-   * Main constructor.
-   */
-  public StreetsideImageDisplay() {
-    ImgDisplayMouseListener mouseListener = new ImgDisplayMouseListener();
-    addMouseListener(mouseListener);
-    addMouseWheelListener(mouseListener);
-    addMouseMotionListener(mouseListener);
-
-    StreetsideProperties.SHOW_DETECTED_SIGNS.addListener(valChanged -> repaint());
-  }
-
-  private static Point getCenterImgCoord(Rectangle visibleRect) {
-    return new Point(visibleRect.x + visibleRect.width / 2, visibleRect.y + visibleRect.height / 2);
-  }
-
-  /**
-   * calculateDrawImageRectangle
-   *
-   * @param imgRect  the part of the image that should be drawn (in image coordinates)
-   * @param compRect the part of the component where the image should be drawn (in
-   *         component coordinates)
-   * @return the part of compRect with the same width/height ratio as the image
-   */
-  private static Rectangle calculateDrawImageRectangle(Rectangle imgRect, Rectangle compRect) {
-    int x = 0;
-    int y = 0;
-    int w = compRect.width;
-    int h = compRect.height;
-    int wFact = w * imgRect.height;
-    int hFact = h * imgRect.width;
-    if (wFact != hFact) {
-      if (wFact > hFact) {
-        w = hFact / imgRect.height;
-        x = (compRect.width - w) / 2;
-      } else {
-        h = wFact / imgRect.width;
-        y = (compRect.height - h) / 2;
-      }
-    }
-    return new Rectangle(x + compRect.x, y + compRect.y, w, h);
-  }
-
-  private static void checkVisibleRectPos(Image image, Rectangle visibleRect) {
-    if (visibleRect.x < 0) {
-      visibleRect.x = 0;
-    }
-    if (visibleRect.y < 0) {
-      visibleRect.y = 0;
-    }
-    if (visibleRect.x + visibleRect.width > image.getWidth(null)) {
-      visibleRect.x = image.getWidth(null) - visibleRect.width;
-    }
-    if (visibleRect.y + visibleRect.height > image.getHeight(null)) {
-      visibleRect.y = image.getHeight(null) - visibleRect.height;
-    }
-  }
-
-  private static void checkVisibleRectSize(Image image, Rectangle visibleRect) {
-    if (visibleRect.width > image.getWidth(null)) {
-      visibleRect.width = image.getWidth(null);
-    }
-    if (visibleRect.height > image.getHeight(null)) {
-      visibleRect.height = image.getHeight(null);
-    }
-  }
-
-  /**
-   * Sets a new picture to be displayed.
-   *
-   * @param image    The picture to be displayed.
-   * @param detections image detections
-   */
-  public void setImage(BufferedImage image, Collection<ImageDetection> detections) {
-    synchronized (this) {
-      this.image = image;
-      this.detections.clear();
-      if (detections != null) {
-        this.detections.addAll(detections);
-      }
-      selectedRect = null;
-      if (image != null)
-        visibleRect = new Rectangle(0, 0, image.getWidth(null), image.getHeight(null));
-    }
-    repaint();
-  }
-
-  /**
-   * Returns the picture that is being displayed
-   *
-   * @return The picture that is being displayed.
-   */
-  public BufferedImage getImage() {
-    return image;
-  }
-
-  /**
-   * Paints the visible part of the picture.
-   */
-  @Override
-  public void paintComponent(Graphics g) {
-    Image image;
-    Rectangle visibleRect;
-    synchronized (this) {
-      image = this.image;
-      visibleRect = this.visibleRect;
-    }
-    if (image == null) {
-      g.setColor(Color.black);
-      String noImageStr = StreetsideLayer.hasInstance() ? tr("No image selected")
-          : tr("Press \"{0}\" to download images", StreetsideDownloadAction.SHORTCUT.getKeyText());
-      Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(noImageStr, g);
-      Dimension size = getSize();
-      g.drawString(noImageStr, (int) ((size.width - noImageSize.getWidth()) / 2),
-          (int) ((size.height - noImageSize.getHeight()) / 2));
-    } else {
-      Rectangle target = calculateDrawImageRectangle(visibleRect);
-      g.drawImage(image, target.x, target.y, target.x + target.width, target.y + target.height, visibleRect.x,
-          visibleRect.y, visibleRect.x + visibleRect.width, visibleRect.y + visibleRect.height, null);
-      if (selectedRect != null) {
-        Point topLeft = img2compCoord(visibleRect, selectedRect.x, selectedRect.y);
-        Point bottomRight = img2compCoord(visibleRect, selectedRect.x + selectedRect.width,
-            selectedRect.y + selectedRect.height);
-        g.setColor(new Color(128, 128, 128, 180));
-        g.fillRect(target.x, target.y, target.width, topLeft.y - target.y);
-        g.fillRect(target.x, target.y, topLeft.x - target.x, target.height);
-        g.fillRect(bottomRight.x, target.y, target.x + target.width - bottomRight.x, target.height);
-        g.fillRect(target.x, bottomRight.y, target.width, target.y + target.height - bottomRight.y);
-        g.setColor(Color.black);
-        g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y);
-      }
-
-      if (Boolean.TRUE.equals(StreetsideProperties.SHOW_DETECTED_SIGNS.get())) {
-        Point upperLeft = img2compCoord(visibleRect, 0, 0);
-        Point lowerRight = img2compCoord(visibleRect, getImage().getWidth(), getImage().getHeight());
-
-        // Transformation, which can convert you a Shape relative to the unit square to a Shape relative to the Component
-        AffineTransform unit2compTransform = AffineTransform.getTranslateInstance(upperLeft.getX(),
-            upperLeft.getY());
-        unit2compTransform.concatenate(AffineTransform.getScaleInstance(lowerRight.getX() - upperLeft.getX(),
-            lowerRight.getY() - upperLeft.getY()));
-
-        final Graphics2D g2d = (Graphics2D) g;
-        g2d.setStroke(new BasicStroke(2));
-        for (ImageDetection d : detections) {
-          final Shape shape = d.getShape().createTransformedShape(unit2compTransform);
-          g2d.setColor(d.isTrafficSign() ? StreetsideColorScheme.IMAGEDETECTION_TRAFFICSIGN
-              : StreetsideColorScheme.IMAGEDETECTION_UNKNOWN);
-          g2d.draw(shape);
-          if (d.isTrafficSign()) {
-            g2d.drawImage(MapObject.getIcon(d.getValue()).getImage(), shape.getBounds().x,
-                shape.getBounds().y, shape.getBounds().width, shape.getBounds().height, null);
-          }
-        }
-      }
-    }
-  }
-
-  private Point img2compCoord(Rectangle visibleRect, int xImg, int yImg) {
-    Rectangle drawRect = calculateDrawImageRectangle(visibleRect);
-    return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) / visibleRect.width,
-        drawRect.y + ((yImg - visibleRect.y) * drawRect.height) / visibleRect.height);
-  }
-
-  private Point comp2imgCoord(Rectangle visibleRect, int xComp, int yComp) {
-    Rectangle drawRect = calculateDrawImageRectangle(visibleRect);
-    return new Point(visibleRect.x + ((xComp - drawRect.x) * visibleRect.width) / drawRect.width,
-        visibleRect.y + ((yComp - drawRect.y) * visibleRect.height) / drawRect.height);
-  }
-
-  private Rectangle calculateDrawImageRectangle(Rectangle visibleRect) {
-    return calculateDrawImageRectangle(visibleRect, new Rectangle(0, 0, getSize().width, getSize().height));
-  }
-
-  /**
-   * Zooms to 1:1 and, if it is already in 1:1, to best fit.
-   */
-  public void zoomBestFitOrOne() {
-    Image image;
-    Rectangle visibleRect;
-    synchronized (this) {
-      image = this.image;
-      visibleRect = this.visibleRect;
-    }
-    if (image == null)
-      return;
-    if (visibleRect.width != image.getWidth(null) || visibleRect.height != image.getHeight(null)) {
-      // The display is not at best fit. => Zoom to best fit
-      visibleRect = new Rectangle(0, 0, image.getWidth(null), image.getHeight(null));
-    } else {
-      // The display is at best fit => zoom to 1:1
-      Point center = getCenterImgCoord(visibleRect);
-      visibleRect = new Rectangle(center.x - getWidth() / 2, center.y - getHeight() / 2, getWidth(), getHeight());
-      checkVisibleRectPos(image, visibleRect);
-    }
-    synchronized (this) {
-      this.visibleRect = visibleRect;
-    }
-    repaint();
-  }
-
-  private class ImgDisplayMouseListener implements MouseListener, MouseWheelListener, MouseMotionListener {
-    private boolean mouseIsDragging;
-    private long lastTimeForMousePoint;
-    private Point mousePointInImg;
-
-    /**
-     * Zoom in and out, trying to preserve the point of the image that was under
-     * the mouse cursor at the same place
+    @Serial
+    private static final long serialVersionUID = -3188274185432686201L;
+
+    /**
+     * The image currently displayed
+     */
+    private volatile BufferedImage image;
+
+    /**
+     * The rectangle (in image coordinates) of the image that is visible. This
+     * rectangle is calculated each time the zoom is modified
+     */
+    private volatile Rectangle visibleRect;
+
+    /**
+     * When a selection is done, the rectangle of the selection (in image
+     * coordinates)
+     */
+    private Rectangle selectedRect;
+
+    /**
+     * Main constructor.
+     */
+    public StreetsideImageDisplay() {
+        ImgDisplayMouseListener mouseListener = new ImgDisplayMouseListener();
+        addMouseListener(mouseListener);
+        addMouseWheelListener(mouseListener);
+        addMouseMotionListener(mouseListener);
+
+        StreetsideProperties.SHOW_DETECTED_SIGNS.addListener(valChanged -> repaint());
+    }
+
+    private static Point getCenterImgCoord(Rectangle visibleRect) {
+        return new Point(visibleRect.x + visibleRect.width / 2, visibleRect.y + visibleRect.height / 2);
+    }
+
+    /**
+     * calculateDrawImageRectangle
+     *
+     * @param imgRect  the part of the image that should be drawn (in image coordinates)
+     * @param compRect the part of the component where the image should be drawn (in
+     *         component coordinates)
+     * @return the part of compRect with the same width/height ratio as the image
+     */
+    private static Rectangle calculateDrawImageRectangle(Rectangle imgRect, Rectangle compRect) {
+        int x = 0;
+        int y = 0;
+        int w = compRect.width;
+        int h = compRect.height;
+        int wFact = w * imgRect.height;
+        int hFact = h * imgRect.width;
+        if (wFact != hFact) {
+            if (wFact > hFact) {
+                w = hFact / imgRect.height;
+                x = (compRect.width - w) / 2;
+            } else {
+                h = wFact / imgRect.width;
+                y = (compRect.height - h) / 2;
+            }
+        }
+        return new Rectangle(x + compRect.x, y + compRect.y, w, h);
+    }
+
+    private static void checkVisibleRectPos(Image image, Rectangle visibleRect) {
+        if (visibleRect.x < 0) {
+            visibleRect.x = 0;
+        }
+        if (visibleRect.y < 0) {
+            visibleRect.y = 0;
+        }
+        if (visibleRect.x + visibleRect.width > image.getWidth(null)) {
+            visibleRect.x = image.getWidth(null) - visibleRect.width;
+        }
+        if (visibleRect.y + visibleRect.height > image.getHeight(null)) {
+            visibleRect.y = image.getHeight(null) - visibleRect.height;
+        }
+    }
+
+    private static void checkVisibleRectSize(Image image, Rectangle visibleRect) {
+        if (visibleRect.width > image.getWidth(null)) {
+            visibleRect.width = image.getWidth(null);
+        }
+        if (visibleRect.height > image.getHeight(null)) {
+            visibleRect.height = image.getHeight(null);
+        }
+    }
+
+    /**
+     * Sets a new picture to be displayed.
+     *
+     * @param image    The picture to be displayed.
+     */
+    public void setImage(BufferedImage image) {
+        synchronized (this) {
+            this.image = image;
+            selectedRect = null;
+            if (image != null)
+                visibleRect = new Rectangle(0, 0, image.getWidth(null), image.getHeight(null));
+        }
+        repaint();
+    }
+
+    /**
+     * Returns the picture that is being displayed
+     *
+     * @return The picture that is being displayed.
+     */
+    public BufferedImage getImage() {
+        return image;
+    }
+
+    /**
+     * Paints the visible part of the picture.
      */
     @Override
-    public void mouseWheelMoved(MouseWheelEvent e) {
-      Image image;
-      Rectangle visibleRect;
-      synchronized (StreetsideImageDisplay.this) {
-        image = getImage();
-        visibleRect = StreetsideImageDisplay.this.visibleRect;
-      }
-      mouseIsDragging = false;
-      selectedRect = null;
-      if (image != null && Math.min(getSize().getWidth(), getSize().getHeight()) > 0) {
-        // Calculate the mouse cursor position in image coordinates, so that
-        // we can center the zoom
-        // on that mouse position.
-        // To avoid issues when the user tries to zoom in on the image
-        // borders, this point is not calculated
-        // again if there was less than 1.5seconds since the last event.
-        if (e.getWhen() - lastTimeForMousePoint > 1500 || mousePointInImg == null) {
-          lastTimeForMousePoint = e.getWhen();
-          mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY());
-        }
-        // Set the zoom to the visible rectangle in image coordinates
-        if (e.getWheelRotation() > 0) {
-          visibleRect.width = visibleRect.width * 3 / 2;
-          visibleRect.height = visibleRect.height * 3 / 2;
+    public void paintComponent(Graphics g) {
+        Image image;
+        Rectangle visibleRect;
+        synchronized (this) {
+            image = this.image;
+            visibleRect = this.visibleRect;
+        }
+        if (image == null) {
+            g.setColor(Color.black);
+            String noImageStr = StreetsideLayer.hasInstance() ? tr("No image selected")
+                    : tr("Press \"{0}\" to download images", StreetsideDownloadAction.SHORTCUT.getKeyText());
+            Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(noImageStr, g);
+            Dimension size = getSize();
+            g.drawString(noImageStr, (int) ((size.width - noImageSize.getWidth()) / 2),
+                    (int) ((size.height - noImageSize.getHeight()) / 2));
         } else {
-          visibleRect.width = visibleRect.width * 2 / 3;
-          visibleRect.height = visibleRect.height * 2 / 3;
-        }
-        // Check that the zoom doesn't exceed 2:1
-        if (visibleRect.width < getSize().width / 2) {
-          visibleRect.width = getSize().width / 2;
-        }
-        if (visibleRect.height < getSize().height / 2) {
-          visibleRect.height = getSize().height / 2;
-        }
-        // Set the same ratio for the visible rectangle and the display area
-        int hFact = visibleRect.height * getSize().width;
-        int wFact = visibleRect.width * getSize().height;
-        if (hFact > wFact) {
-          visibleRect.width = hFact / getSize().height;
-        } else {
-          visibleRect.height = wFact / getSize().width;
-        }
-        // The size of the visible rectangle is limited by the image size.
-        checkVisibleRectSize(image, visibleRect);
-        // Set the position of the visible rectangle, so that the mouse
-        // cursor doesn't move on the image.
+            Rectangle target = calculateDrawImageRectangle(visibleRect);
+            g.drawImage(image, target.x, target.y, target.x + target.width, target.y + target.height, visibleRect.x,
+                    visibleRect.y, visibleRect.x + visibleRect.width, visibleRect.y + visibleRect.height, null);
+            if (selectedRect != null) {
+                Point topLeft = img2compCoord(visibleRect, selectedRect.x, selectedRect.y);
+                Point bottomRight = img2compCoord(visibleRect, selectedRect.x + selectedRect.width,
+                        selectedRect.y + selectedRect.height);
+                g.setColor(new Color(128, 128, 128, 180));
+                g.fillRect(target.x, target.y, target.width, topLeft.y - target.y);
+                g.fillRect(target.x, target.y, topLeft.x - target.x, target.height);
+                g.fillRect(bottomRight.x, target.y, target.x + target.width - bottomRight.x, target.height);
+                g.fillRect(target.x, bottomRight.y, target.width, target.y + target.height - bottomRight.y);
+                g.setColor(Color.black);
+                g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y);
+            }
+
+            if (Boolean.TRUE.equals(StreetsideProperties.SHOW_DETECTED_SIGNS.get())) {
+                Point upperLeft = img2compCoord(visibleRect, 0, 0);
+                Point lowerRight = img2compCoord(visibleRect, getImage().getWidth(), getImage().getHeight());
+
+                // Transformation, which can convert you a Shape relative to the unit square to a Shape relative to the Component
+                AffineTransform unit2compTransform = AffineTransform.getTranslateInstance(upperLeft.getX(),
+                        upperLeft.getY());
+                unit2compTransform.concatenate(AffineTransform.getScaleInstance(lowerRight.getX() - upperLeft.getX(),
+                        lowerRight.getY() - upperLeft.getY()));
+
+                final Graphics2D g2d = (Graphics2D) g;
+                g2d.setStroke(new BasicStroke(2));
+            }
+        }
+    }
+
+    private Point img2compCoord(Rectangle visibleRect, int xImg, int yImg) {
         Rectangle drawRect = calculateDrawImageRectangle(visibleRect);
-        visibleRect.x = mousePointInImg.x + ((drawRect.x - e.getX()) * visibleRect.width) / drawRect.width;
-        visibleRect.y = mousePointInImg.y + ((drawRect.y - e.getY()) * visibleRect.height) / drawRect.height;
-        // The position is also limited by the image size
-        checkVisibleRectPos(image, visibleRect);
-        synchronized (StreetsideImageDisplay.this) {
-          StreetsideImageDisplay.this.visibleRect = visibleRect;
-        }
-        StreetsideImageDisplay.this.repaint();
-      }
-    }
-
-    /**
-     * Center the display on the point that has been clicked
-     */
-    @Override
-    public void mouseClicked(MouseEvent e) {
-      // Move the center to the clicked point.
-      Image image;
-      Rectangle visibleRect;
-      synchronized (StreetsideImageDisplay.this) {
-        image = getImage();
-        visibleRect = StreetsideImageDisplay.this.visibleRect;
-      }
-      if (image != null && Math.min(getSize().getWidth(), getSize().getHeight()) > 0) {
-        if (e.getButton() == StreetsideProperties.PICTURE_OPTION_BUTTON.get()) {
-          if (!StreetsideImageDisplay.this.visibleRect
-              .equals(new Rectangle(0, 0, image.getWidth(null), image.getHeight(null)))) {
-            // Zooms to 1:1
-            StreetsideImageDisplay.this.visibleRect = new Rectangle(0, 0, image.getWidth(null),
-                image.getHeight(null));
-          } else {
-            // Zooms to best fit.
-            StreetsideImageDisplay.this.visibleRect = new Rectangle(0,
-                (image.getHeight(null) - (image.getWidth(null) * getHeight()) / getWidth()) / 2,
-                image.getWidth(null), (image.getWidth(null) * getHeight()) / getWidth());
-          }
-          StreetsideImageDisplay.this.repaint();
-          return;
-        } else if (e.getButton() != StreetsideProperties.PICTURE_DRAG_BUTTON.get()) {
-          return;
-        }
-        // Calculate the translation to set the clicked point the center of
-        // the view.
-        Point click = comp2imgCoord(visibleRect, e.getX(), e.getY());
-        Point center = getCenterImgCoord(visibleRect);
-        visibleRect.x += click.x - center.x;
-        visibleRect.y += click.y - center.y;
-        checkVisibleRectPos(image, visibleRect);
-        synchronized (StreetsideImageDisplay.this) {
-          StreetsideImageDisplay.this.visibleRect = visibleRect;
-        }
-        StreetsideImageDisplay.this.repaint();
-      }
-    }
-
-    /**
-     * Initialize the dragging, either with button 1 (simple dragging) or button
-     * 3 (selection of a picture part)
-     */
-    @Override
-    public void mousePressed(MouseEvent e) {
-      if (getImage() == null) {
-        mouseIsDragging = false;
-        selectedRect = null;
-        return;
-      }
-      Image image;
-      Rectangle visibleRect;
-      synchronized (StreetsideImageDisplay.this) {
-        image = StreetsideImageDisplay.this.image;
-        visibleRect = StreetsideImageDisplay.this.visibleRect;
-      }
-      if (image == null)
-        return;
-      if (e.getButton() == StreetsideProperties.PICTURE_DRAG_BUTTON.get()) {
-        mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY());
-        mouseIsDragging = true;
-        selectedRect = null;
-      } else if (e.getButton() == StreetsideProperties.PICTURE_ZOOM_BUTTON.get()) {
-        mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY());
-        checkPointInVisibleRect(mousePointInImg, visibleRect);
-        mouseIsDragging = false;
-        selectedRect = new Rectangle(mousePointInImg.x, mousePointInImg.y, 0, 0);
-        StreetsideImageDisplay.this.repaint();
-      } else {
-        mouseIsDragging = false;
-        selectedRect = null;
-      }
-    }
-
-    @Override
-    public void mouseDragged(MouseEvent e) {
-      if (!mouseIsDragging && selectedRect == null)
-        return;
-      Image image;
-      Rectangle visibleRect;
-      synchronized (StreetsideImageDisplay.this) {
-        image = getImage();
-        visibleRect = StreetsideImageDisplay.this.visibleRect;
-      }
-      if (image == null) {
-        mouseIsDragging = false;
-        selectedRect = null;
-        return;
-      }
-      if (mouseIsDragging) {
-        Point p = comp2imgCoord(visibleRect, e.getX(), e.getY());
-        visibleRect.x += mousePointInImg.x - p.x;
-        visibleRect.y += mousePointInImg.y - p.y;
-        checkVisibleRectPos(image, visibleRect);
-        synchronized (StreetsideImageDisplay.this) {
-          StreetsideImageDisplay.this.visibleRect = visibleRect;
-        }
-        StreetsideImageDisplay.this.repaint();
-      } else if (selectedRect != null) {
-        Point p = comp2imgCoord(visibleRect, e.getX(), e.getY());
-        checkPointInVisibleRect(p, visibleRect);
-        Rectangle rect = new Rectangle(p.x < mousePointInImg.x ? p.x : mousePointInImg.x,
-            p.y < mousePointInImg.y ? p.y : mousePointInImg.y,
-            p.x < mousePointInImg.x ? mousePointInImg.x - p.x : p.x - mousePointInImg.x,
-            p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y);
-        checkVisibleRectSize(image, rect);
-        checkVisibleRectPos(image, rect);
-        selectedRect = rect;
-        StreetsideImageDisplay.this.repaint();
-      }
-    }
-
-    @Override
-    public void mouseReleased(MouseEvent e) {
-      if (!mouseIsDragging && selectedRect == null)
-        return;
-      Image image;
-      synchronized (StreetsideImageDisplay.this) {
-        image = getImage();
-      }
-      if (image == null) {
-        mouseIsDragging = false;
-        selectedRect = null;
-        return;
-      }
-      if (mouseIsDragging) {
-        mouseIsDragging = false;
-      } else if (selectedRect != null) {
-        int oldWidth = selectedRect.width;
-        int oldHeight = selectedRect.height;
-        // Check that the zoom doesn't exceed 2:1
-        if (selectedRect.width < getSize().width / 2) {
-          selectedRect.width = getSize().width / 2;
-        }
-        if (selectedRect.height < getSize().height / 2) {
-          selectedRect.height = getSize().height / 2;
-        }
-        // Set the same ratio for the visible rectangle and the display
-        // area
-        int hFact = selectedRect.height * getSize().width;
-        int wFact = selectedRect.width * getSize().height;
-        if (hFact > wFact) {
-          selectedRect.width = hFact / getSize().height;
-        } else {
-          selectedRect.height = wFact / getSize().width;
-        }
-        // Keep the center of the selection
-        if (selectedRect.width != oldWidth) {
-          selectedRect.x -= (selectedRect.width - oldWidth) / 2;
-        }
-        if (selectedRect.height != oldHeight) {
-          selectedRect.y -= (selectedRect.height - oldHeight) / 2;
-        }
-        checkVisibleRectSize(image, selectedRect);
-        checkVisibleRectPos(image, selectedRect);
-        synchronized (StreetsideImageDisplay.this) {
-          visibleRect = selectedRect;
-        }
-        selectedRect = null;
-        StreetsideImageDisplay.this.repaint();
-      }
-    }
-
-    @Override
-    public void mouseEntered(MouseEvent e) {
-      // Do nothing, method is enforced by MouseListener
-    }
-
-    @Override
-    public void mouseExited(MouseEvent e) {
-      // Do nothing, method is enforced by MouseListener
-    }
-
-    @Override
-    public void mouseMoved(MouseEvent e) {
-      // Do nothing, method is enforced by MouseListener
-    }
-
-    private void checkPointInVisibleRect(Point p, Rectangle visibleRect) {
-      if (p.x < visibleRect.x) {
-        p.x = visibleRect.x;
-      }
-      if (p.x > visibleRect.x + visibleRect.width) {
-        p.x = visibleRect.x + visibleRect.width;
-      }
-      if (p.y < visibleRect.y) {
-        p.y = visibleRect.y;
-      }
-      if (p.y > visibleRect.y + visibleRect.height) {
-        p.y = visibleRect.y + visibleRect.height;
-      }
-    }
-  }
+        return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) / visibleRect.width,
+                drawRect.y + ((yImg - visibleRect.y) * drawRect.height) / visibleRect.height);
+    }
+
+    private Point comp2imgCoord(Rectangle visibleRect, int xComp, int yComp) {
+        Rectangle drawRect = calculateDrawImageRectangle(visibleRect);
+        return new Point(visibleRect.x + ((xComp - drawRect.x) * visibleRect.width) / drawRect.width,
+                visibleRect.y + ((yComp - drawRect.y) * visibleRect.height) / drawRect.height);
+    }
+
+    private Rectangle calculateDrawImageRectangle(Rectangle visibleRect) {
+        return calculateDrawImageRectangle(visibleRect, new Rectangle(0, 0, getSize().width, getSize().height));
+    }
+
+    private class ImgDisplayMouseListener implements MouseListener, MouseWheelListener, MouseMotionListener {
+        private boolean mouseIsDragging;
+        private long lastTimeForMousePoint;
+        private Point mousePointInImg;
+
+        /**
+         * Zoom in and out, trying to preserve the point of the image that was under
+         * the mouse cursor at the same place
+         */
+        @Override
+        public void mouseWheelMoved(MouseWheelEvent e) {
+            Image image;
+            Rectangle visibleRect;
+            synchronized (StreetsideImageDisplay.this) {
+                image = getImage();
+                visibleRect = StreetsideImageDisplay.this.visibleRect;
+            }
+            mouseIsDragging = false;
+            selectedRect = null;
+            if (image != null && Math.min(getSize().getWidth(), getSize().getHeight()) > 0) {
+                // Calculate the mouse cursor position in image coordinates, so that
+                // we can center the zoom
+                // on that mouse position.
+                // To avoid issues when the user tries to zoom in on the image
+                // borders, this point is not calculated
+                // again if there was less than 1.5seconds since the last event.
+                if (e.getWhen() - lastTimeForMousePoint > 1500 || mousePointInImg == null) {
+                    lastTimeForMousePoint = e.getWhen();
+                    mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY());
+                }
+                // Set the zoom to the visible rectangle in image coordinates
+                if (e.getWheelRotation() > 0) {
+                    visibleRect.width = visibleRect.width * 3 / 2;
+                    visibleRect.height = visibleRect.height * 3 / 2;
+                } else {
+                    visibleRect.width = visibleRect.width * 2 / 3;
+                    visibleRect.height = visibleRect.height * 2 / 3;
+                }
+                // Check that the zoom doesn't exceed 2:1
+                if (visibleRect.width < getSize().width / 2) {
+                    visibleRect.width = getSize().width / 2;
+                }
+                if (visibleRect.height < getSize().height / 2) {
+                    visibleRect.height = getSize().height / 2;
+                }
+                // Set the same ratio for the visible rectangle and the display area
+                int hFact = visibleRect.height * getSize().width;
+                int wFact = visibleRect.width * getSize().height;
+                if (hFact > wFact) {
+                    visibleRect.width = hFact / getSize().height;
+                } else {
+                    visibleRect.height = wFact / getSize().width;
+                }
+                // The size of the visible rectangle is limited by the image size.
+                checkVisibleRectSize(image, visibleRect);
+                // Set the position of the visible rectangle, so that the mouse
+                // cursor doesn't move on the image.
+                Rectangle drawRect = calculateDrawImageRectangle(visibleRect);
+                visibleRect.x = mousePointInImg.x + ((drawRect.x - e.getX()) * visibleRect.width) / drawRect.width;
+                visibleRect.y = mousePointInImg.y + ((drawRect.y - e.getY()) * visibleRect.height) / drawRect.height;
+                // The position is also limited by the image size
+                checkVisibleRectPos(image, visibleRect);
+                synchronized (StreetsideImageDisplay.this) {
+                    StreetsideImageDisplay.this.visibleRect = visibleRect;
+                }
+                StreetsideImageDisplay.this.repaint();
+            }
+        }
+
+        /**
+         * Center the display on the point that has been clicked
+         */
+        @Override
+        public void mouseClicked(MouseEvent e) {
+            // Move the center to the clicked point.
+            Image image;
+            Rectangle visibleRect;
+            synchronized (StreetsideImageDisplay.this) {
+                image = getImage();
+                visibleRect = StreetsideImageDisplay.this.visibleRect;
+            }
+            if (image != null && Math.min(getSize().getWidth(), getSize().getHeight()) > 0) {
+                if (e.getButton() == StreetsideProperties.PICTURE_OPTION_BUTTON.get()) {
+                    if (!StreetsideImageDisplay.this.visibleRect
+                            .equals(new Rectangle(0, 0, image.getWidth(null), image.getHeight(null)))) {
+                        // Zooms to 1:1
+                        StreetsideImageDisplay.this.visibleRect = new Rectangle(0, 0, image.getWidth(null),
+                                image.getHeight(null));
+                    } else {
+                        // Zooms to best fit.
+                        StreetsideImageDisplay.this.visibleRect = new Rectangle(0,
+                                (image.getHeight(null) - (image.getWidth(null) * getHeight()) / getWidth()) / 2,
+                                image.getWidth(null), (image.getWidth(null) * getHeight()) / getWidth());
+                    }
+                    StreetsideImageDisplay.this.repaint();
+                    return;
+                } else if (e.getButton() != StreetsideProperties.PICTURE_DRAG_BUTTON.get()) {
+                    return;
+                }
+                // Calculate the translation to set the clicked point the center of
+                // the view.
+                Point click = comp2imgCoord(visibleRect, e.getX(), e.getY());
+                Point center = getCenterImgCoord(visibleRect);
+                visibleRect.x += click.x - center.x;
+                visibleRect.y += click.y - center.y;
+                checkVisibleRectPos(image, visibleRect);
+                synchronized (StreetsideImageDisplay.this) {
+                    StreetsideImageDisplay.this.visibleRect = visibleRect;
+                }
+                StreetsideImageDisplay.this.repaint();
+            }
+        }
+
+        /**
+         * Initialize the dragging, either with button 1 (simple dragging) or button
+         * 3 (selection of a picture part)
+         */
+        @Override
+        public void mousePressed(MouseEvent e) {
+            if (getImage() == null) {
+                mouseIsDragging = false;
+                selectedRect = null;
+                return;
+            }
+            Image image;
+            Rectangle visibleRect;
+            synchronized (StreetsideImageDisplay.this) {
+                image = StreetsideImageDisplay.this.image;
+                visibleRect = StreetsideImageDisplay.this.visibleRect;
+            }
+            if (image == null)
+                return;
+            if (e.getButton() == StreetsideProperties.PICTURE_DRAG_BUTTON.get()) {
+                mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY());
+                mouseIsDragging = true;
+                selectedRect = null;
+            } else if (e.getButton() == StreetsideProperties.PICTURE_ZOOM_BUTTON.get()) {
+                mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY());
+                checkPointInVisibleRect(mousePointInImg, visibleRect);
+                mouseIsDragging = false;
+                selectedRect = new Rectangle(mousePointInImg.x, mousePointInImg.y, 0, 0);
+                StreetsideImageDisplay.this.repaint();
+            } else {
+                mouseIsDragging = false;
+                selectedRect = null;
+            }
+        }
+
+        @Override
+        public void mouseDragged(MouseEvent e) {
+            if (!mouseIsDragging && selectedRect == null)
+                return;
+            Image image;
+            Rectangle visibleRect;
+            synchronized (StreetsideImageDisplay.this) {
+                image = getImage();
+                visibleRect = StreetsideImageDisplay.this.visibleRect;
+            }
+            if (image == null) {
+                mouseIsDragging = false;
+                selectedRect = null;
+                return;
+            }
+            if (mouseIsDragging) {
+                Point p = comp2imgCoord(visibleRect, e.getX(), e.getY());
+                visibleRect.x += mousePointInImg.x - p.x;
+                visibleRect.y += mousePointInImg.y - p.y;
+                checkVisibleRectPos(image, visibleRect);
+                synchronized (StreetsideImageDisplay.this) {
+                    StreetsideImageDisplay.this.visibleRect = visibleRect;
+                }
+                StreetsideImageDisplay.this.repaint();
+            } else if (selectedRect != null) {
+                Point p = comp2imgCoord(visibleRect, e.getX(), e.getY());
+                checkPointInVisibleRect(p, visibleRect);
+                Rectangle rect = new Rectangle(Math.min(p.x, mousePointInImg.x), Math.min(p.y, mousePointInImg.y),
+                        p.x < mousePointInImg.x ? mousePointInImg.x - p.x : p.x - mousePointInImg.x,
+                        p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y);
+                checkVisibleRectSize(image, rect);
+                checkVisibleRectPos(image, rect);
+                selectedRect = rect;
+                StreetsideImageDisplay.this.repaint();
+            }
+        }
+
+        @Override
+        public void mouseReleased(MouseEvent e) {
+            if (!mouseIsDragging && selectedRect == null)
+                return;
+            Image image;
+            synchronized (StreetsideImageDisplay.this) {
+                image = getImage();
+            }
+            if (image == null) {
+                mouseIsDragging = false;
+                selectedRect = null;
+                return;
+            }
+            if (mouseIsDragging) {
+                mouseIsDragging = false;
+            } else if (selectedRect != null) {
+                int oldWidth = selectedRect.width;
+                int oldHeight = selectedRect.height;
+                // Check that the zoom doesn't exceed 2:1
+                if (selectedRect.width < getSize().width / 2) {
+                    selectedRect.width = getSize().width / 2;
+                }
+                if (selectedRect.height < getSize().height / 2) {
+                    selectedRect.height = getSize().height / 2;
+                }
+                // Set the same ratio for the visible rectangle and the display
+                // area
+                int hFact = selectedRect.height * getSize().width;
+                int wFact = selectedRect.width * getSize().height;
+                if (hFact > wFact) {
+                    selectedRect.width = hFact / getSize().height;
+                } else {
+                    selectedRect.height = wFact / getSize().width;
+                }
+                // Keep the center of the selection
+                if (selectedRect.width != oldWidth) {
+                    selectedRect.x -= (selectedRect.width - oldWidth) / 2;
+                }
+                if (selectedRect.height != oldHeight) {
+                    selectedRect.y -= (selectedRect.height - oldHeight) / 2;
+                }
+                checkVisibleRectSize(image, selectedRect);
+                checkVisibleRectPos(image, selectedRect);
+                synchronized (StreetsideImageDisplay.this) {
+                    visibleRect = selectedRect;
+                }
+                selectedRect = null;
+                StreetsideImageDisplay.this.repaint();
+            }
+        }
+
+        @Override
+        public void mouseEntered(MouseEvent e) {
+            // Do nothing, method is enforced by MouseListener
+        }
+
+        @Override
+        public void mouseExited(MouseEvent e) {
+            // Do nothing, method is enforced by MouseListener
+        }
+
+        @Override
+        public void mouseMoved(MouseEvent e) {
+            // Do nothing, method is enforced by MouseListener
+        }
+
+        private void checkPointInVisibleRect(Point p, Rectangle visibleRect) {
+            if (p.x < visibleRect.x) {
+                p.x = visibleRect.x;
+            }
+            if (p.x > visibleRect.x + visibleRect.width) {
+                p.x = visibleRect.x + visibleRect.width;
+            }
+            if (p.y < visibleRect.y) {
+                p.y = visibleRect.y;
+            }
+            if (p.y > visibleRect.y + visibleRect.height) {
+                p.y = visibleRect.y + visibleRect.height;
+            }
+        }
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsideImageTreeCellRenderer.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsideImageTreeCellRenderer.java	(revision 36194)
+++ 	(revision )
@@ -1,28 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.streetside.gui;
-
-import java.awt.Component;
-
-import javax.swing.Icon;
-import javax.swing.JTree;
-import javax.swing.tree.DefaultTreeCellRenderer;
-
-import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
-import org.openstreetmap.josm.tools.ImageProvider;
-
-/**
- * Renders an item in a {@link JTree} that represents a {@link StreetsideAbstractImage}.
- */
-public class StreetsideImageTreeCellRenderer extends DefaultTreeCellRenderer {
-  private static final long serialVersionUID = 5359276673450659572L;
-
-  private static final Icon ICON = new ImageProvider("mapicon").setMaxSize(16).get();
-
-  @Override
-  public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf,
-      int row, boolean hasFocus) {
-    super.getTreeCellRendererComponent(tree, value.toString(), sel, expanded, leaf, row, hasFocus);
-    setIcon(ICON);
-    return this;
-  }
-}
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsideMainDialog.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsideMainDialog.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsideMainDialog.java	(revision 36228)
@@ -9,8 +9,13 @@
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.io.Serial;
 import java.text.MessageFormat;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 import java.util.logging.Logger;
+import java.util.stream.Collectors;
 
 import javax.imageio.ImageIO;
@@ -21,4 +26,5 @@
 import javax.swing.SwingUtilities;
 
+import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
 import org.openstreetmap.josm.data.cache.CacheEntry;
 import org.openstreetmap.josm.data.cache.CacheEntryAttributes;
@@ -26,4 +32,5 @@
 import org.openstreetmap.josm.gui.SideButton;
 import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
+import org.openstreetmap.josm.plugins.streetside.CubeMapTileXY;
 import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
 import org.openstreetmap.josm.plugins.streetside.StreetsideDataListener;
@@ -34,5 +41,7 @@
 import org.openstreetmap.josm.plugins.streetside.actions.WalkThread;
 import org.openstreetmap.josm.plugins.streetside.cache.StreetsideCache;
+import org.openstreetmap.josm.plugins.streetside.cubemap.CubemapUtils;
 import org.openstreetmap.josm.plugins.streetside.gui.imageinfo.ImageInfoHelpPopup;
+import org.openstreetmap.josm.plugins.streetside.utils.GraphicsUtils;
 import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
 import org.openstreetmap.josm.tools.I18n;
@@ -48,501 +57,535 @@
 public final class StreetsideMainDialog extends ToggleDialog implements ICachedLoaderListener, StreetsideDataListener {
 
-  public static final String BASE_TITLE = I18n.marktr("Microsoft Streetside image");
-  private static final long serialVersionUID = 2645654786827812861L;
-  private static final Logger LOGGER = Logger.getLogger(StreetsideMainDialog.class.getCanonicalName());
-  private static final String MESSAGE_SEPARATOR = " — ";
-
-  private static StreetsideMainDialog instance;
-  /**
-   * Button used to jump to the image following the red line
-   */
-  public final SideButton redButton = new SideButton(new RedAction());
-  /**
-   * Button used to jump to the image following the blue line
-   */
-  public final SideButton blueButton = new SideButton(new BlueAction());
-  private final SideButton nextButton = new SideButton(new NextPictureAction());
-  private final SideButton previousButton = new SideButton(new PreviousPictureAction());
-  private final SideButton playButton = new SideButton(new PlayAction());
-  private final SideButton pauseButton = new SideButton(new PauseAction());
-  private final SideButton stopButton = new SideButton(new StopAction());
-  /**
-   * Object containing the shown image and that handles zoom and drag
-   */
-  public StreetsideImageDisplay streetsideImageDisplay;
-  public StreetsideCache thumbnailCache;
-  private volatile StreetsideAbstractImage image;
-  private ImageInfoHelpPopup imageInfoHelp;
-  private StreetsideCache imageCache;
-
-  private StreetsideMainDialog() {
-    super(I18n.tr(StreetsideMainDialog.BASE_TITLE), "streetside-main", I18n.tr("Open Streetside window"), null, 200,
-        true, StreetsidePreferenceSetting.class);
-    addShortcuts();
-
-    streetsideImageDisplay = new StreetsideImageDisplay();
-
-    blueButton.setForeground(Color.BLUE);
-    redButton.setForeground(Color.RED);
-
-    setMode(MODE.NORMAL);
-  }
-
-  /**
-   * Returns the unique instance of the class.
-   *
-   * @return The unique instance of the class.
-   */
-  public static synchronized StreetsideMainDialog getInstance() {
-    if (StreetsideMainDialog.instance == null) {
-      StreetsideMainDialog.instance = new StreetsideMainDialog();
-    }
-    return StreetsideMainDialog.instance;
-  }
-
-  /**
-   * @return true, iff the singleton instance is present
-   */
-  public static boolean hasInstance() {
-    return StreetsideMainDialog.instance != null;
-  }
-
-  /**
-   * Destroys the unique instance of the class.
-   */
-  public static synchronized void destroyInstance() {
-    StreetsideMainDialog.instance = null;
-  }
-
-  /**
-   * Adds the shortcuts to the buttons.
-   */
-  private void addShortcuts() {
-    nextButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("PAGE_DOWN"), "next");
-    nextButton.getActionMap().put("next", new NextPictureAction());
-    previousButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("PAGE_UP"),
-        "previous");
-    previousButton.getActionMap().put("previous", new PreviousPictureAction());
-    blueButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control PAGE_UP"),
-        "blue");
-    blueButton.getActionMap().put("blue", new BlueAction());
-    redButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control PAGE_DOWN"),
-        "red");
-    redButton.getActionMap().put("red", new RedAction());
-  }
-
-  public synchronized void setImageInfoHelp(ImageInfoHelpPopup popup) {
-    imageInfoHelp = popup;
-  }
-
-  /**
-   * Sets a new mode for the dialog.
-   *
-   * @param mode The mode to be set. Must not be {@code null}.
-   */
-  public void setMode(MODE mode) {
-    switch (mode) {
-    case WALK:
-      createLayout(streetsideImageDisplay, Arrays.asList(playButton, pauseButton, stopButton));
-      break;
-    case NORMAL:
-    default:
-      createLayout(streetsideImageDisplay, Arrays.asList(blueButton, previousButton, nextButton, redButton));
-      break;
-    }
-    disableAllButtons();
-    if (MODE.NORMAL == mode) {
-      updateImage();
-    }
-    revalidate();
-    repaint();
-  }
-
-  /**
-   * Downloads the full quality picture of the selected StreetsideImage and sets
-   * in the StreetsideImageDisplay object.
-   */
-  public synchronized void updateImage() {
-    updateImage(true);
-  }
-
-  /**
-   * Downloads the picture of the selected StreetsideImage and sets in the
-   * StreetsideImageDisplay object.
-   *
-   * @param fullQuality If the full quality picture must be downloaded or just the
-   *          thumbnail.
-   */
-  public synchronized void updateImage(boolean fullQuality) {
-    if (!SwingUtilities.isEventDispatchThread()) {
-      SwingUtilities.invokeLater(this::updateImage);
-    } else {
-      if (!StreetsideLayer.hasInstance()) {
-        return;
-      }
-      if (image == null) {
-        streetsideImageDisplay.setImage(null, null);
-        setTitle(I18n.tr(StreetsideMainDialog.BASE_TITLE));
-        return;
-      }
-
-      if (imageInfoHelp != null && StreetsideProperties.IMAGEINFO_HELP_COUNTDOWN.get() > 0
-          && imageInfoHelp.showPopup()) {
-        // Count down the number of times the popup will be displayed
-        StreetsideProperties.IMAGEINFO_HELP_COUNTDOWN
-            .put(StreetsideProperties.IMAGEINFO_HELP_COUNTDOWN.get() - 1);
-      }
-
-      if (image instanceof StreetsideImage) {
-        final StreetsideImage streetsideImage = (StreetsideImage) image;
-        // Downloads the thumbnail.
-        streetsideImageDisplay.setImage(null, null);
-        if (thumbnailCache != null) {
-          thumbnailCache.cancelOutstandingTasks();
-        }
-        thumbnailCache = new StreetsideCache(streetsideImage.getId(), StreetsideCache.Type.THUMBNAIL);
-        try {
-          thumbnailCache.submit(this, false);
-        } catch (final IOException e) {
-          LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
-        }
-
-        // Downloads the full resolution image.
-        if (fullQuality || new StreetsideCache(streetsideImage.getId(), StreetsideCache.Type.FULL_IMAGE)
-            .get() != null) {
-          if (imageCache != null) {
-            imageCache.cancelOutstandingTasks();
-          }
-          imageCache = new StreetsideCache(streetsideImage.getId(), StreetsideCache.Type.FULL_IMAGE);
-          try {
-            imageCache.submit(this, false);
-          } catch (final IOException e) {
-            LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
-          }
-        }
-      }
-      updateTitle();
-    }
-  }
-
-  /**
-   * Disables all the buttons in the dialog
-   */
-  private void disableAllButtons() {
-    nextButton.setEnabled(false);
-    previousButton.setEnabled(false);
-    blueButton.setEnabled(false);
-    redButton.setEnabled(false);
-  }
-
-  /**
-   * Updates the title of the dialog.
-   */
-  public synchronized void updateTitle() {
-    if (!SwingUtilities.isEventDispatchThread()) {
-      SwingUtilities.invokeLater(this::updateTitle);
-    } else if (image != null) {
-      final StringBuilder title = new StringBuilder(I18n.tr(StreetsideMainDialog.BASE_TITLE));
-      if (image instanceof StreetsideImage) {
-        title.append(StreetsideMainDialog.MESSAGE_SEPARATOR)
-            .append(MessageFormat.format("(heading {0}°)", Double.toString(image.getHe())));
-        setTitle(title.toString());
-      }
-    }
-  }
-
-  /**
-   * Returns the {@link StreetsideAbstractImage} object which is being shown.
-   *
-   * @return The {@link StreetsideAbstractImage} object which is being shown.
-   */
-  public synchronized StreetsideAbstractImage getImage() {
-    return image;
-  }
-
-  /**
-   * Sets a new StreetsideImage to be shown.
-   *
-   * @param image The image to be shown.
-   */
-  public synchronized void setImage(StreetsideAbstractImage image) {
-    this.image = image;
-  }
-
-  /**
-   * When the pictures are returned from the cache, they are set in the
-   * {@link StreetsideImageDisplay} object.
-   */
-  @Override
-  public void loadingFinished(final CacheEntry data, final CacheEntryAttributes attributes, final LoadResult result) {
-    if (!SwingUtilities.isEventDispatchThread()) {
-      SwingUtilities.invokeLater(() -> loadingFinished(data, attributes, result));
-
-    } else if (data != null && result == LoadResult.SUCCESS) {
-      try {
-        final BufferedImage img = ImageIO.read(new ByteArrayInputStream(data.getContent()));
-        if (img == null) {
-          return;
-        }
-        if (streetsideImageDisplay.getImage() == null
-            || img.getHeight() > streetsideImageDisplay.getImage().getHeight()) {
-          streetsideImageDisplay.setImage(img, null);
-        }
-      } catch (final IOException e) {
-        LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
-      }
-    }
-  }
-
-  /**
-   * Creates the layout of the dialog.
-   *
-   * @param data  The content of the dialog
-   * @param buttons The buttons where you can click
-   */
-  public void createLayout(Component data, List<SideButton> buttons) {
-    removeAll();
-    createLayout(data, true, buttons);
-    add(titleBar, BorderLayout.NORTH);
-  }
-
-  @Override
-  public void selectedImageChanged(StreetsideAbstractImage oldImage, StreetsideAbstractImage newImage) {
-    setImage(newImage);
-    if (newImage != null && newImage.getSequence() != null) {
-      if (newImage.next() != null) {
-        nextButton.setEnabled(true);
-      }
-      if (newImage.previous() != null) {
-        previousButton.setEnabled(true);
-      }
-    }
-    updateImage();
-  }
-
-  @Override
-  public void imagesAdded() {
-    // This method is enforced by StreetsideDataListener, but only selectedImageChanged() is needed
-  }
-
-  /**
-   * @return the streetsideImageDisplay
-   */
-  public StreetsideImageDisplay getStreetsideImageDisplay() {
-    return streetsideImageDisplay;
-  }
-
-  /**
-   * @param streetsideImageDisplay the streetsideImageDisplay to set
-   */
-  public void setStreetsideImageDisplay(StreetsideImageDisplay streetsideImageDisplay) {
-    this.streetsideImageDisplay = streetsideImageDisplay;
-  }
-
-  /**
-   * Buttons mode.
-   *
-   * @author nokutu
-   */
-  public enum MODE {
-    /**
-     * Standard mode to view pictures.
-     */
-    NORMAL,
-    /**
-     * Mode when in walk.
-     */
-    WALK
-  }
-
-  /**
-   * Action class form the next image button.
-   *
-   * @author nokutu
-   */
-  private static class NextPictureAction extends AbstractAction {
-
-    private static final long serialVersionUID = 6333692154558730392L;
-
-    /**
-     * Constructs a normal NextPictureAction
-     */
-    NextPictureAction() {
-      super(I18n.tr("Next picture"));
-      putValue(Action.SHORT_DESCRIPTION, I18n.tr("Shows the next picture in the sequence"));
-      new ImageProvider("help", "next").getResource().attachImageIcon(this, true);
-    }
-
+    public static final String BASE_TITLE = I18n.marktr("Microsoft Streetside image");
+    @Serial
+    private static final long serialVersionUID = 2645654786827812861L;
+    private static final Logger LOGGER = Logger.getLogger(StreetsideMainDialog.class.getCanonicalName());
+    private static final String MESSAGE_SEPARATOR = " — ";
+
+    private static StreetsideMainDialog instance;
+    /**
+     * Button used to jump to the image following the red line
+     */
+    public final SideButton redButton = new SideButton(new RedAction());
+    /**
+     * Button used to jump to the image following the blue line
+     */
+    public final SideButton blueButton = new SideButton(new BlueAction());
+    private final SideButton nextButton = new SideButton(new NextPictureAction());
+    private final SideButton previousButton = new SideButton(new PreviousPictureAction());
+    private final SideButton playButton = new SideButton(new PlayAction());
+    private final SideButton pauseButton = new SideButton(new PauseAction());
+    private final SideButton stopButton = new SideButton(new StopAction());
+    /**
+     * Object containing the shown image and that handles zoom and drag
+     */
+    public final StreetsideImageDisplay streetsideImageDisplay;
+    public StreetsideCache thumbnailCache;
+    private StreetsideAbstractImage image;
+    /** Used to avoid  rerendering images multiple times (once per tile, effectively) */
+    private long lastRenderedImageHash;
+    private ImageInfoHelpPopup imageInfoHelp;
+    private final Map<CubeMapTileXY, StreetsideCache> imageCache = new HashMap<>();
+
+    private StreetsideMainDialog() {
+        super(I18n.tr(StreetsideMainDialog.BASE_TITLE), "streetside-main", I18n.tr("Open Streetside window"), null, 200,
+                true, StreetsidePreferenceSetting.class);
+        addShortcuts();
+
+        streetsideImageDisplay = new StreetsideImageDisplay();
+
+        blueButton.setForeground(Color.BLUE);
+        redButton.setForeground(Color.RED);
+
+        setMode(MODE.NORMAL);
+    }
+
+    /**
+     * Returns the unique instance of the class.
+     *
+     * @return The unique instance of the class.
+     */
+    public static synchronized StreetsideMainDialog getInstance() {
+        if (StreetsideMainDialog.instance == null) {
+            StreetsideMainDialog.instance = new StreetsideMainDialog();
+        }
+        return StreetsideMainDialog.instance;
+    }
+
+    /**
+     * Check if there is an instance
+     * @return true, iff the singleton instance is present
+     */
+    public static boolean hasInstance() {
+        return StreetsideMainDialog.instance != null;
+    }
+
+    /**
+     * Destroys the unique instance of the class.
+     */
+    public static synchronized void destroyInstance() {
+        StreetsideMainDialog.instance = null;
+    }
+
+    /**
+     * Adds the shortcuts to the buttons.
+     */
+    private void addShortcuts() {
+        nextButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("PAGE_DOWN"), "next");
+        nextButton.getActionMap().put("next", new NextPictureAction());
+        previousButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("PAGE_UP"),
+                "previous");
+        previousButton.getActionMap().put("previous", new PreviousPictureAction());
+        blueButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control PAGE_UP"),
+                "blue");
+        blueButton.getActionMap().put("blue", new BlueAction());
+        redButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control PAGE_DOWN"),
+                "red");
+        redButton.getActionMap().put("red", new RedAction());
+    }
+
+    public synchronized void setImageInfoHelp(ImageInfoHelpPopup popup) {
+        imageInfoHelp = popup;
+    }
+
+    /**
+     * Sets a new mode for the dialog.
+     *
+     * @param mode The mode to be set. Must not be {@code null}.
+     */
+    public void setMode(MODE mode) {
+        switch (mode) {
+        case WALK:
+            createLayout(streetsideImageDisplay, Arrays.asList(playButton, pauseButton, stopButton));
+            break;
+        case NORMAL:
+        default:
+            createLayout(streetsideImageDisplay, Arrays.asList(blueButton, previousButton, nextButton, redButton));
+            break;
+        }
+        disableAllButtons();
+        if (MODE.NORMAL == mode) {
+            updateImage();
+        }
+        revalidate();
+        repaint();
+    }
+
+    /**
+     * Downloads the full quality picture of the selected StreetsideImage and sets
+     * in the StreetsideImageDisplay object.
+     */
+    public synchronized void updateImage() {
+        updateImage(true);
+    }
+
+    /**
+     * Downloads the picture of the selected StreetsideImage and sets in the
+     * StreetsideImageDisplay object.
+     *
+     * @param fullQuality If the full quality picture must be downloaded or just the
+     *          thumbnail.
+     */
+    public synchronized void updateImage(boolean fullQuality) {
+        if (!SwingUtilities.isEventDispatchThread()) {
+            SwingUtilities.invokeLater(this::updateImage);
+        } else {
+            if (!StreetsideLayer.hasInstance()) {
+                return;
+            }
+            if (image == null) {
+                streetsideImageDisplay.setImage(null);
+                setTitle(I18n.tr(StreetsideMainDialog.BASE_TITLE));
+                return;
+            }
+
+            if (imageInfoHelp != null && StreetsideProperties.IMAGEINFO_HELP_COUNTDOWN.get() > 0
+                    && imageInfoHelp.showPopup()) {
+                // Count down the number of times the popup will be displayed
+                StreetsideProperties.IMAGEINFO_HELP_COUNTDOWN
+                        .put(StreetsideProperties.IMAGEINFO_HELP_COUNTDOWN.get() - 1);
+            }
+
+            if (image instanceof StreetsideImage streetsideImage) {
+                // Downloads the thumbnail.
+                streetsideImageDisplay.setImage(null);
+                if (thumbnailCache != null) {
+                    thumbnailCache.cancelOutstandingTasks();
+                }
+                thumbnailCache = new StreetsideCache(streetsideImage.getThumbnail());
+                try {
+                    thumbnailCache.submit(this, false);
+                } catch (final IOException e) {
+                    LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
+                }
+
+                // Downloads the full resolution image.
+                if (fullQuality) {
+                    synchronized (this.imageCache) {
+                        this.imageCache.values().forEach(StreetsideCache::cancelOutstandingTasks);
+                        this.imageCache.clear();
+                        this.imageCache.putAll(
+                                streetsideImage.getFaceTiles(CubemapUtils.CubemapFaces.FRONT, streetsideImage.zoomMax())
+                                        .collect(Collectors.toMap(p -> p.a, p -> new StreetsideCache(p.b))));
+                        for (StreetsideCache cache : this.imageCache.values()) {
+                            try {
+                                cache.submit(this, false);
+                            } catch (final IOException e) {
+                                LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
+                            }
+                        }
+                    }
+                }
+            }
+            updateTitle();
+        }
+    }
+
+    /**
+     * Disables all the buttons in the dialog
+     */
+    private void disableAllButtons() {
+        nextButton.setEnabled(false);
+        previousButton.setEnabled(false);
+        blueButton.setEnabled(false);
+        redButton.setEnabled(false);
+    }
+
+    /**
+     * Updates the title of the dialog.
+     */
+    public synchronized void updateTitle() {
+        if (!SwingUtilities.isEventDispatchThread()) {
+            SwingUtilities.invokeLater(this::updateTitle);
+        } else if (image != null) {
+            final var title = new StringBuilder(I18n.tr(StreetsideMainDialog.BASE_TITLE));
+            if (image instanceof StreetsideImage) {
+                title.append(StreetsideMainDialog.MESSAGE_SEPARATOR)
+                        .append(MessageFormat.format("(heading {0}°)", Double.toString(image.heading())));
+                setTitle(title.toString());
+            }
+        }
+    }
+
+    /**
+     * Returns the {@link StreetsideAbstractImage} object which is being shown.
+     *
+     * @return The {@link StreetsideAbstractImage} object which is being shown.
+     */
+    public synchronized StreetsideAbstractImage getImage() {
+        return image;
+    }
+
+    /**
+     * Sets a new StreetsideImage to be shown.
+     *
+     * @param image The image to be shown.
+     */
+    public synchronized void setImage(StreetsideAbstractImage image) {
+        this.image = image;
+        this.lastRenderedImageHash = Long.MIN_VALUE;
+    }
+
+    /**
+     * When the pictures are returned from the cache, they are set in the
+     * {@link StreetsideImageDisplay} object.
+     */
     @Override
-    public void actionPerformed(ActionEvent e) {
-      StreetsideLayer.getInstance().getData().selectNext();
-    }
-  }
-
-  /**
-   * Action class for the previous image button.
-   *
-   * @author nokutu
-   */
-  private static class PreviousPictureAction extends AbstractAction {
-
-    private static final long serialVersionUID = 4390593660514657107L;
-
-    /**
-     * Constructs a normal PreviousPictureAction
-     */
-    PreviousPictureAction() {
-      super(I18n.tr("Previous picture"));
-      putValue(Action.SHORT_DESCRIPTION, I18n.tr("Shows the previous picture in the sequence"));
-      new ImageProvider("help", "previous").getResource().attachImageIcon(this, true);
+    public void loadingFinished(final CacheEntry data, final CacheEntryAttributes attributes, final LoadResult result) {
+        if (!SwingUtilities.isEventDispatchThread()) {
+            SwingUtilities.invokeLater(() -> loadingFinished(data, attributes, result));
+
+        } else if (data != null && result == LoadResult.SUCCESS) {
+            try {
+                final BufferedImage img = data instanceof BufferedImageCacheEntry bufferedData ? bufferedData.getImage()
+                        : ImageIO.read(new ByteArrayInputStream(data.getContent()));
+                if (img == null) {
+                    return;
+                }
+                synchronized (this.imageCache) {
+                    if (this.imageCache.isEmpty() || this.imageCache.size() == 1 || this.imageCache.containsKey(null)) {
+                        streetsideImageDisplay.setImage(img);
+                        return;
+                    }
+                    final var images = new HashMap<CubeMapTileXY, BufferedImage>(this.imageCache.size());
+                    for (var entry : this.imageCache.entrySet()) {
+                        images.put(entry.getKey(),
+                                Optional.ofNullable(entry.getValue()).map(StreetsideCache::get).map(e -> {
+                                    try {
+                                        return e.getImage();
+                                    } catch (IOException exception) {
+                                        Logging.trace(exception);
+                                    }
+                                    return null;
+                                }).orElse(null));
+                    }
+
+                    if (images.containsValue(null)) {
+                        return; // Not yet loaded, or something happened.
+                    }
+                    if (this.lastRenderedImageHash != images.hashCode()) {
+                        this.lastRenderedImageHash = images.hashCode();
+                        final var fullImage = GraphicsUtils.buildMultiTiledCubemapFaceImage(images,
+                                (int) Math.round(Math.log(images.size()) / Math.log(4)));
+                        streetsideImageDisplay.setImage(fullImage);
+                    }
+                }
+            } catch (final IOException e) {
+                LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
+            }
+        }
+    }
+
+    /**
+     * Creates the layout of the dialog.
+     *
+     * @param data  The content of the dialog
+     * @param buttons The buttons where you can click
+     */
+    public void createLayout(Component data, List<SideButton> buttons) {
+        removeAll();
+        createLayout(data, true, buttons);
+        add(titleBar, BorderLayout.NORTH);
     }
 
     @Override
-    public void actionPerformed(ActionEvent e) {
-      StreetsideLayer.getInstance().getData().selectPrevious();
-    }
-  }
-
-  /**
-   * Action class to jump to the image following the red line.
-   *
-   * @author nokutu
-   */
-  private static class RedAction extends AbstractAction {
-
-    private static final long serialVersionUID = -1244456062285831231L;
-
-    /**
-     * Constructs a normal RedAction
-     */
-    RedAction() {
-      putValue(Action.NAME, I18n.tr("Jump to red"));
-      putValue(Action.SHORT_DESCRIPTION, I18n.tr("Jumps to the picture at the other side of the red line"));
-      new ImageProvider("dialogs", "red").getResource().attachImageIcon(this, true);
+    public void selectedImageChanged(StreetsideImage oldImage, StreetsideImage newImage) {
+        setImage(newImage);
+        if (newImage != null) {
+            if (StreetsideLayer.getInstance().getData().next(newImage) != null) {
+                nextButton.setEnabled(true);
+            }
+            if (StreetsideLayer.getInstance().getData().previous(newImage) != null) {
+                previousButton.setEnabled(true);
+            }
+        }
+        updateImage();
     }
 
     @Override
-    public void actionPerformed(ActionEvent e) {
-      if (StreetsideMainDialog.getInstance().getImage() != null) {
-        StreetsideLayer.getInstance().getData()
-            .setSelectedImage(StreetsideLayer.getInstance().getNNearestImage(1), true);
-      }
-    }
-  }
-
-  /**
-   * Action class to jump to the image following the blue line.
-   *
-   * @author nokutu
-   */
-  private static class BlueAction extends AbstractAction {
-
-    private static final long serialVersionUID = 5951233534212838780L;
-
-    /**
-     * Constructs a normal BlueAction
-     */
-    BlueAction() {
-      putValue(Action.NAME, I18n.tr("Jump to blue"));
-      putValue(Action.SHORT_DESCRIPTION, I18n.tr("Jumps to the picture at the other side of the blue line"));
-      new ImageProvider("dialogs", "blue").getResource().attachImageIcon(this, true);
-    }
-
-    @Override
-    public void actionPerformed(ActionEvent e) {
-      if (StreetsideMainDialog.getInstance().getImage() != null) {
-        StreetsideLayer.getInstance().getData()
-            .setSelectedImage(StreetsideLayer.getInstance().getNNearestImage(2), true);
-      }
-    }
-  }
-
-  private static class StopAction extends AbstractAction implements WalkListener {
-
-    private static final long serialVersionUID = 8789972456611625341L;
-
-    private WalkThread thread;
-
-    /**
-     * Constructs a normal StopAction
-     */
-    StopAction() {
-      putValue(Action.NAME, I18n.tr("Stop"));
-      putValue(Action.SHORT_DESCRIPTION, I18n.tr("Stops the walk."));
-      new ImageProvider("dialogs/streetsideStop.png").getResource().attachImageIcon(this, true);
-      StreetsidePlugin.getStreetsideWalkAction().addListener(this);
-    }
-
-    @Override
-    public void actionPerformed(ActionEvent e) {
-      if (thread != null) {
-        thread.stopWalk();
-      }
-    }
-
-    @Override
-    public void walkStarted(WalkThread thread) {
-      this.thread = thread;
-    }
-  }
-
-  private static class PlayAction extends AbstractAction implements WalkListener {
-
-    private static final long serialVersionUID = -1572747020946842769L;
-
-    private transient WalkThread thread;
-
-    /**
-     * Constructs a normal PlayAction
-     */
-    PlayAction() {
-      putValue(Action.NAME, I18n.tr("Play"));
-      putValue(Action.SHORT_DESCRIPTION, I18n.tr("Continues with the paused walk."));
-      new ImageProvider("dialogs/streetsidePlay.png").getResource().attachImageIcon(this, true);
-      StreetsidePlugin.getStreetsideWalkAction().addListener(this);
-    }
-
-    @Override
-    public void actionPerformed(ActionEvent e) {
-      if (thread != null) {
-        thread.play();
-      }
-    }
-
-    @Override
-    public void walkStarted(WalkThread thread) {
-      if (thread != null) {
-        this.thread = thread;
-      }
-    }
-  }
-
-  private static class PauseAction extends AbstractAction implements WalkListener {
-
-    /**
-     *
-     */
-    private static final long serialVersionUID = -8758326399460817222L;
-    private WalkThread thread;
-
-    /**
-     * Constructs a normal PauseAction
-     */
-    PauseAction() {
-      putValue(Action.NAME, I18n.tr("Pause"));
-      putValue(Action.SHORT_DESCRIPTION, I18n.tr("Pauses the walk."));
-      new ImageProvider("dialogs/streetsidePause.png").getResource().attachImageIcon(this, true);
-      StreetsidePlugin.getStreetsideWalkAction().addListener(this);
-    }
-
-    @Override
-    public void actionPerformed(ActionEvent e) {
-      thread.pause();
-    }
-
-    @Override
-    public void walkStarted(WalkThread thread) {
-      this.thread = thread;
-    }
-  }
+    public void imagesAdded() {
+        // This method is enforced by StreetsideDataListener, but only selectedImageChanged() is needed
+    }
+
+    /**
+     * Get the image display
+     * @return the streetsideImageDisplay
+     */
+    public StreetsideImageDisplay getStreetsideImageDisplay() {
+        return streetsideImageDisplay;
+    }
+
+    /**
+     * Buttons mode.
+     *
+     * @author nokutu
+     */
+    public enum MODE {
+        /**
+         * Standard mode to view pictures.
+         */
+        NORMAL,
+        /**
+         * Mode when in walk.
+         */
+        WALK
+    }
+
+    /**
+     * Action class form the next image button.
+     *
+     * @author nokutu
+     */
+    private static class NextPictureAction extends AbstractAction {
+
+        @Serial
+        private static final long serialVersionUID = 6333692154558730392L;
+
+        /**
+         * Constructs a normal NextPictureAction
+         */
+        NextPictureAction() {
+            super(I18n.tr("Next picture"));
+            putValue(Action.SHORT_DESCRIPTION, I18n.tr("Shows the next picture in the sequence"));
+            new ImageProvider("help", "next").getResource().attachImageIcon(this, true);
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            StreetsideLayer.getInstance().getData().selectNext();
+        }
+    }
+
+    /**
+     * Action class for the previous image button.
+     *
+     * @author nokutu
+     */
+    private static class PreviousPictureAction extends AbstractAction {
+
+        @Serial
+        private static final long serialVersionUID = 4390593660514657107L;
+
+        /**
+         * Constructs a normal PreviousPictureAction
+         */
+        PreviousPictureAction() {
+            super(I18n.tr("Previous picture"));
+            putValue(Action.SHORT_DESCRIPTION, I18n.tr("Shows the previous picture in the sequence"));
+            new ImageProvider("help", "previous").getResource().attachImageIcon(this, true);
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            StreetsideLayer.getInstance().getData().selectPrevious();
+        }
+    }
+
+    /**
+     * Action class to jump to the image following the red line.
+     *
+     * @author nokutu
+     */
+    private static class RedAction extends AbstractAction {
+
+        @Serial
+        private static final long serialVersionUID = -1244456062285831231L;
+
+        /**
+         * Constructs a normal RedAction
+         */
+        RedAction() {
+            putValue(Action.NAME, I18n.tr("Jump to red"));
+            putValue(Action.SHORT_DESCRIPTION, I18n.tr("Jumps to the picture at the other side of the red line"));
+            new ImageProvider("dialogs", "red").getResource().attachImageIcon(this, true);
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            if (StreetsideMainDialog.getInstance().getImage() != null) {
+                StreetsideLayer.getInstance().getData()
+                        .setSelectedImage(StreetsideLayer.getInstance().getNNearestImage(1), true);
+            }
+        }
+    }
+
+    /**
+     * Action class to jump to the image following the blue line.
+     *
+     * @author nokutu
+     */
+    private static class BlueAction extends AbstractAction {
+
+        @Serial
+        private static final long serialVersionUID = 5951233534212838780L;
+
+        /**
+         * Constructs a normal BlueAction
+         */
+        BlueAction() {
+            putValue(Action.NAME, I18n.tr("Jump to blue"));
+            putValue(Action.SHORT_DESCRIPTION, I18n.tr("Jumps to the picture at the other side of the blue line"));
+            new ImageProvider("dialogs", "blue").getResource().attachImageIcon(this, true);
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            if (StreetsideMainDialog.getInstance().getImage() != null) {
+                StreetsideLayer.getInstance().getData()
+                        .setSelectedImage(StreetsideLayer.getInstance().getNNearestImage(2), true);
+            }
+        }
+    }
+
+    private static class StopAction extends AbstractAction implements WalkListener {
+
+        @Serial
+        private static final long serialVersionUID = 8789972456611625341L;
+
+        private WalkThread thread;
+
+        /**
+         * Constructs a normal StopAction
+         */
+        StopAction() {
+            putValue(Action.NAME, I18n.tr("Stop"));
+            putValue(Action.SHORT_DESCRIPTION, I18n.tr("Stops the walk."));
+            new ImageProvider("dialogs/streetsideStop.png").getResource().attachImageIcon(this, true);
+            StreetsidePlugin.getStreetsideWalkAction().addListener(this);
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            if (thread != null) {
+                thread.stopWalk();
+            }
+        }
+
+        @Override
+        public void walkStarted(WalkThread thread) {
+            this.thread = thread;
+        }
+    }
+
+    private static class PlayAction extends AbstractAction implements WalkListener {
+
+        @Serial
+        private static final long serialVersionUID = -1572747020946842769L;
+
+        private transient WalkThread thread;
+
+        /**
+         * Constructs a normal PlayAction
+         */
+        PlayAction() {
+            putValue(Action.NAME, I18n.tr("Play"));
+            putValue(Action.SHORT_DESCRIPTION, I18n.tr("Continues with the paused walk."));
+            new ImageProvider("dialogs/streetsidePlay.png").getResource().attachImageIcon(this, true);
+            StreetsidePlugin.getStreetsideWalkAction().addListener(this);
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            if (thread != null) {
+                thread.play();
+            }
+        }
+
+        @Override
+        public void walkStarted(WalkThread thread) {
+            if (thread != null) {
+                this.thread = thread;
+            }
+        }
+    }
+
+    private static class PauseAction extends AbstractAction implements WalkListener {
+
+        /**
+         *
+         */
+        @Serial
+        private static final long serialVersionUID = -8758326399460817222L;
+        private WalkThread thread;
+
+        /**
+         * Constructs a normal PauseAction
+         */
+        PauseAction() {
+            putValue(Action.NAME, I18n.tr("Pause"));
+            putValue(Action.SHORT_DESCRIPTION, I18n.tr("Pauses the walk."));
+            new ImageProvider("dialogs/streetsidePause.png").getResource().attachImageIcon(this, true);
+            StreetsidePlugin.getStreetsideWalkAction().addListener(this);
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            thread.pause();
+        }
+
+        @Override
+        public void walkStarted(WalkThread thread) {
+            this.thread = thread;
+        }
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsidePreferenceSetting.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsidePreferenceSetting.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsidePreferenceSetting.java	(revision 36228)
@@ -3,19 +3,15 @@
 
 import java.awt.BorderLayout;
-import java.awt.Color;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
-import java.awt.event.ActionEvent;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Objects;
 import java.util.logging.Logger;
 
 import javax.imageio.ImageIO;
-import javax.swing.AbstractAction;
 import javax.swing.BorderFactory;
 import javax.swing.Box;
-import javax.swing.BoxLayout;
 import javax.swing.ImageIcon;
-import javax.swing.JButton;
 import javax.swing.JCheckBox;
 import javax.swing.JComboBox;
@@ -25,5 +21,4 @@
 import javax.swing.JSpinner;
 import javax.swing.SpinnerNumberModel;
-import javax.swing.SwingUtilities;
 
 import org.openstreetmap.josm.actions.ExpertToggleAction;
@@ -32,9 +27,5 @@
 import org.openstreetmap.josm.gui.preferences.TabPreferenceSetting;
 import org.openstreetmap.josm.plugins.streetside.StreetsidePlugin;
-import org.openstreetmap.josm.plugins.streetside.gui.boilerplate.StreetsideButton;
 import org.openstreetmap.josm.plugins.streetside.io.download.StreetsideDownloader.DOWNLOAD_MODE;
-import org.openstreetmap.josm.plugins.streetside.oauth.OAuthPortListener;
-import org.openstreetmap.josm.plugins.streetside.oauth.StreetsideLoginListener;
-import org.openstreetmap.josm.plugins.streetside.oauth.StreetsideUser;
 import org.openstreetmap.josm.plugins.streetside.utils.StreetsideColorScheme;
 import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
@@ -49,201 +40,116 @@
  *
  */
-public class StreetsidePreferenceSetting implements SubPreferenceSetting, StreetsideLoginListener {
+public class StreetsidePreferenceSetting implements SubPreferenceSetting {
 
-  private static final Logger logger = Logger.getLogger(StreetsidePreferenceSetting.class.getCanonicalName());
+    private static final Logger logger = Logger.getLogger(StreetsidePreferenceSetting.class.getCanonicalName());
 
-  private final JComboBox<String> downloadModeComboBox = new JComboBox<>(
-      new String[] { DOWNLOAD_MODE.VISIBLE_AREA.getLabel(), DOWNLOAD_MODE.OSM_AREA.getLabel(),
-          DOWNLOAD_MODE.MANUAL_ONLY.getLabel() });
+    private final JComboBox<String> downloadModeComboBox = new JComboBox<>(
+            new String[] { DOWNLOAD_MODE.VISIBLE_AREA.getLabel(), DOWNLOAD_MODE.OSM_AREA.getLabel(),
+                    DOWNLOAD_MODE.MANUAL_ONLY.getLabel() });
 
-  private final JCheckBox displayHour = new JCheckBox(I18n.tr("Display hour when the picture was taken"),
-      StreetsideProperties.DISPLAY_HOUR.get());
-  private final JCheckBox format24 = new JCheckBox(I18n.tr("Use 24 hour format"),
-      StreetsideProperties.TIME_FORMAT_24.get());
-  private final JCheckBox moveTo = new JCheckBox(I18n.tr("Move to picture''s location with next/previous buttons"),
-      StreetsideProperties.MOVE_TO_IMG.get());
-  private final JCheckBox hoverEnabled = new JCheckBox(I18n.tr("Preview images when hovering its icon"),
-      StreetsideProperties.HOVER_ENABLED.get());
-  private final JCheckBox cutOffSeq = new JCheckBox(I18n.tr("Cut off sequences at download bounds"),
-      StreetsideProperties.CUT_OFF_SEQUENCES_AT_BOUNDS.get());
-  private final JCheckBox imageLinkToBlurEditor = new JCheckBox(
-      I18n.tr("When opening Streetside image in web browser, show the blur editor instead of the image viewer"),
-      StreetsideProperties.IMAGE_LINK_TO_BLUR_EDITOR.get());
-  private final JCheckBox developer = new JCheckBox(I18n.tr("Enable experimental beta-features (might be unstable)"),
-      StreetsideProperties.DEVELOPER.get());
-  private final SpinnerNumberModel preFetchSize = new SpinnerNumberModel(
-      StreetsideProperties.PRE_FETCH_IMAGE_COUNT.get().intValue(), 0, Integer.MAX_VALUE, 1);
-  private final JButton loginButton = new StreetsideButton(new LoginAction(this));
-  private final JButton logoutButton = new StreetsideButton(new LogoutAction());
-  private final JLabel loginLabel = new JLabel();
-  private final JPanel loginPanel = new JPanel();
+    private final JCheckBox displayHour = new JCheckBox(I18n.tr("Display hour when the picture was taken"),
+            StreetsideProperties.DISPLAY_HOUR.get());
+    private final JCheckBox format24 = new JCheckBox(I18n.tr("Use 24 hour format"),
+            StreetsideProperties.TIME_FORMAT_24.get());
+    private final JCheckBox moveTo = new JCheckBox(I18n.tr("Move to picture''s location with next/previous buttons"),
+            StreetsideProperties.MOVE_TO_IMG.get());
+    private final JCheckBox hoverEnabled = new JCheckBox(I18n.tr("Preview images when hovering its icon"),
+            StreetsideProperties.HOVER_ENABLED.get());
+    private final JCheckBox cutOffSeq = new JCheckBox(I18n.tr("Cut off sequences at download bounds"),
+            StreetsideProperties.CUT_OFF_SEQUENCES_AT_BOUNDS.get());
+    private final JCheckBox imageLinkToBlurEditor = new JCheckBox(
+            I18n.tr("When opening Streetside image in web browser, show the blur editor instead of the image viewer"),
+            StreetsideProperties.IMAGE_LINK_TO_BLUR_EDITOR.get());
+    private final JCheckBox developer = new JCheckBox(I18n.tr("Enable experimental beta-features (might be unstable)"),
+            StreetsideProperties.DEVELOPER.get());
+    private final SpinnerNumberModel preFetchSize = new SpinnerNumberModel(
+            StreetsideProperties.PRE_FETCH_IMAGE_COUNT.get().intValue(), 0, Integer.MAX_VALUE, 1);
 
-  @Override
-  public TabPreferenceSetting getTabPreferenceSetting(PreferenceTabbedPane gui) {
-    return gui.getDisplayPreference();
-  }
-
-  @Override
-  public void addGui(PreferenceTabbedPane gui) {
-    JPanel container = new JPanel(new BorderLayout());
-
-    loginPanel.setLayout(new BoxLayout(loginPanel, BoxLayout.LINE_AXIS));
-    loginPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
-    loginPanel.setBackground(StreetsideColorScheme.TOOLBAR_DARK_GREY);
-    JLabel brandImage = new JLabel();
-    try (InputStream is = StreetsidePreferenceSetting.class
-        .getResourceAsStream("/images/streetside-logo-white.png")) {
-      if (is != null) {
-        brandImage.setIcon(new ImageIcon(ImageIO.read(is)));
-      } else {
-        logger.log(Logging.LEVEL_WARN, "Could not load Streetside brand image!");
-      }
-    } catch (IOException e) {
-      logger.log(Logging.LEVEL_WARN, "While reading Streetside brand image, an IO-exception occured!", e);
-    }
-    loginPanel.add(brandImage, 0);
-    loginPanel.add(Box.createHorizontalGlue(), 1);
-    loginLabel.setForeground(Color.WHITE);
-    loginLabel.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10));
-    loginPanel.add(loginLabel, 2);
-    loginPanel.add(loginButton, 3);
-    onLogout();
-    container.add(loginPanel, BorderLayout.NORTH);
-
-    JPanel mainPanel = new JPanel();
-    mainPanel.setLayout(new GridBagLayout());
-    mainPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
-
-    downloadModeComboBox
-        .setSelectedItem(DOWNLOAD_MODE.fromPrefId(StreetsideProperties.DOWNLOAD_MODE.get()).getLabel());
-
-    JPanel downloadModePanel = new JPanel();
-    downloadModePanel.add(new JLabel(I18n.tr("Download mode")));
-    downloadModePanel.add(downloadModeComboBox);
-    mainPanel.add(downloadModePanel, GBC.eol());
-
-    mainPanel.add(displayHour, GBC.eol());
-    mainPanel.add(format24, GBC.eol());
-    mainPanel.add(moveTo, GBC.eol());
-    mainPanel.add(hoverEnabled, GBC.eol());
-    mainPanel.add(cutOffSeq, GBC.eol());
-    mainPanel.add(imageLinkToBlurEditor, GBC.eol());
-
-    final JPanel preFetchPanel = new JPanel();
-    preFetchPanel.add(new JLabel(I18n.tr("Number of images to be pre-fetched (forwards and backwards)")));
-    final JSpinner spinner = new JSpinner(preFetchSize);
-    final JSpinner.DefaultEditor editor = new JSpinner.NumberEditor(spinner);
-    editor.getTextField().setColumns(3);
-    spinner.setEditor(editor);
-    preFetchPanel.add(spinner);
-    mainPanel.add(preFetchPanel, GBC.eol());
-
-    if (ExpertToggleAction.isExpert() || developer.isSelected()) {
-      mainPanel.add(developer, GBC.eol());
-    }
-    StreetsideColorScheme.styleAsDefaultPanel(mainPanel, downloadModePanel, displayHour, format24, moveTo,
-        hoverEnabled, cutOffSeq, imageLinkToBlurEditor, developer, preFetchPanel);
-    mainPanel.add(Box.createVerticalGlue(), GBC.eol().fill(GridBagConstraints.BOTH));
-
-    container.add(mainPanel, BorderLayout.CENTER);
-
-    synchronized (gui.getDisplayPreference().getTabPane()) {
-      gui.getDisplayPreference().addSubTab(this, "Streetside", new JScrollPane(container));
-      gui.getDisplayPreference().getTabPane().setIconAt(gui.getDisplayPreference().getTabPane().getTabCount() - 1,
-          StreetsidePlugin.LOGO.setSize(12, 12).get());
-    }
-
-    new Thread(() -> {
-      String username = StreetsideUser.getUsername();
-      if (username != null) {
-        SwingUtilities.invokeLater(() -> onLogin(StreetsideUser.getUsername()));
-      }
-    }).start();
-  }
-
-  @Override
-  public void onLogin(final String username) {
-    loginPanel.remove(loginButton);
-    loginPanel.add(logoutButton, 3);
-    loginLabel.setText(I18n.tr("You are logged in as ''{0}''.", username));
-    loginPanel.revalidate();
-    loginPanel.repaint();
-  }
-
-  @Override
-  public void onLogout() {
-    loginPanel.remove(logoutButton);
-    loginPanel.add(loginButton, 3);
-    loginLabel.setText(I18n.tr("You are currently not logged in."));
-    loginPanel.revalidate();
-    loginPanel.repaint();
-  }
-
-  @SuppressWarnings("PMD.ShortMethodName")
-  @Override
-  public boolean ok() {
-    StreetsideProperties.DOWNLOAD_MODE
-        .put(DOWNLOAD_MODE.fromLabel(downloadModeComboBox.getSelectedItem().toString()).getPrefId());
-    StreetsideProperties.DISPLAY_HOUR.put(displayHour.isSelected());
-    StreetsideProperties.TIME_FORMAT_24.put(format24.isSelected());
-    StreetsideProperties.MOVE_TO_IMG.put(moveTo.isSelected());
-    StreetsideProperties.HOVER_ENABLED.put(hoverEnabled.isSelected());
-    StreetsideProperties.CUT_OFF_SEQUENCES_AT_BOUNDS.put(cutOffSeq.isSelected());
-    StreetsideProperties.IMAGE_LINK_TO_BLUR_EDITOR.put(imageLinkToBlurEditor.isSelected());
-    StreetsideProperties.DEVELOPER.put(developer.isSelected());
-    StreetsideProperties.PRE_FETCH_IMAGE_COUNT.put(preFetchSize.getNumber().intValue());
-
-    //Restart is never required
-    return false;
-  }
-
-  @Override
-  public boolean isExpert() {
-    return false;
-  }
-
-  /**
-   * Opens the StreetsideOAuthUI window and lets the user log in.
-   *
-   * @author nokutu
-   */
-  private static class LoginAction extends AbstractAction {
-
-    private static final long serialVersionUID = 8743119160917296506L;
-
-    private final transient StreetsideLoginListener callback;
-
-    LoginAction(StreetsideLoginListener loginCallback) {
-      super(I18n.tr("Login"));
-      callback = loginCallback;
+    @Override
+    public TabPreferenceSetting getTabPreferenceSetting(PreferenceTabbedPane gui) {
+        return gui.getDisplayPreference();
     }
 
     @Override
-    public void actionPerformed(ActionEvent arg0) {
-      OAuthPortListener portListener = new OAuthPortListener(callback);
-      portListener.start();
-      // user authentication not supported for Streetside (Mapillary relic)
+    public void addGui(PreferenceTabbedPane gui) {
+        JPanel container = new JPanel(new BorderLayout());
+
+        JLabel brandImage = new JLabel();
+        try (InputStream is = StreetsidePreferenceSetting.class
+                .getResourceAsStream("/images/streetside-logo-white.png")) {
+            if (is != null) {
+                brandImage.setIcon(new ImageIcon(ImageIO.read(is)));
+            } else {
+                logger.log(Logging.LEVEL_WARN, "Could not load Streetside brand image!");
+            }
+        } catch (IOException e) {
+            logger.log(Logging.LEVEL_WARN, "While reading Streetside brand image, an IO-exception occured!", e);
+        }
+
+        JPanel mainPanel = new JPanel();
+        mainPanel.setLayout(new GridBagLayout());
+        mainPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+
+        downloadModeComboBox
+                .setSelectedItem(DOWNLOAD_MODE.fromPrefId(StreetsideProperties.DOWNLOAD_MODE.get()).getLabel());
+
+        JPanel downloadModePanel = new JPanel();
+        downloadModePanel.add(new JLabel(I18n.tr("Download mode")));
+        downloadModePanel.add(downloadModeComboBox);
+        mainPanel.add(downloadModePanel, GBC.eol());
+
+        mainPanel.add(displayHour, GBC.eol());
+        mainPanel.add(format24, GBC.eol());
+        mainPanel.add(moveTo, GBC.eol());
+        mainPanel.add(hoverEnabled, GBC.eol());
+        mainPanel.add(cutOffSeq, GBC.eol());
+        mainPanel.add(imageLinkToBlurEditor, GBC.eol());
+
+        final JPanel preFetchPanel = new JPanel();
+        preFetchPanel.add(new JLabel(I18n.tr("Number of images to be pre-fetched (forwards and backwards)")));
+        final JSpinner spinner = new JSpinner(preFetchSize);
+        final JSpinner.DefaultEditor editor = new JSpinner.NumberEditor(spinner);
+        editor.getTextField().setColumns(3);
+        spinner.setEditor(editor);
+        preFetchPanel.add(spinner);
+        mainPanel.add(preFetchPanel, GBC.eol());
+
+        if (ExpertToggleAction.isExpert() || developer.isSelected()) {
+            mainPanel.add(developer, GBC.eol());
+        }
+        StreetsideColorScheme.styleAsDefaultPanel(mainPanel, downloadModePanel, displayHour, format24, moveTo,
+                hoverEnabled, cutOffSeq, imageLinkToBlurEditor, developer, preFetchPanel);
+        mainPanel.add(Box.createVerticalGlue(), GBC.eol().fill(GridBagConstraints.BOTH));
+
+        container.add(mainPanel, BorderLayout.CENTER);
+
+        synchronized (gui.getDisplayPreference().getTabPane()) {
+            gui.getDisplayPreference().addSubTab(this, "Streetside", new JScrollPane(container));
+            gui.getDisplayPreference().getTabPane().setIconAt(gui.getDisplayPreference().getTabPane().getTabCount() - 1,
+                    StreetsidePlugin.LOGO.setSize(12, 12).get());
+        }
     }
-  }
 
-  /**
-   * Logs the user out.
-   *
-   * @author nokutu
-   *
-   */
-  private class LogoutAction extends AbstractAction {
+    @SuppressWarnings("PMD.ShortMethodName")
+    @Override
+    public boolean ok() {
+        StreetsideProperties.DOWNLOAD_MODE
+                .put(DOWNLOAD_MODE.fromLabel(Objects.requireNonNull(downloadModeComboBox.getSelectedItem()).toString()).getPrefId());
+        StreetsideProperties.DISPLAY_HOUR.put(displayHour.isSelected());
+        StreetsideProperties.TIME_FORMAT_24.put(format24.isSelected());
+        StreetsideProperties.MOVE_TO_IMG.put(moveTo.isSelected());
+        StreetsideProperties.HOVER_ENABLED.put(hoverEnabled.isSelected());
+        StreetsideProperties.CUT_OFF_SEQUENCES_AT_BOUNDS.put(cutOffSeq.isSelected());
+        StreetsideProperties.IMAGE_LINK_TO_BLUR_EDITOR.put(imageLinkToBlurEditor.isSelected());
+        StreetsideProperties.DEVELOPER.put(developer.isSelected());
+        StreetsideProperties.PRE_FETCH_IMAGE_COUNT.put(preFetchSize.getNumber().intValue());
 
-    private static final long serialVersionUID = -4146587895393766981L;
-
-    private LogoutAction() {
-      super(I18n.tr("Logout"));
+        //Restart is never required
+        return false;
     }
 
     @Override
-    public void actionPerformed(ActionEvent arg0) {
-      StreetsideUser.reset();
-      onLogout();
+    public boolean isExpert() {
+        return false;
     }
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsideViewerDialog.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsideViewerDialog.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsideViewerDialog.java	(revision 36228)
@@ -2,9 +2,6 @@
 package org.openstreetmap.josm.plugins.streetside.gui;
 
-import java.awt.BorderLayout;
-import java.awt.Component;
-import java.util.List;
+import java.io.Serial;
 
-import org.openstreetmap.josm.gui.SideButton;
 import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
 import org.openstreetmap.josm.plugins.streetside.gui.imageinfo.StreetsideViewerPanel;
@@ -18,62 +15,44 @@
 public final class StreetsideViewerDialog extends ToggleDialog {
 
-  private static final long serialVersionUID = -8983900297628236197L;
+    @Serial
+    private static final long serialVersionUID = 6424974077669812562L;
 
-  private static final String BASE_TITLE = "360° Streetside Viewer";
+    private static final String BASE_TITLE = "360° Streetside Viewer";
 
-  private static StreetsideViewerDialog instance;
+    private static StreetsideViewerDialog instance;
 
-  /**
-   * Object containing the shown image and that handles zoom and drag
-   */
-  private final StreetsideViewerPanel streetsideViewerPanel;
+    /**
+     * Object containing the shown image and that handles zoom and drag
+     */
+    private final StreetsideViewerPanel streetsideViewerPanel;
 
-  private StreetsideViewerDialog() {
-    super(StreetsideViewerDialog.BASE_TITLE, "streetside-viewer", "Open Streetside Viewer window", null, 200, true,
-        StreetsidePreferenceSetting.class);
-    streetsideViewerPanel = new StreetsideViewerPanel();
-    createLayout(streetsideViewerPanel, true, null);
-  }
+    private StreetsideViewerDialog() {
+        super(StreetsideViewerDialog.BASE_TITLE, "streetside-viewer", "Open Streetside Viewer window", null, 200, true,
+                StreetsidePreferenceSetting.class);
+        streetsideViewerPanel = new StreetsideViewerPanel();
+        createLayout(streetsideViewerPanel, true, null);
+    }
 
-  /**
-   * Returns the unique instance of the class.
-   *
-   * @return The unique instance of the class.
-   */
-  public static synchronized StreetsideViewerDialog getInstance() {
-    if (StreetsideViewerDialog.instance == null) {
-      StreetsideViewerDialog.instance = new StreetsideViewerDialog();
+    /**
+     * Returns the unique instance of the class.
+     *
+     * @return The unique instance of the class.
+     */
+    public static synchronized StreetsideViewerDialog getInstance() {
+        if (StreetsideViewerDialog.instance == null) {
+            StreetsideViewerDialog.instance = new StreetsideViewerDialog();
+        }
+        return StreetsideViewerDialog.instance;
     }
-    return StreetsideViewerDialog.instance;
-  }
 
-  /**
-   * @return true, iff the singleton instance is present
-   */
-  public static boolean hasInstance() {
-    return StreetsideViewerDialog.instance != null;
-  }
+    /**
+     * Destroys the unique instance of the class.
+     */
+    public static synchronized void destroyInstance() {
+        StreetsideViewerDialog.instance = null;
+    }
 
-  /**
-   * Destroys the unique instance of the class.
-   */
-  public static synchronized void destroyInstance() {
-    StreetsideViewerDialog.instance = null;
-  }
-
-  /**
-   * Creates the layout of the dialog.
-   *
-   * @param data  The content of the dialog
-   * @param buttons The buttons where you can click
-   */
-  public void createLayout(Component data, List<SideButton> buttons) {
-    removeAll();
-    createLayout(data, true, buttons);
-    add(titleBar, BorderLayout.NORTH);
-  }
-
-  public StreetsideViewerPanel getStreetsideViewerPanel() {
-    return streetsideViewerPanel;
-  }
+    public StreetsideViewerPanel getStreetsideViewerPanel() {
+        return streetsideViewerPanel;
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsideWalkDialog.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsideWalkDialog.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/StreetsideWalkDialog.java	(revision 36228)
@@ -3,4 +3,6 @@
 
 import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.Serial;
 
 import javax.swing.JCheckBox;
@@ -19,44 +21,45 @@
 public class StreetsideWalkDialog extends JPanel {
 
-  private static final long serialVersionUID = 7974881240732957573L;
+    @Serial
+    private static final long serialVersionUID = 7974881240732957573L;
 
-  /**
-   * Spin containing the interval value.
-   */
-  public SpinnerModel spin;
-  /**
-   * Whether it must wait for the picture to be downloaded
-   */
-  public JCheckBox waitForPicture;
-  /**
-   * Whether the view must follow the selected image.
-   */
-  public JCheckBox followSelection;
-  /**
-   * Go forward or backwards
-   */
-  public JCheckBox goForward;
+    /**
+     * Spin containing the interval value.
+     */
+    public final SpinnerModel spin;
+    /**
+     * Whether it must wait for the picture to be downloaded
+     */
+    public final JCheckBox waitForPicture;
+    /**
+     * Whether the view must follow the selected image.
+     */
+    public final JCheckBox followSelection;
+    /**
+     * Go forward or backwards
+     */
+    public final JCheckBox goForward;
 
-  /**
-   * Main constructor
-   */
-  public StreetsideWalkDialog() {
-    JPanel interval = new JPanel();
-    spin = new SpinnerNumberModel(2000, 500, 10000, 500);
-    interval.add(new JLabel("Interval (miliseconds): "));
-    interval.add(new JSpinner(spin));
-    add(interval);
+    /**
+     * Main constructor
+     */
+    public StreetsideWalkDialog() {
+        JPanel interval = new JPanel();
+        spin = new SpinnerNumberModel(2000, 500, 10000, 500);
+        interval.add(new JLabel("Interval (miliseconds): "));
+        interval.add(new JSpinner(spin));
+        add(interval);
 
-    waitForPicture = new JCheckBox(tr("Wait for full quality pictures"));
-    waitForPicture.setSelected(true);
-    add(waitForPicture);
+        waitForPicture = new JCheckBox(tr("Wait for full quality pictures"));
+        waitForPicture.setSelected(true);
+        add(waitForPicture);
 
-    followSelection = new JCheckBox(tr("Follow selected image"));
-    followSelection.setSelected(true);
-    add(followSelection);
+        followSelection = new JCheckBox(tr("Follow selected image"));
+        followSelection.setSelected(true);
+        add(followSelection);
 
-    goForward = new JCheckBox(tr("Go forward"));
-    goForward.setSelected(true);
-    add(goForward);
-  }
+        goForward = new JCheckBox(tr("Go forward"));
+        goForward.setSelected(true);
+        add(goForward);
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/boilerplate/SelectableLabel.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/boilerplate/SelectableLabel.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/boilerplate/SelectableLabel.java	(revision 36228)
@@ -4,4 +4,5 @@
 import java.awt.Color;
 import java.awt.Font;
+import java.io.Serial;
 
 import javax.swing.JTextPane;
@@ -10,24 +11,25 @@
 public class SelectableLabel extends JTextPane {
 
-  public static final Font DEFAULT_FONT = UIManager.getFont("Label.font").deriveFont(Font.PLAIN);
-  public static final Color DEFAULT_BACKGROUND = UIManager.getColor("Panel.background");
-  private static final long serialVersionUID = 5432480892000739831L;
+    public static final Font DEFAULT_FONT = UIManager.getFont("Label.font").deriveFont(Font.PLAIN);
+    public static final Color DEFAULT_BACKGROUND = UIManager.getColor("Panel.background");
+    @Serial
+    private static final long serialVersionUID = 5432480892000739831L;
 
-  public SelectableLabel() {
-    super();
-    init();
-  }
+    public SelectableLabel() {
+        super();
+        init();
+    }
 
-  public SelectableLabel(String text) {
-    this();
-    setText(text);
-  }
+    public SelectableLabel(String text) {
+        this();
+        setText(text);
+    }
 
-  private void init() {
-    setEditable(false);
-    setFont(DEFAULT_FONT);
-    setContentType("text/html");
-    setBackground(DEFAULT_BACKGROUND);
-    setBorder(null);
-  }
+    private void init() {
+        setEditable(false);
+        setFont(DEFAULT_FONT);
+        setContentType("text/html");
+        setBackground(DEFAULT_BACKGROUND);
+        setBorder(null);
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/boilerplate/StreetsideButton.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/boilerplate/StreetsideButton.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/boilerplate/StreetsideButton.java	(revision 36228)
@@ -6,4 +6,5 @@
 import java.awt.Graphics2D;
 import java.awt.RenderingHints;
+import java.io.Serial;
 
 import javax.swing.Action;
@@ -15,35 +16,36 @@
 public class StreetsideButton extends JButton {
 
-  private static final long serialVersionUID = -3060978712233067368L;
+    @Serial
+    private static final long serialVersionUID = -3060978712233067368L;
 
-  public StreetsideButton(final Action action) {
-    this(action, false);
-  }
+    public StreetsideButton(final Action action) {
+        this(action, false);
+    }
 
-  public StreetsideButton(final Action action, boolean slim) {
-    super(action);
-    setForeground(Color.WHITE);
-    setBorder(slim ? BorderFactory.createEmptyBorder(3, 4, 3, 4) : BorderFactory.createEmptyBorder(7, 10, 7, 10));
-  }
+    public StreetsideButton(final Action action, boolean slim) {
+        super(action);
+        setForeground(Color.WHITE);
+        setBorder(slim ? BorderFactory.createEmptyBorder(3, 4, 3, 4) : BorderFactory.createEmptyBorder(7, 10, 7, 10));
+    }
 
-  @Override
-  protected void paintComponent(final Graphics g) {
-    if (!isEnabled()) {
-      g.setColor(StreetsideColorScheme.TOOLBAR_DARK_GREY);
-    } else if (getModel().isPressed()) {
-      g.setColor(StreetsideColorScheme.STREETSIDE_BLUE.darker().darker());
-    } else if (getModel().isRollover()) {
-      g.setColor(StreetsideColorScheme.STREETSIDE_BLUE.darker());
-    } else {
-      g.setColor(StreetsideColorScheme.STREETSIDE_BLUE);
+    @Override
+    protected void paintComponent(final Graphics g) {
+        if (!isEnabled()) {
+            g.setColor(StreetsideColorScheme.TOOLBAR_DARK_GREY);
+        } else if (getModel().isPressed()) {
+            g.setColor(StreetsideColorScheme.STREETSIDE_BLUE.darker().darker());
+        } else if (getModel().isRollover()) {
+            g.setColor(StreetsideColorScheme.STREETSIDE_BLUE.darker());
+        } else {
+            g.setColor(StreetsideColorScheme.STREETSIDE_BLUE);
+        }
+        ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+        g.fillRoundRect(0, 0, getWidth(), getHeight(), 3, 3);
+        super.paintComponent(g);
     }
-    ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
-    g.fillRoundRect(0, 0, getWidth(), getHeight(), 3, 3);
-    super.paintComponent(g);
-  }
 
-  @Override
-  public boolean isContentAreaFilled() {
-    return false;
-  }
+    @Override
+    public boolean isContentAreaFilled() {
+        return false;
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/AddTagToPrimitiveAction.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/AddTagToPrimitiveAction.java	(revision 36194)
+++ 	(revision )
@@ -1,60 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.streetside.gui.imageinfo;
-
-import java.awt.event.ActionEvent;
-
-import javax.swing.AbstractAction;
-import javax.swing.JOptionPane;
-
-import org.openstreetmap.josm.data.osm.AbstractPrimitive;
-import org.openstreetmap.josm.data.osm.Tag;
-import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.tools.I18n;
-import org.openstreetmap.josm.tools.ImageProvider;
-import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
-
-public class AddTagToPrimitiveAction extends AbstractAction {
-
-  private static final long serialVersionUID = -2134831346322019333L;
-
-  private Tag tag;
-  private AbstractPrimitive target;
-
-  public AddTagToPrimitiveAction(final String name) {
-    super(name, ImageProvider.get("dialogs/add", ImageSizes.SMALLICON));
-  }
-
-  public void setTag(Tag tag) {
-    this.tag = tag;
-    updateEnabled();
-  }
-
-  public void setTarget(AbstractPrimitive target) {
-    this.target = target;
-    updateEnabled();
-  }
-
-  private void updateEnabled() {
-    setEnabled(tag != null && target != null);
-  }
-
-  @Override
-  public void actionPerformed(ActionEvent e) {
-    if (target != null && tag != null) {
-      int conflictResolution = JOptionPane.YES_OPTION;
-      if (target.hasKey(tag.getKey()) && !target.hasTag(tag.getKey(), tag.getValue())) {
-        conflictResolution = JOptionPane.showConfirmDialog(MainApplication.getMainFrame(), "<html>" + I18n
-            .tr("A tag with key <i>{0}</i> is already present on the selected OSM object.", tag.getKey())
-            + "<br>"
-            + I18n.tr(
-                "Do you really want to replace the current value <i>{0}</i> with the new value <i>{1}</i>?",
-                target.get(tag.getKey()), tag.getValue())
-            + "</html>", I18n.tr("Tag conflict"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
-      }
-      if (JOptionPane.YES_OPTION == conflictResolution) {
-        target.put(tag);
-        target.setModified(true);
-      }
-    }
-  }
-}
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/ClipboardAction.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/ClipboardAction.java	(revision 36194)
+++ 	(revision )
@@ -1,103 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.streetside.gui.imageinfo;
-
-import java.awt.Color;
-import java.awt.Component;
-import java.awt.Toolkit;
-import java.awt.datatransfer.Transferable;
-import java.awt.event.ActionEvent;
-
-import javax.swing.AbstractAction;
-import javax.swing.Action;
-import javax.swing.BorderFactory;
-import javax.swing.JLabel;
-import javax.swing.JPopupMenu;
-
-import org.openstreetmap.josm.tools.I18n;
-import org.openstreetmap.josm.tools.ImageProvider;
-import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
-
-public class ClipboardAction extends AbstractAction {
-
-  private static final long serialVersionUID = -7298944557860158010L;
-  /**
-   * The duration in milliseconds for which the popup will be shown
-   */
-  private static final long POPUP_DURATION = 3000;
-  /**
-   * A small popup that shows up when the key has been moved to the clipboard
-   */
-  private final JPopupMenu popup;
-  /**
-   * The component which is used as parent of the shown popup.
-   * If this is <code>null</code>, no popup will be shown.
-   */
-  private Component popupParent;
-  /**
-   * The UNIX epoch time when the popup for this action was shown the last time
-   */
-  private long lastPopupShowTime;
-  /**
-   * The contents that are transfered into the clipboard when the action is executed.
-   * If this is <code>null</code>, the clipboard won't be changed.
-   */
-  private Transferable contents;
-
-  public ClipboardAction(final String name, final Transferable contents) {
-    super(name, ImageProvider.get("copy", ImageSizes.SMALLICON));
-    this.contents = contents;
-
-    // Init popup
-    popup = new JPopupMenu();
-    JLabel label = new JLabel(I18n.tr("Key copied to clipboard") + '…');
-    label.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
-    popup.add(label);
-    popup.setBackground(new Color(0f, 0f, 0f, .5f));
-    label.setForeground(Color.WHITE);
-  }
-
-  /**
-   * @param contents the contents, which should be copied to the clipboard when the {@link Action} is executed
-   */
-  public void setContents(Transferable contents) {
-    this.contents = contents;
-    setEnabled(contents != null);
-  }
-
-  /**
-   * Sets the component, under which the popup will be shown, which indicates that the key was copied to the clipboard.
-   *
-   * @param popupParent the component to set as parent of the popup
-   */
-  public void setPopupParent(Component popupParent) {
-    this.popupParent = popupParent;
-  }
-
-  /* (non-Javadoc)
-   * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
-   */
-  @Override
-  public void actionPerformed(ActionEvent e) {
-    if (contents != null) {
-      Toolkit.getDefaultToolkit().getSystemClipboard().setContents(contents, null);
-      if (popupParent != null) {
-        popup.show(popupParent, 0, popupParent.getHeight() + 2);
-        new Thread(() -> {
-          long threadStart = System.currentTimeMillis();
-          lastPopupShowTime = threadStart;
-          try {
-            Thread.sleep(POPUP_DURATION);
-          } catch (InterruptedException e1) {
-            if (threadStart == lastPopupShowTime) {
-              popup.setVisible(false);
-            }
-          }
-          if (System.currentTimeMillis() >= lastPopupShowTime + POPUP_DURATION) {
-            popup.setVisible(false);
-          }
-        }).start();
-      }
-    }
-  }
-
-}
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/ImageInfoHelpPopup.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/ImageInfoHelpPopup.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/ImageInfoHelpPopup.java	(revision 36228)
@@ -7,4 +7,5 @@
 import java.awt.IllegalComponentStateException;
 import java.awt.event.ActionEvent;
+import java.io.Serial;
 import java.util.logging.Logger;
 
@@ -25,70 +26,73 @@
 public class ImageInfoHelpPopup extends JPopupMenu {
 
-  private static final long serialVersionUID = -1721594904273820586L;
+    @Serial
+    private static final long serialVersionUID = -1721594904273820586L;
 
-  private static final Logger LOGGER = Logger.getLogger(ImageInfoHelpPopup.class.getCanonicalName());
+    private static final Logger LOGGER = Logger.getLogger(ImageInfoHelpPopup.class.getCanonicalName());
 
-  private final Component invokerComp;
-  private boolean alreadyDisplayed;
+    private final Component invokerComp;
+    private boolean alreadyDisplayed;
 
-  public ImageInfoHelpPopup(Component invoker) {
-    invokerComp = invoker;
-    removeAll();
-    setLayout(new BorderLayout());
+    public ImageInfoHelpPopup(Component invoker) {
+        invokerComp = invoker;
+        removeAll();
+        setLayout(new BorderLayout());
 
-    JPanel topBar = new JPanel();
-    topBar.add(new JLabel(ImageProvider.get("streetside-logo-white")));
-    topBar.setBackground(StreetsideColorScheme.TOOLBAR_DARK_GREY);
-    add(topBar, BorderLayout.NORTH);
+        JPanel topBar = new JPanel();
+        topBar.add(new JLabel(ImageProvider.get("streetside-logo-white")));
+        topBar.setBackground(StreetsideColorScheme.TOOLBAR_DARK_GREY);
+        add(topBar, BorderLayout.NORTH);
 
-    JTextPane mainText = new JTextPane();
-    mainText.setContentType("text/html");
-    mainText.setFont(SelectableLabel.DEFAULT_FONT);
-    mainText.setText("<html><div style='width:250px'>"
-        + "Welcome to the Microsoft Streetside JOSM Plugin. To view the vector bubbles for the 360 degree imagery, select Imagery->Streetside from the JOSM menu."
-        + "<br><br>"
-        + "Once the blue bubbles appear on the map, click on a vector bubble and undock/maximize the 360 viewer to view the imagery."
-        + "</div></html>");
-    add(mainText, BorderLayout.CENTER);
+        JTextPane mainText = new JTextPane();
+        mainText.setContentType("text/html");
+        mainText.setFont(SelectableLabel.DEFAULT_FONT);
+        mainText.setText("<html><div style='width:250px'>"
+                + "Welcome to the Microsoft Streetside JOSM Plugin. To view the vector bubbles for the 360 degree imagery, select Imagery->Streetside from the JOSM menu."
+                + "<br><br>"
+                + "Once the blue bubbles appear on the map, click on a vector bubble and undock/maximize the 360 viewer to view the imagery."
+                + "</div></html>");
+        add(mainText, BorderLayout.CENTER);
 
-    JPanel bottomBar = new JPanel();
-    bottomBar.setBackground(new Color(0x00FFFFFF, true));
-    StreetsideButton infoButton = new StreetsideButton(ImageInfoPanel.getInstance().getToggleAction());
-    infoButton.addActionListener(e -> setVisible(false));
-    bottomBar.add(infoButton);
-    StreetsideButton closeBtn = new StreetsideButton(new AbstractAction() {
+        JPanel bottomBar = new JPanel();
+        bottomBar.setBackground(new Color(0x00FFFFFF, true));
+        StreetsideButton infoButton = new StreetsideButton(ImageInfoPanel.getInstance().getToggleAction());
+        infoButton.addActionListener(e -> setVisible(false));
+        bottomBar.add(infoButton);
+        StreetsideButton closeBtn = new StreetsideButton(new AbstractAction() {
 
-      private static final long serialVersionUID = 2853315308169651854L;
+            @Serial
+            private static final long serialVersionUID = 2853315308169651854L;
 
-      @Override
-      public void actionPerformed(ActionEvent e) {
-        setVisible(false);
-        StreetsideProperties.IMAGEINFO_HELP_COUNTDOWN.put(0);
-      }
-    });
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                setVisible(false);
+                StreetsideProperties.IMAGEINFO_HELP_COUNTDOWN.put(0);
+            }
+        });
 
-    closeBtn.setText(I18n.tr("I got it, close this."));
-    bottomBar.add(closeBtn);
-    add(bottomBar, BorderLayout.SOUTH);
+        closeBtn.setText(I18n.tr("I got it, close this."));
+        bottomBar.add(closeBtn);
+        add(bottomBar, BorderLayout.SOUTH);
 
-    setBackground(Color.WHITE);
-  }
+        setBackground(Color.WHITE);
+    }
 
-  /**
-   * @return <code>true</code> if the popup is displayed
-   */
-  public boolean showPopup() {
-    if (!alreadyDisplayed && invokerComp.isShowing()) {
-      try {
-        show(invokerComp, invokerComp.getWidth(), 0);
-        alreadyDisplayed = true;
-        return true;
-      } catch (IllegalComponentStateException e) {
-        LOGGER.log(Logging.LEVEL_WARN, I18n.tr(
-            "Could not show ImageInfoHelpPopup, because probably the invoker component has disappeared from screen.",
-            e));
-      }
+    /**
+     * Show the popup
+     * @return <code>true</code> if the popup is displayed
+     */
+    public boolean showPopup() {
+        if (!alreadyDisplayed && invokerComp.isShowing()) {
+            try {
+                show(invokerComp, invokerComp.getWidth(), 0);
+                alreadyDisplayed = true;
+                return true;
+            } catch (IllegalComponentStateException e) {
+                LOGGER.log(Logging.LEVEL_WARN, I18n.tr(
+                        "Could not show ImageInfoHelpPopup, because probably the invoker component has disappeared from screen.",
+                        e));
+            }
+        }
+        return false;
     }
-    return false;
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/ImageInfoPanel.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/ImageInfoPanel.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/ImageInfoPanel.java	(revision 36228)
@@ -5,5 +5,5 @@
 import java.awt.GridBagLayout;
 import java.awt.Insets;
-import java.awt.datatransfer.StringSelection;
+import java.io.Serial;
 import java.util.Collection;
 import java.util.logging.Logger;
@@ -11,16 +11,12 @@
 import javax.swing.JLabel;
 import javax.swing.JPanel;
-import javax.swing.JTextPane;
 
 import org.openstreetmap.josm.data.osm.DataSelectionListener;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.Tag;
 import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
 import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeListener;
 import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
-import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
 import org.openstreetmap.josm.plugins.streetside.StreetsideDataListener;
 import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
-import org.openstreetmap.josm.plugins.streetside.gui.boilerplate.SelectableLabel;
 import org.openstreetmap.josm.plugins.streetside.gui.boilerplate.StreetsideButton;
 import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
@@ -29,173 +25,106 @@
 import org.openstreetmap.josm.tools.Logging;
 
+/**
+ * A panel for showing image information
+ */
 public final class ImageInfoPanel extends ToggleDialog implements StreetsideDataListener, DataSelectionListener {
-  private static final long serialVersionUID = 4141847503072417190L;
+    @Serial
+    private static final long serialVersionUID = 1898445061036887054L;
 
-  private static final Logger LOGGER = Logger.getLogger(ImageInfoPanel.class.getCanonicalName());
+    private static final Logger LOGGER = Logger.getLogger(ImageInfoPanel.class.getCanonicalName());
 
-  private static ImageInfoPanel instance;
+    private static ImageInfoPanel instance;
 
-  private final JTextPane imgKeyValue;
-  private final WebLinkAction imgLinkAction;
-  private final ClipboardAction copyImgKeyAction;
-  private final AddTagToPrimitiveAction addStreetsideTagAction;
-  private final JTextPane seqKeyValue;
+    private final WebLinkAction imgLinkAction;
 
-  private ValueChangeListener<Boolean> imageLinkChangeListener;
+    private ValueChangeListener<Boolean> imageLinkChangeListener;
 
-  private ImageInfoPanel() {
-    super(I18n.tr("Streetside 360° image info"), "streetside-info",
-        I18n.tr("Displays detail information on the currently selected Streetside image"), null, 150);
-    SelectionEventManager.getInstance().addSelectionListener(this);
+    private ImageInfoPanel() {
+        super(I18n.tr("Streetside 360° image info"), "streetside-info",
+                I18n.tr("Displays detail information on the currently selected Streetside image"), null, 150);
+        SelectionEventManager.getInstance().addSelectionListener(this);
 
-    imgKeyValue = new SelectableLabel();
+        imgLinkAction = new WebLinkAction(I18n.tr("View in browser"), null);
 
-    imgLinkAction = new WebLinkAction(I18n.tr("View in browser"), null);
+        final var root = new JPanel(new GridBagLayout());
 
-    copyImgKeyAction = new ClipboardAction(I18n.tr("Copy key"), null);
-    StreetsideButton copyButton = new StreetsideButton(copyImgKeyAction, true);
-    copyImgKeyAction.setPopupParent(copyButton);
+        final var gbc = new GridBagConstraints();
+        gbc.insets = new Insets(0, 5, 0, 5);
 
-    addStreetsideTagAction = new AddTagToPrimitiveAction(I18n.tr("Add Streetside tag"));
+        gbc.fill = GridBagConstraints.HORIZONTAL;
+        gbc.gridx = 0;
+        gbc.gridy = 0;
+        root.add(new JLabel(I18n.tr("Image actions")), gbc);
 
-    seqKeyValue = new SelectableLabel();
+        gbc.fill = GridBagConstraints.HORIZONTAL;
+        gbc.gridx = 1;
+        gbc.gridy = 0;
+        root.add(new StreetsideButton(imgLinkAction, true), gbc);
 
-    JPanel root = new JPanel(new GridBagLayout());
+        gbc.fill = GridBagConstraints.HORIZONTAL;
+        gbc.weightx = 0.5;
+        gbc.gridx = 2;
+        gbc.gridy = 1;
 
-    GridBagConstraints gbc = new GridBagConstraints();
-    gbc.insets = new Insets(0, 5, 0, 5);
-
-    gbc.fill = GridBagConstraints.HORIZONTAL;
-    gbc.gridx = 0;
-    gbc.gridy = 0;
-    root.add(new JLabel(I18n.tr("Image actions")), gbc);
-
-    gbc.fill = GridBagConstraints.HORIZONTAL;
-    gbc.weightx = 0.5;
-    gbc.gridx = 0;
-    gbc.gridy = 1;
-    root.add(new JLabel(I18n.tr("Image key")), gbc);
-
-    gbc.fill = GridBagConstraints.HORIZONTAL;
-    gbc.weightx = 0.5;
-    gbc.gridx = 0;
-    gbc.gridy = 2;
-    root.add(new JLabel(I18n.tr("Sequence key")), gbc);
-
-    gbc.fill = GridBagConstraints.HORIZONTAL;
-    gbc.gridx = 1;
-    gbc.gridy = 0;
-    root.add(new StreetsideButton(imgLinkAction, true), gbc);
-
-    gbc.fill = GridBagConstraints.HORIZONTAL;
-    gbc.weightx = 0.5;
-    gbc.gridx = 1;
-    gbc.gridy = 1;
-    root.add(imgKeyValue, gbc);
-
-    gbc.fill = GridBagConstraints.HORIZONTAL;
-    gbc.weightx = 0.5;
-    gbc.gridx = 1;
-    gbc.gridy = 2;
-    root.add(seqKeyValue, gbc);
-
-    gbc.fill = GridBagConstraints.HORIZONTAL;
-    gbc.weightx = 0.5;
-    gbc.gridx = 2;
-    gbc.gridy = 1;
-    root.add(copyButton, gbc);
-
-    createLayout(root, true, null);
-    selectedImageChanged(null, null);
-  }
-
-  public static ImageInfoPanel getInstance() {
-    synchronized (ImageInfoPanel.class) {
-      if (instance == null) {
-        instance = new ImageInfoPanel();
-      }
-      return instance;
-    }
-  }
-
-  /**
-   * Destroys the unique instance of the class.
-   */
-  public static synchronized void destroyInstance() {
-    instance = null;
-  }
-
-  /* (non-Javadoc)
-   * @see org.openstreetmap.josm.gui.dialogs.ToggleDialog#stateChanged()
-   */
-  @Override
-  protected void stateChanged() {
-    super.stateChanged();
-    if (isDialogShowing()) { // If the user opens the dialog once, no longer show the help message
-      StreetsideProperties.IMAGEINFO_HELP_COUNTDOWN.put(0);
-    }
-  }
-
-  /* (non-Javadoc)
-   * @see org.openstreetmap.josm.plugins.streetside.StreetsideDataListener#imagesAdded()
-   */
-  @Override
-  public void imagesAdded() {
-    // Method is not needed, but enforcesd by the interface StreetsideDataListener
-  }
-
-  /* (non-Javadoc)
-   * @see org.openstreetmap.josm.plugins.streetside.StreetsideDataListener#selectedImageChanged(org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage, org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage)
-   */
-  @Override
-  public synchronized void selectedImageChanged(final StreetsideAbstractImage oldImage,
-      final StreetsideAbstractImage newImage) {
-    LOGGER.info(String.format("Selected Streetside image changed from %s to %s.",
-        oldImage instanceof StreetsideImage ? oldImage.getId() : "‹none›",
-        newImage instanceof StreetsideImage ? newImage.getId() : "‹none›"));
-
-    imgKeyValue.setEnabled(newImage instanceof StreetsideImage);
-    final String newImageKey = newImage instanceof StreetsideImage ? newImage.getId() : null;
-    if (newImageKey != null) {
-      imageLinkChangeListener = b -> imgLinkAction.setURL(StreetsideURL.MainWebsite.browseImage(newImageKey));
-      imageLinkChangeListener.valueChanged(null);
-      StreetsideProperties.IMAGE_LINK_TO_BLUR_EDITOR.addListener(imageLinkChangeListener);
-
-      imgKeyValue.setText(newImageKey);
-      copyImgKeyAction.setContents(new StringSelection(newImageKey));
-      addStreetsideTagAction.setTag(new Tag("streetside", newImageKey));
-    } else {
-      if (imageLinkChangeListener != null) {
-        StreetsideProperties.IMAGE_LINK_TO_BLUR_EDITOR.removeListener(imageLinkChangeListener);
-        imageLinkChangeListener = null;
-      }
-      imgLinkAction.setURL(null);
-
-      imgKeyValue.setText('‹' + I18n.tr("image has no key") + '›');
-      copyImgKeyAction.setContents(null);
-      addStreetsideTagAction.setTag(null);
+        createLayout(root, true, null);
+        selectedImageChanged(null, null);
     }
 
-    final boolean partOfSequence = newImage != null && newImage.getSequence() != null
-        && newImage.getSequence().getId() != null;
-    seqKeyValue.setEnabled(partOfSequence);
-    if (partOfSequence) {
-      seqKeyValue.setText(newImage.getSequence().getId());
-    } else {
-      seqKeyValue.setText('‹' + I18n.tr("sequence has no id") + '›');
+    public static ImageInfoPanel getInstance() {
+        synchronized (ImageInfoPanel.class) {
+            if (instance == null) {
+                instance = new ImageInfoPanel();
+            }
+            return instance;
+        }
     }
-  }
 
-  /* (non-Javadoc)
-   * @see org.openstreetmap.josm.data.SelectionChangedListener#selectionChanged(java.util.Collection)
-   */
-  @Override
-  public synchronized void selectionChanged(final SelectionChangeEvent event) {
-    final Collection<? extends OsmPrimitive> sel = event.getSelection();
-    if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-      LOGGER.log(Logging.LEVEL_DEBUG,
-          String.format("Selection changed. %d primitives are selected.", sel == null ? 0 : sel.size()));
+    /**
+     * Destroys the unique instance of the class.
+     */
+    public static synchronized void destroyInstance() {
+        instance = null;
     }
-    addStreetsideTagAction.setTarget(sel != null && sel.size() == 1 ? sel.iterator().next() : null);
-  }
+
+    @Override
+    protected void stateChanged() {
+        super.stateChanged();
+        if (isDialogShowing()) { // If the user opens the dialog once, no longer show the help message
+            StreetsideProperties.IMAGEINFO_HELP_COUNTDOWN.put(0);
+        }
+    }
+
+    @Override
+    public void imagesAdded() {
+        // Method is not needed, but enforced by the interface StreetsideDataListener
+    }
+
+    @Override
+    public synchronized void selectedImageChanged(final StreetsideImage oldImage, final StreetsideImage newImage) {
+        LOGGER.info(() -> String.format("Selected Streetside image changed from %s to %s.",
+                oldImage != null ? oldImage.id() : "‹none›", newImage != null ? newImage.id() : "‹none›"));
+
+        final String newImageKey = newImage != null ? newImage.id() : null;
+        if (newImageKey != null) {
+            imageLinkChangeListener = b -> imgLinkAction.setURL(StreetsideURL.MainWebsite.browseImage(newImage));
+            imageLinkChangeListener.valueChanged(null);
+            StreetsideProperties.IMAGE_LINK_TO_BLUR_EDITOR.addListener(imageLinkChangeListener);
+
+        } else {
+            if (imageLinkChangeListener != null) {
+                StreetsideProperties.IMAGE_LINK_TO_BLUR_EDITOR.removeListener(imageLinkChangeListener);
+                imageLinkChangeListener = null;
+            }
+            imgLinkAction.setURL(null);
+        }
+    }
+
+    @Override
+    public synchronized void selectionChanged(final SelectionChangeEvent event) {
+        final Collection<? extends OsmPrimitive> sel = event.getSelection();
+        if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
+            LOGGER.log(Logging.LEVEL_DEBUG,
+                    "Selection changed. {0} primitives are selected.", sel == null ? 0 : sel.size());
+        }
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/StreetsideViewerHelpPopup.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/StreetsideViewerHelpPopup.java	(revision 36194)
+++ 	(revision )
@@ -1,95 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.streetside.gui.imageinfo;
-
-import java.awt.BorderLayout;
-import java.awt.Color;
-import java.awt.Component;
-import java.awt.IllegalComponentStateException;
-import java.awt.event.ActionEvent;
-import java.text.MessageFormat;
-import java.util.logging.Logger;
-
-import javax.swing.AbstractAction;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JPopupMenu;
-import javax.swing.JTextPane;
-
-import org.openstreetmap.josm.plugins.streetside.gui.boilerplate.SelectableLabel;
-import org.openstreetmap.josm.plugins.streetside.gui.boilerplate.StreetsideButton;
-import org.openstreetmap.josm.plugins.streetside.utils.StreetsideColorScheme;
-import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
-import org.openstreetmap.josm.tools.I18n;
-import org.openstreetmap.josm.tools.ImageProvider;
-import org.openstreetmap.josm.tools.Logging;
-
-public class StreetsideViewerHelpPopup extends JPopupMenu {
-
-  private static final long serialVersionUID = -7840242522398163839L;
-
-  private static final Logger LOGGER = Logger.getLogger(StreetsideViewerHelpPopup.class.getCanonicalName());
-
-  private final Component invokerComp;
-  private boolean alreadyDisplayed;
-
-  public StreetsideViewerHelpPopup(Component invoker) {
-
-    invokerComp = invoker;
-    removeAll();
-    setLayout(new BorderLayout());
-
-    JPanel topBar = new JPanel();
-    topBar.add(new JLabel(ImageProvider.get("streetside-logo-white")));
-    topBar.setBackground(StreetsideColorScheme.TOOLBAR_DARK_GREY);
-    add(topBar, BorderLayout.NORTH);
-
-    JTextPane mainText = new JTextPane();
-    mainText.setContentType("text/html");
-    mainText.setFont(SelectableLabel.DEFAULT_FONT);
-    mainText.setText("<html><div style='width:250px'>"
-        + "Welcome to the Microsoft Streetside JOSM Plugin. To view the vector bubbles for the 360 degree imagery, select Imagery->Streetside from the JOSM menu."
-        + "<br><br>"
-        + "Once the blue bubbles appear on the map, click on a vector bubble and undock/maximize the 360 viewer to view the imagery."
-        + "</div></html>");
-    add(mainText, BorderLayout.CENTER);
-
-    JPanel bottomBar = new JPanel();
-    bottomBar.setBackground(new Color(0x00FFFFFF, true));
-    StreetsideButton infoButton = new StreetsideButton(ImageInfoPanel.getInstance().getToggleAction());
-    infoButton.addActionListener(e -> setVisible(false));
-    bottomBar.add(infoButton);
-    StreetsideButton closeBtn = new StreetsideButton(new AbstractAction() {
-      private static final long serialVersionUID = -6193886964751195196L;
-
-      @Override
-      public void actionPerformed(ActionEvent e) {
-        setVisible(false);
-        StreetsideProperties.IMAGEINFO_HELP_COUNTDOWN.put(0);
-      }
-    });
-
-    closeBtn.setText(I18n.tr("I got it, close this."));
-    bottomBar.add(closeBtn);
-    add(bottomBar, BorderLayout.SOUTH);
-
-    setBackground(Color.WHITE);
-  }
-
-  /**
-   * @return <code>true</code> if the popup is displayed
-   */
-  public boolean showPopup() {
-    if (!alreadyDisplayed && invokerComp.isShowing()) {
-      try {
-        show(invokerComp, invokerComp.getWidth(), 0);
-        alreadyDisplayed = true;
-        return true;
-      } catch (IllegalComponentStateException e) {
-        LOGGER.log(Logging.LEVEL_ERROR, MessageFormat.format(
-            "Could not show ImageInfoHelpPopup, because probably the invoker component has disappeared from screen. {0}",
-            e.getMessage()), e);
-      }
-    }
-    return false;
-  }
-}
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/StreetsideViewerPanel.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/StreetsideViewerPanel.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/StreetsideViewerPanel.java	(revision 36228)
@@ -4,6 +4,7 @@
 import java.awt.BorderLayout;
 import java.awt.GraphicsEnvironment;
-import java.text.MessageFormat;
+import java.io.Serial;
 import java.util.logging.Logger;
+import java.util.regex.Pattern;
 
 import javax.swing.JCheckBox;
@@ -12,5 +13,4 @@
 
 import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeListener;
-import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
 import org.openstreetmap.josm.plugins.streetside.StreetsideDataListener;
 import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
@@ -26,134 +26,134 @@
 import org.openstreetmap.josm.tools.Logging;
 
+/**
+ * The panel to view 360 images in
+ */
 public final class StreetsideViewerPanel extends JPanel implements StreetsideDataListener {
 
-  private static final long serialVersionUID = 4141847503072417190L;
+    @Serial
+    private static final long serialVersionUID = 4141847503072417190L;
 
-  private static final Logger LOGGER = Logger.getLogger(StreetsideViewerPanel.class.getCanonicalName());
-  private static ThreeSixtyDegreeViewerPanel threeSixtyDegreeViewerPanel;
-  private JCheckBox highResImageryCheck;
-  private WebLinkAction imgLinkAction;
-  private ImageReloadAction imgReloadAction;
-  private ValueChangeListener<Boolean> imageLinkChangeListener;
-  private StreetsideViewerHelpPopup streetsideViewerHelp;
+    private static final Logger LOGGER = Logger.getLogger(StreetsideViewerPanel.class.getCanonicalName());
+    private static ThreeSixtyDegreeViewerPanel threeSixtyDegreeViewerPanel;
+    private WebLinkAction imgLinkAction;
+    private ValueChangeListener<Boolean> imageLinkChangeListener;
 
-  public StreetsideViewerPanel() {
+    /**
+     * Create a new 360 viewer
+     */
+    public StreetsideViewerPanel() {
+        super(new BorderLayout());
 
-    super(new BorderLayout());
+        SwingUtilities.invokeLater(this::initializeAndStartGUI);
 
-    SwingUtilities.invokeLater(this::initializeAndStartGUI);
+        selectedImageChanged(null, null);
 
-    selectedImageChanged(null, null);
-
-    setToolTipText(
-        I18n.tr("Select Microsoft Streetside from the Imagery menu, then click on a blue vector bubble.."));
-  }
-
-  public static CubemapBox getCubemapBox() {
-    return threeSixtyDegreeViewerPanel.getCubemapBox();
-  }
-
-  /**
-   * @return the threeSixtyDegreeViewerPanel
-   */
-  public static ThreeSixtyDegreeViewerPanel getThreeSixtyDegreeViewerPanel() {
-    return threeSixtyDegreeViewerPanel;
-  }
-
-  private void initializeAndStartGUI() {
-
-    threeSixtyDegreeViewerPanel = new ThreeSixtyDegreeViewerPanel();
-
-    if (!GraphicsEnvironment.isHeadless()) {
-      GraphicsUtils.PlatformHelper.run(threeSixtyDegreeViewerPanel::initialize);
+        setToolTipText(
+                I18n.tr("Select Microsoft Streetside from the Imagery menu, then click on a blue vector bubble.."));
     }
 
-    add(threeSixtyDegreeViewerPanel, BorderLayout.CENTER);
-    revalidate();
-    repaint();
-    JPanel checkPanel = new JPanel();
+    /**
+     * Get the {@link CubemapBox} for showing images
+     * @return The box for images
+     */
+    public static CubemapBox getCubemapBox() {
+        return threeSixtyDegreeViewerPanel.getCubemapBox();
+    }
 
-    imgReloadAction = new ImageReloadAction("Reload");
+    /**
+     * Get the current 360 viewer panel
+     * @return the threeSixtyDegreeViewerPanel
+     */
+    public static ThreeSixtyDegreeViewerPanel getThreeSixtyDegreeViewerPanel() {
+        return threeSixtyDegreeViewerPanel;
+    }
 
-    StreetsideButton imgReloadButton = new StreetsideButton(imgReloadAction);
-
-    highResImageryCheck = new JCheckBox("High resolution");
-    highResImageryCheck.setSelected(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get());
-    highResImageryCheck.addActionListener(
-        action -> StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.put(highResImageryCheck.isSelected()));
-    StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.addListener(valueChange -> highResImageryCheck
-        .setSelected(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get()));
-    checkPanel.add(highResImageryCheck, BorderLayout.WEST);
-    checkPanel.add(imgReloadButton, BorderLayout.EAST);
-
-    JPanel privacyLink = new JPanel();
-
-    imgLinkAction = new WebLinkAction("Report a privacy concern with this image", null);
-    privacyLink.add(new StreetsideButton(imgLinkAction, true));
-    checkPanel.add(privacyLink, BorderLayout.PAGE_END);
-
-    add(threeSixtyDegreeViewerPanel, BorderLayout.CENTER);
-
-    JPanel bottomPanel = new JPanel();
-    bottomPanel.add(checkPanel, BorderLayout.NORTH);
-    bottomPanel.add(privacyLink, BorderLayout.SOUTH);
-
-    add(bottomPanel, BorderLayout.PAGE_END);
-  }
-
-  /*
-   * (non-Javadoc)
-   *
-   * @see
-   * org.openstreetmap.josm.plugins.streetside.StreetsideDataListener#imagesAdded(
-   * )
-   */
-  @Override
-  public void imagesAdded() {
-    // Method is not needed, but enforcesd by the interface StreetsideDataListener
-  }
-
-  /*
-   * (non-Javadoc)
-   *
-   * @see org.openstreetmap.josm.plugins.streetside.StreetsideDataListener#
-   * selectedImageChanged(org.openstreetmap.josm.plugins.streetside.
-   * StreetsideAbstractImage,
-   * org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage)
-   */
-  @Override
-  public synchronized void selectedImageChanged(final StreetsideAbstractImage oldImage,
-      final StreetsideAbstractImage newImage) {
-
-    // method is invoked with null initially by framework
-    if (newImage != null) {
-
-      LOGGER.info(String.format("Selected Streetside image changed from %s to %s.",
-          oldImage instanceof StreetsideImage ? oldImage.getId() : "‹none›",
-          newImage instanceof StreetsideImage ? newImage.getId() : "‹none›"));
-
-      final String newImageId = CubemapBuilder.getInstance().getCubemap() != null
-          ? CubemapBuilder.getInstance().getCubemap().getId()
-          : newImage instanceof StreetsideImage ? ((StreetsideImage) newImage).getId() : null;
-      if (newImageId != null) {
-        final String bubbleId = CubemapUtils.convertQuaternary2Decimal(newImageId);
-        imageLinkChangeListener = b -> imgLinkAction
-            .setURL(StreetsideURL.MainWebsite.streetsidePrivacyLink(bubbleId));
-
-        if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-          LOGGER.log(Logging.LEVEL_DEBUG, MessageFormat
-              .format("Privacy link set for Streetside image {0} quadKey {1}", bubbleId, newImageId));
+    private synchronized void initializeAndStartGUI() {
+        try {
+            threeSixtyDegreeViewerPanel = new ThreeSixtyDegreeViewerPanel();
+        } catch (NoClassDefFoundError e) {
+            Logging.trace(e);
+            return;
         }
 
-        imageLinkChangeListener.valueChanged(null);
-        StreetsideProperties.CUBEMAP_LINK_TO_BLUR_EDITOR.addListener(imageLinkChangeListener);
-      } else {
-        if (imageLinkChangeListener != null) {
-          StreetsideProperties.CUBEMAP_LINK_TO_BLUR_EDITOR.removeListener(imageLinkChangeListener);
-          imageLinkChangeListener = null;
+        if (!GraphicsEnvironment.isHeadless()) {
+            GraphicsUtils.PlatformHelper.run(threeSixtyDegreeViewerPanel::initialize);
         }
-        imgLinkAction.setURL(null);
-      }
+
+        add(threeSixtyDegreeViewerPanel, BorderLayout.CENTER);
+        revalidate();
+        repaint();
+        final var checkPanel = new JPanel();
+
+        final var imgReloadAction = new ImageReloadAction("Reload");
+
+        final var imgReloadButton = new StreetsideButton(imgReloadAction);
+
+        final var highResImageryCheck = new JCheckBox("High resolution");
+        highResImageryCheck.setSelected(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get());
+        highResImageryCheck.addActionListener(
+                action -> StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.put(highResImageryCheck.isSelected()));
+        StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.addListener(valueChange -> highResImageryCheck
+                .setSelected(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get()));
+        checkPanel.add(highResImageryCheck, BorderLayout.WEST);
+        checkPanel.add(imgReloadButton, BorderLayout.EAST);
+
+        final var privacyLink = new JPanel();
+
+        imgLinkAction = new WebLinkAction("Report a privacy concern with this image", null);
+        privacyLink.add(new StreetsideButton(imgLinkAction, true));
+        checkPanel.add(privacyLink, BorderLayout.PAGE_END);
+
+        add(threeSixtyDegreeViewerPanel, BorderLayout.CENTER);
+
+        final var bottomPanel = new JPanel();
+        bottomPanel.add(checkPanel, BorderLayout.NORTH);
+        bottomPanel.add(privacyLink, BorderLayout.SOUTH);
+
+        add(bottomPanel, BorderLayout.PAGE_END);
     }
-  }
+
+    @Override
+    public void imagesAdded() {
+        // Method is not needed, but enforcesd by the interface StreetsideDataListener
+    }
+
+    @Override
+    public synchronized void selectedImageChanged(final StreetsideImage oldImage, final StreetsideImage newImage) {
+        // method is invoked with null initially by framework
+        if (newImage != null) {
+            LOGGER.info(() -> String.format("Selected Streetside image changed from %s to %s.",
+                    oldImage != null ? oldImage.id() : "‹none›", newImage.id()));
+
+            final var newImageId = CubemapBuilder.getInstance().getCubemap() != null
+                    ? CubemapBuilder.getInstance().getCubemap().id()
+                    : newImage.id();
+            if (newImageId != null) {
+                updateLinksToNewImage(newImageId);
+            } else {
+                if (imageLinkChangeListener != null) {
+                    StreetsideProperties.CUBEMAP_LINK_TO_BLUR_EDITOR.removeListener(imageLinkChangeListener);
+                    imageLinkChangeListener = null;
+                }
+                imgLinkAction.setURL(null);
+            }
+        }
+    }
+
+    private void updateLinksToNewImage(String newImageId) {
+        final var matcher = Pattern.compile("/tiles/hs([0-9]*)").matcher(newImageId);
+        if (matcher.find()) {
+            final var bubbleId = CubemapUtils.convertQuaternary2Decimal(matcher.group(1));
+            imageLinkChangeListener = b -> imgLinkAction
+                    .setURL(StreetsideURL.MainWebsite.streetsidePrivacyLink(bubbleId));
+
+            if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
+                LOGGER.log(Logging.LEVEL_DEBUG, "Privacy link set for Streetside image {0} quadKey {1}",
+                        new Object[] {bubbleId, newImageId});
+            }
+
+            imageLinkChangeListener.valueChanged(null);
+            StreetsideProperties.CUBEMAP_LINK_TO_BLUR_EDITOR.addListener(imageLinkChangeListener);
+        }
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/ThreeSixtyDegreeViewerPanel.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/ThreeSixtyDegreeViewerPanel.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/ThreeSixtyDegreeViewerPanel.java	(revision 36228)
@@ -1,4 +1,6 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.plugins.streetside.gui.imageinfo;
+
+import java.io.Serial;
 
 import org.openstreetmap.josm.plugins.streetside.cubemap.CameraTransformer;
@@ -14,6 +16,6 @@
 import javafx.scene.control.Label;
 import javafx.scene.control.TextArea;
-import javafx.scene.image.Image;
 import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.layout.VBox;
@@ -22,202 +24,177 @@
 import javafx.scene.text.FontWeight;
 
-@SuppressWarnings("restriction")
+/**
+ * Create a panel for viewing cube mapped 360 iamges
+ */
 public class ThreeSixtyDegreeViewerPanel extends JFXPanel {
 
-  private static final long serialVersionUID = -4940350009018422000L;
-  private static final CameraTransformer cameraTransform = new CameraTransformer();
-  private static final double cameraDistance = 5000;
-  private static Scene cubemapScene;
-  private static Scene defaultScene;
-  private static Scene loadingScene;
-  private static Group root;
-  private static Group subGroup;
-  private static CubemapBox cubemapBox;
-  private static PerspectiveCamera camera;
-  private static double mousePosX;
-  private static double mousePosY;
-  private static double mouseOldX;
-  private static double mouseOldY;
-  private static double mouseDeltaX;
-  private static double mouseDeltaY;
-  private static Image front;
-  private static Image right;
-  private static Image back;
-  private static Image left;
-  private static Image up;
-  private static Image down;
-
-  public ThreeSixtyDegreeViewerPanel() {
-
-  }
-
-  private static Scene createDefaultScene() {
-
-    TextArea textArea = new TextArea();
-    textArea.setText("No Streetside image selected.");
-
-    VBox vbox = new VBox(textArea);
-
-    root = new Group();
-
-    camera = new PerspectiveCamera(true);
-    cameraTransform.setTranslate(0, 0, 0);
-    cameraTransform.getChildren().addAll(camera);
-    camera.setNearClip(0.1);
-    camera.setFarClip(1000000.0);
-    camera.setFieldOfView(42);
-    camera.setTranslateZ(-cameraDistance);
-    final PointLight light = new PointLight(Color.WHITE);
-
-    cameraTransform.getChildren().add(light);
-    light.setTranslateX(camera.getTranslateX());
-    light.setTranslateY(camera.getTranslateY());
-    light.setTranslateZ(camera.getTranslateZ());
-
-    root.getChildren().add(cameraTransform);
-
-    final double size = 100000D;
-
-    cubemapBox = new CubemapBox(null, null, null, null, null, null, size, camera);
-
-    subGroup = new Group();
-    subGroup.getChildren().add(cameraTransform);
-
-    cubemapScene = new Scene(new Group(root), 1024, 668, true, SceneAntialiasing.BALANCED);
-    cubemapScene.setFill(Color.TRANSPARENT);
-    cubemapScene.setCamera(camera);
-
-    cubemapScene.setOnKeyPressed(event -> {
-      double change = 10.0;
-      if (event.isShiftDown()) {
-        change = 50.0;
-      }
-      final KeyCode keycode = event.getCode();
-
-      if (keycode == KeyCode.W) {
-        camera.setTranslateZ(camera.getTranslateZ() + change);
-      }
-      if (keycode == KeyCode.S) {
-        camera.setTranslateZ(camera.getTranslateZ() - change);
-      }
-
-      if (keycode == KeyCode.A) {
-        camera.setTranslateX(camera.getTranslateX() - change);
-      }
-      if (keycode == KeyCode.D) {
-        camera.setTranslateX(camera.getTranslateX() + change);
-      }
-    });
-
-    cubemapScene.setOnMousePressed((MouseEvent me) -> {
-      mousePosX = me.getSceneX();
-      mousePosY = me.getSceneY();
-      mouseOldX = me.getSceneX();
-      mouseOldY = me.getSceneY();
-    });
-    cubemapScene.setOnMouseDragged((MouseEvent me) -> {
-      mouseOldX = mousePosX;
-      mouseOldY = mousePosY;
-      mousePosX = me.getSceneX();
-      mousePosY = me.getSceneY();
-      mouseDeltaX = mousePosX - mouseOldX;
-      mouseDeltaY = mousePosY - mouseOldY;
-
-      double modifier = 10.0;
-      final double modifierFactor = 0.1;
-
-      if (me.isControlDown()) {
-        modifier = 0.1;
-      }
-      if (me.isShiftDown()) {
-        modifier = 50.0;
-      }
-      if (me.isPrimaryButtonDown()) {
-        cameraTransform.ry.setAngle(
-            ((cameraTransform.ry.getAngle() + mouseDeltaX * modifierFactor * modifier * 2.0) % 360 + 540)
-                % 360 - 180); // +
-        cameraTransform.rx.setAngle(
-            ((cameraTransform.rx.getAngle() - mouseDeltaY * modifierFactor * modifier * 2.0) % 360 + 540)
-                % 360 - 180); // -
-
-      } else if (me.isSecondaryButtonDown()) {
-        final double z = camera.getTranslateZ();
-        final double newZ = z + mouseDeltaX * modifierFactor * modifier;
-        camera.setTranslateZ(newZ);
-      } else if (me.isMiddleButtonDown()) {
-        cameraTransform.t.setX(cameraTransform.t.getX() + mouseDeltaX * modifierFactor * modifier * 0.3); // -
-        cameraTransform.t.setY(cameraTransform.t.getY() + mouseDeltaY * modifierFactor * modifier * 0.3); // -
-      }
-    });
-
-    root.getChildren().addAll(cubemapBox, subGroup);
-    root.setAutoSizeChildren(true);
-
-    subGroup.setAutoSizeChildren(true);
-
-    // prevent content from disappearing after resizing
-    Platform.setImplicitExit(false);
-
-    defaultScene = new Scene(vbox, 200, 100);
-    return defaultScene;
-  }
-
-  private static void createLoadingScene() {
-    Label label = new Label(" Loading...");
-    label.setFont(Font.font(null, FontWeight.BOLD, 14));
-    VBox vbox = new VBox(label);
-    loadingScene = new Scene(vbox, 200, 100);
-  }
-
-  public void initialize() {
-
-    root = new Group();
-
-    camera = new PerspectiveCamera(true);
-    cameraTransform.setTranslate(0, 0, 0);
-    cameraTransform.getChildren().addAll(camera);
-    camera.setNearClip(0.1);
-    camera.setFarClip(1000000.0);
-    camera.setFieldOfView(42);
-    camera.setTranslateZ(-cameraDistance);
-    final PointLight light = new PointLight(Color.WHITE);
-
-    cameraTransform.getChildren().add(light);
-    light.setTranslateX(camera.getTranslateX());
-    light.setTranslateY(camera.getTranslateY());
-    light.setTranslateZ(camera.getTranslateZ());
-
-    root.getChildren().add(cameraTransform);
-
-    final double size = 100000D;
-
-    cubemapBox = new CubemapBox(front, right, back, left, up, down, size, camera);
-
-    subGroup = new Group();
-    subGroup.getChildren().add(cameraTransform);
-
-    createLoadingScene();
-
-    Platform.runLater(() -> setScene(createDefaultScene()));
-  }
-
-  public CubemapBox getCubemapBox() {
-    if (cubemapBox == null) {
-      // shouldn't happen
-      initialize();
-    }
-    return cubemapBox;
-  }
-
-  public Scene getDefaultScene() {
-    return defaultScene;
-  }
-
-  public Scene getCubemapScene() {
-    return cubemapScene;
-  }
-
-  public Scene getLoadingScene() {
-    return loadingScene;
-  }
+    @Serial
+    private static final long serialVersionUID = -7032369684012156320L;
+    private static final CameraTransformer cameraTransform = new CameraTransformer();
+    private static final double CAMERA_DISTANCE = 5000;
+    private static Scene cubemapScene;
+    private static Scene defaultScene;
+    private static Scene loadingScene;
+    private static Group root;
+    private static Group subGroup;
+    private static CubemapBox cubemapBox;
+    private static PerspectiveCamera camera;
+    private static double mousePosX;
+    private static double mousePosY;
+    private static double mouseOldX;
+    private static double mouseOldY;
+
+    /**
+     * Create the default scene
+     * @return The default scene (pretty much to tell the user that nothing is selected)
+     */
+    private static Scene createDefaultScene() {
+
+        final var textArea = new TextArea();
+        textArea.setText("No Streetside image selected.");
+
+        final var vbox = new VBox(textArea);
+
+        initializeStatic();
+
+        cubemapScene = new Scene(new Group(root), 1024, 668, true, SceneAntialiasing.BALANCED);
+        cubemapScene.setFill(Color.TRANSPARENT);
+        cubemapScene.setCamera(camera);
+
+        cubemapScene.setOnKeyPressed(ThreeSixtyDegreeViewerPanel::keyPressed);
+        cubemapScene.setOnMousePressed(ThreeSixtyDegreeViewerPanel::mouseClicked);
+        cubemapScene.setOnMouseDragged(ThreeSixtyDegreeViewerPanel::mouseDragged);
+
+        root.getChildren().addAll(cubemapBox, subGroup);
+        root.setAutoSizeChildren(true);
+
+        subGroup.setAutoSizeChildren(true);
+
+        // prevent content from disappearing after resizing
+        Platform.setImplicitExit(false);
+
+        defaultScene = new Scene(vbox, 200, 100);
+        return defaultScene;
+    }
+
+    private static void keyPressed(KeyEvent event) {
+        var change = 10.0;
+        if (event.isShiftDown()) {
+            change = 50.0;
+        }
+        final var keycode = event.getCode();
+
+        if (keycode == KeyCode.W) {
+            camera.setTranslateZ(camera.getTranslateZ() + change);
+        }
+        if (keycode == KeyCode.S) {
+            camera.setTranslateZ(camera.getTranslateZ() - change);
+        }
+
+        if (keycode == KeyCode.A) {
+            camera.setTranslateX(camera.getTranslateX() - change);
+        }
+        if (keycode == KeyCode.D) {
+            camera.setTranslateX(camera.getTranslateX() + change);
+        }
+    }
+
+    private static void mouseClicked(MouseEvent me) {
+        mousePosX = me.getSceneX();
+        mousePosY = me.getSceneY();
+        mouseOldX = me.getSceneX();
+        mouseOldY = me.getSceneY();
+    }
+
+    private static void mouseDragged(MouseEvent me) {
+        mouseOldX = mousePosX;
+        mouseOldY = mousePosY;
+        mousePosX = me.getSceneX();
+        mousePosY = me.getSceneY();
+        final double mouseDeltaX = mousePosX - mouseOldX;
+        final double mouseDeltaY = mousePosY - mouseOldY;
+
+        var modifier = 0.375;
+        final var modifierFactor = 0.1;
+
+        if (me.isControlDown()) {
+            modifier = 0.1;
+        }
+        if (me.isShiftDown()) {
+            modifier = 50.0;
+        }
+        if (me.isSecondaryButtonDown()) { // JOSM viewer uses right-click for moving.
+            cameraTransform.setRy(
+                    ((cameraTransform.ry.getAngle() - mouseDeltaX * modifierFactor * modifier * 2.0) % 360 + 540)
+                            % 360 - 180); // +
+            cameraTransform.setRx(
+                    ((cameraTransform.rx.getAngle() + mouseDeltaY * modifierFactor * modifier * 2.0) % 360 + 540)
+                            % 360 - 180); // -
+        } else if (me.isPrimaryButtonDown()) {
+            final double z = camera.getTranslateZ();
+            final double newZ = z + mouseDeltaX * modifierFactor * modifier;
+            camera.setTranslateZ(newZ);
+        } else if (me.isMiddleButtonDown()) {
+            cameraTransform.setTx(cameraTransform.t.getX() + mouseDeltaX * modifierFactor * modifier * 0.3); // -
+            cameraTransform.setTy(cameraTransform.t.getY() + mouseDeltaY * modifierFactor * modifier * 0.3); // -
+        }
+    }
+
+    private static void createLoadingScene() {
+        final var label = new Label(" Loading...");
+        label.setFont(Font.font(null, FontWeight.BOLD, 14));
+        final var vbox = new VBox(label);
+        loadingScene = new Scene(vbox, 200, 100);
+    }
+
+    private static void initializeStatic() {
+        root = new Group();
+
+        camera = new PerspectiveCamera(true);
+        cameraTransform.setTranslate(0, 0, 0);
+        cameraTransform.getChildren().addAll(camera);
+        camera.setNearClip(0.1);
+        camera.setFarClip(1000000.0);
+        camera.setFieldOfView(42);
+        camera.setTranslateZ(-CAMERA_DISTANCE);
+        final var light = new PointLight(Color.WHITE);
+
+        cameraTransform.getChildren().add(light);
+        light.setTranslateX(camera.getTranslateX());
+        light.setTranslateY(camera.getTranslateY());
+        light.setTranslateZ(camera.getTranslateZ());
+
+        root.getChildren().add(cameraTransform);
+
+        cubemapBox = new CubemapBox(null, null, null, null, null, null, 100_000d, camera);
+
+        subGroup = new Group();
+        subGroup.getChildren().add(cameraTransform);
+    }
+
+    void initialize() {
+        initializeStatic();
+        createLoadingScene();
+        Platform.runLater(() -> setScene(createDefaultScene()));
+    }
+
+    public CubemapBox getCubemapBox() {
+        if (cubemapBox == null) {
+            // shouldn't happen
+            initialize();
+        }
+        return cubemapBox;
+    }
+
+    public Scene getDefaultScene() {
+        return defaultScene;
+    }
+
+    public Scene getCubemapScene() {
+        return cubemapScene;
+    }
+
+    public Scene getLoadingScene() {
+        return loadingScene;
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/WebLinkAction.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/WebLinkAction.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/gui/imageinfo/WebLinkAction.java	(revision 36228)
@@ -3,5 +3,6 @@
 
 import java.awt.event.ActionEvent;
-import java.io.IOException;
+import java.io.Serial;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.util.logging.Logger;
@@ -11,42 +12,51 @@
 
 import org.openstreetmap.josm.gui.Notification;
-import org.openstreetmap.josm.plugins.streetside.utils.StreetsideUtils;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
 import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.OpenBrowser;
 
+/**
+ * Open an image on Microsoft's Streetside website
+ */
 public class WebLinkAction extends AbstractAction {
 
-  private static final long serialVersionUID = -8168227661356480455L;
+    @Serial
+    private static final long serialVersionUID = 6157320554869780625L;
 
-  private static final Logger LOGGER = Logger.getLogger(WebLinkAction.class.getCanonicalName());
+    private static final Logger LOGGER = Logger.getLogger(WebLinkAction.class.getCanonicalName());
 
-  private URL url;
+    private URL url;
 
-  public WebLinkAction(final String name, final URL url) {
-    super(name, ImageProvider.get("link", ImageSizes.SMALLICON));
-    setURL(url);
-  }
+    /**
+     * Create a new web link
+     * @param name The name to show the user
+     * @param url The URL to open
+     */
+    public WebLinkAction(final String name, final URL url) {
+        super(name, ImageProvider.get("link", ImageSizes.SMALLICON));
+        setURL(url);
+    }
 
-  /**
-   * @param url the url to set
-   */
-  public final void setURL(URL url) {
-    this.url = url;
-    setEnabled(url != null);
-  }
+    /**
+     * Set the URL for this action
+     * @param url the url to set
+     */
+    public final void setURL(URL url) {
+        this.url = url;
+        setEnabled(url != null);
+    }
 
-  /* (non-Javadoc)
-   * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
-   */
-  @Override
-  public void actionPerformed(ActionEvent e) {
-    try {
-      StreetsideUtils.browse(url);
-    } catch (IOException e1) {
-      String msg = "Could not open the URL " + url == null ? "‹null›" : url + " in a browser";
-      LOGGER.log(Logging.LEVEL_WARN, msg, e1);
-      new Notification(msg).setIcon(JOptionPane.WARNING_MESSAGE).show();
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        try {
+            if (this.url != null) {
+                OpenBrowser.displayUrl(this.url.toURI());
+            }
+        } catch (URISyntaxException e1) {
+            String msg = url + " in a browser";
+            LOGGER.log(Logging.LEVEL_WARN, msg, e1);
+            new Notification(msg).setIcon(JOptionPane.WARNING_MESSAGE).show();
+        }
     }
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/download/BoundsDownloadRunnable.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/download/BoundsDownloadRunnable.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/download/BoundsDownloadRunnable.java	(revision 36228)
@@ -4,5 +4,4 @@
 import java.awt.GraphicsEnvironment;
 import java.io.IOException;
-import java.net.HttpURLConnection;
 import java.net.URL;
 import java.net.URLConnection;
@@ -20,64 +19,41 @@
 public abstract class BoundsDownloadRunnable implements Runnable {
 
-  private static final Logger LOGGER = Logger.getLogger(BoundsDownloadRunnable.class.getCanonicalName());
+    private static final Logger LOGGER = Logger.getLogger(BoundsDownloadRunnable.class.getCanonicalName());
 
-  protected Bounds bounds;
+    protected final Bounds bounds;
 
-  protected BoundsDownloadRunnable(final Bounds bounds) {
-    this.bounds = bounds;
-  }
+    protected BoundsDownloadRunnable(final Bounds bounds) {
+        this.bounds = bounds;
+    }
 
-  /**
-   * Logs information about the given connection via {@link Logger}.
-   * If it's a {@link HttpURLConnection}, the request method, the response code and the URL itself are logged.
-   * Otherwise only the URL is logged.
-   *
-   * @param con  the {@link URLConnection} for which information is logged
-   * @param info an additional info text, which is appended to the output in braces
-   * @throws IOException if {@link HttpURLConnection#getResponseCode()} throws an {@link IOException}
-   */
-  public static void logConnectionInfo(final URLConnection con, final String info) throws IOException {
-    final StringBuilder message;
-    if (con instanceof HttpURLConnection) {
-      message = new StringBuilder(((HttpURLConnection) con).getRequestMethod()).append(' ').append(con.getURL())
-          .append(" → ").append(((HttpURLConnection) con).getResponseCode());
-    } else {
-      message = new StringBuilder("Download from ").append(con.getURL());
+    protected abstract Function<Bounds, URL> getUrlGenerator();
+
+    @Override
+    public void run() {
+        URL nextURL = getUrlGenerator().apply(bounds);
+        if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
+            LOGGER.log(Logging.LEVEL_DEBUG, "Downloading bounds: URL: {0}", nextURL);
+        }
+        try {
+            while (nextURL != null) {
+                if (Thread.interrupted()) {
+                    LOGGER.log(Logging.LEVEL_ERROR, "{0} for {1} interrupted!",
+                            new Object[] { getClass().getSimpleName(), bounds });
+                    return;
+                }
+                final URLConnection con = nextURL.openConnection();
+                run(con);
+                nextURL = APIv3.parseNextFromLinkHeaderValue(con.getHeaderField("Link"));
+            }
+        } catch (IOException e) {
+            String message = "Could not read from URL " + nextURL + "!";
+            LOGGER.log(Logging.LEVEL_WARN, message, e);
+            if (!GraphicsEnvironment.isHeadless()) {
+                new Notification(message).setIcon(StreetsidePlugin.LOGO.setSize(ImageSizes.LARGEICON).get())
+                        .setDuration(Notification.TIME_LONG).show();
+            }
+        }
     }
-    if (info != null && info.length() >= 1) {
-      message.append(" (").append(info).append(')');
-    }
-    LOGGER.info(message::toString);
-  }
 
-  protected abstract Function<Bounds, URL> getUrlGenerator();
-
-  @Override
-  public void run() {
-    URL nextURL = getUrlGenerator().apply(bounds);
-    if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-      LOGGER.log(Logging.LEVEL_DEBUG, "Downloading bounds: URL: {0}", nextURL);
-    }
-    try {
-      while (nextURL != null) {
-        if (Thread.interrupted()) {
-          LOGGER.log(Logging.LEVEL_ERROR, "{0} for {1} interrupted!",
-              new Object[] { getClass().getSimpleName(), bounds });
-          return;
-        }
-        final URLConnection con = nextURL.openConnection();
-        run(con);
-        nextURL = APIv3.parseNextFromLinkHeaderValue(con.getHeaderField("Link"));
-      }
-    } catch (IOException e) {
-      String message = "Could not read from URL " + nextURL + "!";
-      LOGGER.log(Logging.LEVEL_WARN, message, e);
-      if (!GraphicsEnvironment.isHeadless()) {
-        new Notification(message).setIcon(StreetsidePlugin.LOGO.setSize(ImageSizes.LARGEICON).get())
-            .setDuration(Notification.TIME_LONG).show();
-      }
-    }
-  }
-
-  public abstract void run(final URLConnection connection) throws IOException;
+    public abstract void run(final URLConnection connection) throws IOException;
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/download/SequenceDownloadRunnable.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/download/SequenceDownloadRunnable.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/download/SequenceDownloadRunnable.java	(revision 36228)
@@ -6,170 +6,167 @@
 import java.net.URLConnection;
 import java.text.MessageFormat;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
-import java.util.EnumSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Function;
+import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import org.openstreetmap.josm.data.Bounds;
-import org.openstreetmap.josm.data.coor.LatLon;
-import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
 import org.openstreetmap.josm.plugins.streetside.StreetsideData;
 import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
-import org.openstreetmap.josm.plugins.streetside.StreetsideSequence;
-import org.openstreetmap.josm.plugins.streetside.cubemap.CubemapUtils;
 import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
-import org.openstreetmap.josm.plugins.streetside.utils.StreetsideSequenceIdGenerator;
 import org.openstreetmap.josm.plugins.streetside.utils.StreetsideURL.APIv3;
-import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.JosmRuntimeException;
 
 import jakarta.json.Json;
-import jakarta.json.JsonException;
 import jakarta.json.JsonObject;
-import jakarta.json.JsonValue;
+import jakarta.json.JsonString;
 import jakarta.json.stream.JsonParser;
 
+/**
+ * Download an area
+ */
 public final class SequenceDownloadRunnable extends BoundsDownloadRunnable {
-  private static final Logger LOG = Logger.getLogger(BoundsDownloadRunnable.class.getCanonicalName());
-  private static final Function<Bounds, URL> URL_GEN = APIv3::searchStreetsideImages;
-  private final StreetsideData data;
+    private static final Logger LOG = Logger.getLogger(BoundsDownloadRunnable.class.getCanonicalName());
+    private static final Function<Bounds, URL> URL_GEN = APIv3::searchStreetsideImages;
+    private final StreetsideData data;
+    private String logo;
+    private String copyright;
 
-  public SequenceDownloadRunnable(final StreetsideData data, final Bounds bounds) {
-    super(bounds);
-    this.data = data;
-  }
-
-  @Override
-  public void run(final URLConnection con) throws IOException {
-    if (Thread.interrupted()) {
-      return;
+    /**
+     * Create a new downloader
+     * @param data The data to add to
+     * @param bounds The bounds to download
+     */
+    public SequenceDownloadRunnable(final StreetsideData data, final Bounds bounds) {
+        super(bounds);
+        this.data = data;
     }
 
-    StreetsideSequence seq = new StreetsideSequence(StreetsideSequenceIdGenerator.generateId());
+    @Override
+    public void run(final URLConnection con) throws IOException {
+        if (Thread.interrupted()) {
+            return;
+        }
 
-    List<StreetsideImage> bubbleImages = new ArrayList<>();
+        final long startTime = System.currentTimeMillis();
 
-    final long startTime = System.currentTimeMillis();
+        // Structure is
+        // { "authenticationResultCode": "foo", "brandLogoUri": "bar", "copyright": "text", "resource
+        try (JsonParser parser = Json.createParser(con.getInputStream())) {
+            if (!parser.hasNext() || parser.next() != JsonParser.Event.START_OBJECT) {
+                throw new IllegalStateException("Expected an object");
+            }
 
-    try (JsonParser parser = Json.createParser(con.getInputStream())) {
-      if (!parser.hasNext() || parser.next() != JsonParser.Event.START_ARRAY) {
-        throw new IllegalStateException("Expected an array");
-      }
-
-      StreetsideImage previous = null;
-
-      while (parser.hasNext() && parser.next() == JsonParser.Event.START_OBJECT) {
-        // read everything from this START_OBJECT to the matching END_OBJECT
-        // and return it as a tree model ObjectNode
-        JsonObject node = parser.getObject();
-        // Discard the first sequence ('enabled') - it does not contain bubble data
-        if (node.get("id") != null && node.get("la") != null && node.get("lo") != null) {
-          StreetsideImage image = new StreetsideImage(
-              CubemapUtils.convertDecimal2Quaternary(node.getJsonNumber("id").longValue()),
-              new LatLon(node.getJsonNumber("la").doubleValue(), node.getJsonNumber("lo").doubleValue()),
-              node.getJsonNumber("he").doubleValue());
-          if (previous != null) {
-            image.setPr(Long.parseLong(previous.getId()));
-            previous.setNe(Long.parseLong(image.getId()));
-
-          }
-          previous = image;
-          if (node.containsKey("ad"))
-            image.setAd(node.getJsonNumber("ad").intValue());
-          if (node.containsKey("al"))
-            image.setAl(node.getJsonNumber("al").doubleValue());
-          if (node.containsKey("bl"))
-            image.setBl(node.getString("bl"));
-          if (node.containsKey("ml"))
-            image.setMl(node.getJsonNumber("ml").intValue());
-          if (node.containsKey("ne"))
-            image.setNe(node.getJsonNumber("ne").longValue());
-          if (node.containsKey("pi"))
-            image.setPi(node.getJsonNumber("pi").doubleValue());
-          if (node.containsKey("pr"))
-            image.setPr(node.getJsonNumber("pr").longValue());
-          if (node.containsKey("ro"))
-            image.setRo(node.getJsonNumber("ro").doubleValue());
-          if (node.containsKey("nbn"))
-              image.setNbn(node.getJsonArray("nbn").getValuesAs(JsonValue::toString));
-          if (node.containsKey("pbn"))
-              image.setPbn(node.getJsonArray("pbn").getValuesAs(JsonValue::toString));
-
-          // Add list of cubemap tile images to images
-          List<StreetsideImage> tiles = new ArrayList<>();
-
-          EnumSet.allOf(CubemapUtils.CubemapFaces.class).forEach(face -> {
-
-            for (int i = 0; i < 4; i++) {
-              // Initialize four-tiled cubemap faces (four images per cube side with 18-length
-              // Quadkey)
-              if (Boolean.FALSE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get())) {
-                StreetsideImage tile = new StreetsideImage(image.getId() + i);
-                tiles.add(tile);
-              }
-              // Initialize four-tiled cubemap faces (four images per cub eside with 20-length
-              // Quadkey)
-              if (Boolean.TRUE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get())) {
-                for (int j = 0; j < 4; j++) {
-                  StreetsideImage tile = new StreetsideImage(image.getId() + face.getValue()
-                      + CubemapUtils.rowCol2StreetsideCellAddressMap
-                          .get(i + Integer.toString(j)));
-                  tiles.add(tile);
-                }
-              }
-            }
-          });
-
-          bubbleImages.add(image);
-          LOG.info("Added image with id <" + image.getId() + ">");
-          if (Boolean.TRUE.equals(StreetsideProperties.PREDOWNLOAD_CUBEMAPS.get())) {
-            StreetsideData.downloadSurroundingCubemaps(image);
-          }
-        } else {
-          LOG.info(MessageFormat.format("Unparsable JSON node object: {0}", node));
+            parseJson(parser);
+            final long endTime = System.currentTimeMillis();
+            LOG.log(Level.INFO, "Successfully loaded {0} Microsoft Streetside images in {1} seconds.",
+                    new Object[] {this.data.getImages().size(), (endTime - startTime) / 1000});
         }
-      }
-    } catch (ClassCastException | JsonException e) {
-      LOG.log(Logging.LEVEL_ERROR, e, () -> MessageFormat
-          .format("JSON parsing error occurred during Streetside sequence download {0}", e.getMessage()));
-    } catch (IOException e) {
-      LOG.log(Logging.LEVEL_ERROR, e, () -> MessageFormat
-          .format("Input/output error occurred during Streetside sequence download {0}", e.getMessage()));
     }
 
-    /*
-     * Top Level Bubble Metadata in Streetside are bubble (aka images) not Sequences
-     *  so a sequence needs to be created and have images added to it. If the distribution
-     *  of Streetside images is non-sequential, the Mapillary "Walking Action" may behave
-     *  unpredictably.
-     */
-    seq.add(bubbleImages);
-
-    if (Boolean.TRUE.equals(StreetsideProperties.CUT_OFF_SEQUENCES_AT_BOUNDS.get())) {
-      for (StreetsideAbstractImage img : seq.getImages()) {
-        if (bounds.contains(img.getLatLon())) {
-          data.add(img);
-        } else {
-          seq.remove(img);
+    void parseJson(JsonParser parser) {
+        while (parser.hasNext()) {
+            if (Objects.requireNonNull(parser.next()) == JsonParser.Event.KEY_NAME) {
+                switch (parser.getString()) {
+                case "errorDetails" -> parseErrorDetails(parser);
+                case "resourceSets" -> parseResourceSets(parser);
+                case "brandLogoUri" -> parseBrandLogoUri(parser);
+                case "copyright" -> parseCopyright(parser);
+                default -> { /* Do nothing for now */ }
+                }
+            }
         }
-      }
-    } else {
-      boolean sequenceCrossesThroughBounds = false;
-      for (int i = 0; i < seq.getImages().size() && !sequenceCrossesThroughBounds; i++) {
-        sequenceCrossesThroughBounds = bounds.contains(seq.getImages().get(i).getLatLon());
-      }
-      if (sequenceCrossesThroughBounds) {
-        data.addAll(seq.getImages(), true);
-      }
     }
 
-    final long endTime = System.currentTimeMillis();
-    LOG.info(MessageFormat.format("Successfully loaded {0} Microsoft Streetside images in {1} seconds.",
-        seq.getImages().size(), (endTime - startTime) / 1000));
-  }
+    private static void parseErrorDetails(JsonParser parser) {
+        if (parser.next() == JsonParser.Event.START_ARRAY) {
+            final var errors = new StringBuilder();
+            while (parser.next() != JsonParser.Event.END_ARRAY) {
+                if (parser.currentEvent() == JsonParser.Event.VALUE_STRING) {
+                    errors.append(parser.getString()).append('\n');
+                }
+            }
+            throw new JosmRuntimeException(errors.toString());
+        }
+    }
 
-  @Override
-  protected Function<Bounds, URL> getUrlGenerator() {
-    return URL_GEN;
-  }
+    private void parseResourceSets(JsonParser parser) {
+        if (parser.next() == JsonParser.Event.START_ARRAY) {
+            while (parser.hasNext() && parser.next() == JsonParser.Event.START_OBJECT) {
+                while (parser.hasNext() && parser.currentEvent() != JsonParser.Event.END_OBJECT) {
+                    if (parser.next() == JsonParser.Event.KEY_NAME
+                            && "resources".equals(parser.getString())) {
+                        parser.next();
+                        List<StreetsideImage> bubbleImages = new ArrayList<>();
+                        parseResource(parser, bubbleImages);
+                        this.data.addAll(bubbleImages, true);
+                    }
+                }
+            }
+        }
+    }
+
+    private void parseBrandLogoUri(JsonParser parser) {
+        if (parser.next() == JsonParser.Event.VALUE_STRING) {
+            this.logo = parser.getString();
+        }
+    }
+
+    private void parseCopyright(JsonParser parser) {
+        if (parser.next() == JsonParser.Event.VALUE_STRING) {
+            this.copyright = parser.getString();
+        }
+    }
+
+    private void parseResource(JsonParser parser, List<StreetsideImage> bubbleImages) {
+        while (parser.hasNext() && parser.next() == JsonParser.Event.START_OBJECT) {
+            // read everything from this START_OBJECT to the matching END_OBJECT
+            // and return it as a tree model ObjectNode
+            JsonObject node = parser.getObject();
+            // Discard the first sequence ('enabled') - it does not contain bubble data
+            if (node.get("imageUrl") != null && node.get("lat") != null && node.get("lon") != null) {
+                final var id = node.getString("imageUrl");
+                final var lat = node.getJsonNumber("lat").doubleValue();
+                final var lon = node.getJsonNumber("lon").doubleValue();
+                final var heading = node.getJsonNumber("he").doubleValue();
+                final var pitch = node.containsKey("pi") ? node.getJsonNumber("pi").doubleValue() : Double.NaN;
+                final var roll = node.containsKey("ro") ? node.getJsonNumber("ro").doubleValue() : Double.NaN;
+                final var vintageStart = LocalDate
+                        .parse(node.getString("vintageStart").replace("GMT", "UTC"),
+                                DateTimeFormatter.ofPattern("dd LLL yyyy zzz"))
+                        .atStartOfDay().toInstant(ZoneOffset.UTC);
+                final var vintageEnd = LocalDate
+                        .parse(node.getString("vintageStart").replace("GMT", "UTC"),
+                                DateTimeFormatter.ofPattern("dd LLL yyyy zzz"))
+                        .atTime(LocalTime.MAX).toInstant(ZoneOffset.UTC);
+                final List<String> imageUrlSubdomains = node.getJsonArray("imageUrlSubdomains")
+                        .getValuesAs(JsonString.class).stream().map(JsonString::getString).toList();
+                final var zoomMax = node.getInt("zoomMax");
+                final var zoomMin = node.getInt("zoomMin");
+                final var imageHeight = node.getInt("imageHeight");
+                final var imageWidth = node.getInt("imageWidth");
+                final var image = new StreetsideImage(id, lat, lon, heading, pitch, roll, vintageStart,
+                        vintageEnd, this.logo, this.copyright, zoomMin, zoomMax, imageHeight, imageWidth,
+                        imageUrlSubdomains);
+                bubbleImages.add(image);
+                LOG.info(() -> "Added image with id <" + image.id() + ">");
+                if (Boolean.TRUE.equals(StreetsideProperties.PREDOWNLOAD_CUBEMAPS.get())) {
+                    this.data.downloadSurroundingCubemaps(image);
+                }
+            } else {
+                LOG.info(() -> MessageFormat.format("Unparsable JSON node object: {0}", node));
+            }
+        }
+    }
+
+    @Override
+    protected Function<Bounds, URL> getUrlGenerator() {
+        return URL_GEN;
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/download/StreetsideDownloader.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/download/StreetsideDownloader.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/download/StreetsideDownloader.java	(revision 36228)
@@ -25,219 +25,237 @@
 public final class StreetsideDownloader {
 
-  private static final Logger LOGGER = Logger.getLogger(StreetsideDownloader.class.getCanonicalName());
-  /**
-   * Max area to be downloaded
-   */
-  private static final double MAX_AREA = StreetsideProperties.MAX_DOWNLOAD_AREA.get();
-  /**
-   * Executor that will run the petitions.
-   */
-  private static ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 5, 100, TimeUnit.SECONDS,
-      new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.DiscardPolicy());
-  /**
-   * Indicates whether the last download request has been rejected because it requested an area that was too big.
-   * Iff true, the last download has been rejected, if false, it was executed.
-   */
-  private static boolean stoppedDownload;
-
-  private StreetsideDownloader() {
-    // Private constructor to avoid instantiation
-  }
-
-  /**
-   * Downloads all images of the area covered by the OSM data.
-   */
-  public static void downloadOSMArea() {
-    if (MainApplication.getLayerManager().getEditLayer() == null) {
-      return;
-    }
-    if (isAreaTooBig(MainApplication.getLayerManager().getEditLayer().data.getDataSourceBounds().stream()
-        .map(Bounds::getArea).reduce(0.0, Double::sum))) {
-      return;
-    }
-    MainApplication.getLayerManager().getEditLayer().data.getDataSourceBounds().stream()
-        .filter(bounds -> !StreetsideLayer.getInstance().getData().getBounds().contains(bounds))
-        .forEach(bounds -> {
-          StreetsideLayer.getInstance().getData().getBounds().add(bounds);
-          StreetsideDownloader.getImages(bounds.getMin(), bounds.getMax());
-        });
-  }
-
-  /**
-   * Gets all the images in a square. It downloads all the images of all the
-   * sequences that pass through the given rectangle.
-   *
-   * @param minLatLon The minimum latitude and longitude of the rectangle.
-   * @param maxLatLon The maximum latitude and longitude of the rectangle
-   */
-  public static void getImages(LatLon minLatLon, LatLon maxLatLon) {
-    if (minLatLon == null || maxLatLon == null) {
-      throw new IllegalArgumentException();
-    }
-    getImages(new Bounds(minLatLon, maxLatLon));
-  }
-
-  /**
-   * Gets the images within the given bounds.
-   *
-   * @param bounds A {@link Bounds} object containing the area to be downloaded.
-   */
-  public static void getImages(Bounds bounds) {
-    run(new StreetsideSquareDownloadRunnable(bounds));
-  }
-
-  /**
-   * Returns the current download mode.
-   *
-   * @return the currently enabled {@link DOWNLOAD_MODE}
-   */
-  public static DOWNLOAD_MODE getMode() {
-    return DOWNLOAD_MODE.fromPrefId(StreetsideProperties.DOWNLOAD_MODE.get());
-  }
-
-  private static void run(Runnable t) {
-    executor.execute(t);
-  }
-
-  /**
-   * If some part of the current view has not been downloaded, it is downloaded.
-   */
-  public static void downloadVisibleArea() {
-    Bounds view = MainApplication.getMap().mapView.getRealBounds();
-    if (isAreaTooBig(view.getArea())) {
-      return;
-    }
-    if (isViewDownloaded(view)) {
-      return;
-    }
-    StreetsideLayer.getInstance().getData().getBounds().add(view);
-    getImages(view);
-  }
-
-  private static boolean isViewDownloaded(Bounds view) {
-    int n = 15;
-    boolean[][] inside = new boolean[n][n];
-    for (int i = 0; i < n; i++) {
-      for (int j = 0; j < n; j++) {
-        if (isInBounds(new LatLon(view.getMinLat() + (view.getMaxLat() - view.getMinLat()) * ((double) i / n),
-            view.getMinLon() + (view.getMaxLon() - view.getMinLon()) * ((double) j / n)))) {
-          inside[i][j] = true;
-        }
-      }
-    }
-    for (int i = 0; i < n; i++) {
-      for (int j = 0; j < n; j++) {
-        if (!inside[i][j])
-          return false;
-      }
-    }
-    return true;
-  }
-
-  /**
-   * Checks if the given {@link LatLon} object lies inside the bounds of the
-   * image.
-   *
-   * @param latlon The coordinates to check.
-   * @return true if it lies inside the bounds; false otherwise;
-   */
-  private static boolean isInBounds(LatLon latlon) {
-    return StreetsideLayer.getInstance().getData().getBounds().parallelStream().anyMatch(b -> b.contains(latlon));
-  }
-
-  /**
-   * Checks if the area for which Streetside images should be downloaded is too big. This means that probably
-   * lots of Streetside images are going to be downloaded, slowing down the
-   * program too much. A notification is shown when the download has stopped or continued.
-   *
-   * @param area area to check
-   * @return {@code true} if the area is too big
-   */
-  private static boolean isAreaTooBig(final double area) {
-    final boolean tooBig = area > MAX_AREA;
-    if (!stoppedDownload && tooBig) {
-      new Notification(I18n
-          .tr("The Streetside layer has stopped downloading images, because the requested area is too big!")
-          + (getMode() == DOWNLOAD_MODE.VISIBLE_AREA
-              ? "\n" + I18n
-                  .tr("To solve this problem, you could zoom in and load a smaller area of the map.")
-              : (getMode() == DOWNLOAD_MODE.OSM_AREA ? "\n" + I18n.tr(
-                  "To solve this problem, you could switch to download mode ''{0}'' and load Streetside images for a smaller portion of the map.",
-                  DOWNLOAD_MODE.MANUAL_ONLY) : ""))).setIcon(StreetsidePlugin.LOGO.get())
-                      .setDuration(Notification.TIME_LONG).show();
-    }
-    if (stoppedDownload && !tooBig) {
-      new Notification("The Streetside layer now continues to download images…")
-          .setIcon(StreetsidePlugin.LOGO.get()).show();
-    }
-    stoppedDownload = tooBig;
-    return tooBig;
-  }
-
-  /**
-   * Stops all running threads.
-   */
-  public static void stopAll() {
-    executor.shutdownNow();
-    try {
-      executor.awaitTermination(30, TimeUnit.SECONDS);
-    } catch (InterruptedException e) {
-      LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
-    }
-    executor = new ThreadPoolExecutor(3, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100),
-        new ThreadPoolExecutor.DiscardPolicy());
-  }
-
-  /**
-   * Possible download modes.
-   */
-  public enum DOWNLOAD_MODE {
-    VISIBLE_AREA("visibleArea", I18n.tr("everything in the visible area")),
-
-    OSM_AREA("osmArea", I18n.tr("areas with downloaded OSM-data")),
-
-    MANUAL_ONLY("manualOnly", I18n.tr("only when manually requested"));
-
-    public static final DOWNLOAD_MODE DEFAULT = OSM_AREA;
-
-    private final String prefId;
-    private final String label;
-
-    DOWNLOAD_MODE(String prefId, String label) {
-      this.prefId = prefId;
-      this.label = label;
-    }
-
-    public static DOWNLOAD_MODE fromPrefId(String prefId) {
-      for (DOWNLOAD_MODE mode : DOWNLOAD_MODE.values()) {
-        if (mode.getPrefId().equals(prefId)) {
-          return mode;
-        }
-      }
-      return DEFAULT;
-    }
-
-    public static DOWNLOAD_MODE fromLabel(String label) {
-      for (DOWNLOAD_MODE mode : DOWNLOAD_MODE.values()) {
-        if (mode.getLabel().equals(label)) {
-          return mode;
-        }
-      }
-      return DEFAULT;
-    }
-
-    /**
-     * @return the ID that is used to represent this download mode in the JOSM preferences
-     */
-    public String getPrefId() {
-      return prefId;
-    }
-
-    /**
-     * @return the (internationalized) label describing this download mode
-     */
-    public String getLabel() {
-      return label;
-    }
-  }
+    private static final Logger LOGGER = Logger.getLogger(StreetsideDownloader.class.getCanonicalName());
+    /**
+     * Max area to be downloaded
+     */
+    private static final double MAX_AREA = StreetsideProperties.MAX_DOWNLOAD_AREA.get();
+    /**
+     * Executor that will run the petitions.
+     */
+    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 5, 100, TimeUnit.SECONDS,
+            new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.DiscardPolicy());
+    /**
+     * Indicates whether the last download request has been rejected because it requested an area that was too big.
+     * Iff true, the last download has been rejected, if false, it was executed.
+     */
+    private static boolean stoppedDownload;
+
+    private StreetsideDownloader() {
+        // Private constructor to avoid instantiation
+    }
+
+    /**
+     * Downloads all images of the area covered by the OSM data.
+     */
+    public static void downloadOSMArea() {
+        if (MainApplication.getLayerManager().getEditLayer() == null) {
+            return;
+        }
+        if (isAreaTooBig(MainApplication.getLayerManager().getEditLayer().data.getDataSourceBounds().stream()
+                .map(Bounds::getArea).reduce(0.0, Double::sum))) {
+            return;
+        }
+        MainApplication.getLayerManager().getEditLayer().data.getDataSourceBounds().stream()
+                .filter(bounds -> !StreetsideLayer.getInstance().getData().getBounds().contains(bounds))
+                .forEach(bounds -> {
+                    StreetsideLayer.getInstance().getData().getBounds().add(bounds);
+                    StreetsideDownloader.getImages(bounds.getMin(), bounds.getMax());
+                });
+    }
+
+    /**
+     * Gets all the images in a square. It downloads all the images of all the
+     * sequences that pass through the given rectangle.
+     *
+     * @param minLatLon The minimum latitude and longitude of the rectangle.
+     * @param maxLatLon The maximum latitude and longitude of the rectangle
+     */
+    public static void getImages(LatLon minLatLon, LatLon maxLatLon) {
+        if (minLatLon == null || maxLatLon == null) {
+            throw new IllegalArgumentException();
+        }
+        getImages(new Bounds(minLatLon, maxLatLon));
+    }
+
+    /**
+     * Gets the images within the given bounds.
+     *
+     * @param bounds A {@link Bounds} object containing the area to be downloaded.
+     */
+    public static void getImages(Bounds bounds) {
+        run(new StreetsideSquareDownloadRunnable(bounds));
+    }
+
+    /**
+     * Returns the current download mode.
+     *
+     * @return the currently enabled {@link DOWNLOAD_MODE}
+     */
+    public static DOWNLOAD_MODE getMode() {
+        return DOWNLOAD_MODE.fromPrefId(StreetsideProperties.DOWNLOAD_MODE.get());
+    }
+
+    private static void run(Runnable t) {
+        executor.execute(t);
+    }
+
+    /**
+     * If some part of the current view has not been downloaded, it is downloaded.
+     */
+    public static void downloadVisibleArea() {
+        final var view = MainApplication.getMap().mapView.getRealBounds();
+        if (isAreaTooBig(view.getArea())) {
+            return;
+        }
+        if (isViewDownloaded(view)) {
+            return;
+        }
+        StreetsideLayer.getInstance().getData().getBounds().add(view);
+        getImages(view);
+    }
+
+    private static boolean isViewDownloaded(Bounds view) {
+        final var n = 15;
+        final var inside = new boolean[n][n];
+        for (var i = 0; i < n; i++) {
+            for (var j = 0; j < n; j++) {
+                if (isInBounds(new LatLon(view.getMinLat() + (view.getMaxLat() - view.getMinLat()) * ((double) i / n),
+                        view.getMinLon() + (view.getMaxLon() - view.getMinLon()) * ((double) j / n)))) {
+                    inside[i][j] = true;
+                }
+            }
+        }
+        for (var i = 0; i < n; i++) {
+            for (var j = 0; j < n; j++) {
+                if (!inside[i][j])
+                    return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Checks if the given {@link LatLon} object lies inside the bounds of the
+     * image.
+     *
+     * @param latlon The coordinates to check.
+     * @return true if it lies inside the bounds; false otherwise;
+     */
+    private static boolean isInBounds(LatLon latlon) {
+        return StreetsideLayer.getInstance().getData().getBounds().parallelStream().anyMatch(b -> b.contains(latlon));
+    }
+
+    /**
+     * Checks if the area for which Streetside images should be downloaded is too big. This means that probably
+     * lots of Streetside images are going to be downloaded, slowing down the
+     * program too much. A notification is shown when the download has stopped or continued.
+     *
+     * @param area area to check
+     * @return {@code true} if the area is too big
+     */
+    private static boolean isAreaTooBig(final double area) {
+        final boolean tooBig = area > MAX_AREA;
+        if (!stoppedDownload && tooBig) {
+            new Notification(I18n
+                    .tr("The Streetside layer has stopped downloading images, because the requested area is too big!")
+                    + getDownloadMessage()).setIcon(StreetsidePlugin.LOGO.get()).setDuration(Notification.TIME_LONG)
+                            .show();
+        }
+        if (stoppedDownload && !tooBig) {
+            new Notification("The Streetside layer now continues to download images…")
+                    .setIcon(StreetsidePlugin.LOGO.get()).show();
+        }
+        stoppedDownload = tooBig;
+        return tooBig;
+    }
+
+    private static String getDownloadMessage() {
+        return switch (getMode()) {
+        case VISIBLE_AREA -> "\n"
+                + I18n.tr("To solve this problem, you could zoom in and load a smaller area of the map.");
+        case OSM_AREA -> "\n" + I18n.tr("To solve this problem, you could switch to download mode ''{0}'' and"
+                + "load Streetside images for a smaller portion of the map.");
+        case MANUAL_ONLY -> "";
+        };
+    }
+
+    /**
+     * Stops all running threads.
+     */
+    public static void stopAll() {
+        executor.shutdownNow();
+        try {
+            executor.awaitTermination(30, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
+        }
+        executor = new ThreadPoolExecutor(3, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100),
+                new ThreadPoolExecutor.DiscardPolicy());
+    }
+
+    /**
+     * Possible download modes.
+     */
+    public enum DOWNLOAD_MODE {
+        VISIBLE_AREA("visibleArea", I18n.tr("everything in the visible area")),
+
+        OSM_AREA("osmArea", I18n.tr("areas with downloaded OSM-data")),
+
+        MANUAL_ONLY("manualOnly", I18n.tr("only when manually requested"));
+
+        public static final DOWNLOAD_MODE DEFAULT = OSM_AREA;
+
+        private final String prefId;
+        private final String label;
+
+        DOWNLOAD_MODE(String prefId, String label) {
+            this.prefId = prefId;
+            this.label = label;
+        }
+
+        /**
+         * Convert a preference value to a mode
+         * @param prefId The preference value to convert
+         * @return The download mode, or {@link #DEFAULT}
+         */
+        public static DOWNLOAD_MODE fromPrefId(String prefId) {
+            for (DOWNLOAD_MODE mode : DOWNLOAD_MODE.values()) {
+                if (mode.getPrefId().equals(prefId)) {
+                    return mode;
+                }
+            }
+            return DEFAULT;
+        }
+
+        /**
+         * Convert a label to a mode
+         * @param label The label to convert
+         * @return The download mode, or {@link #DEFAULT}
+         */
+        public static DOWNLOAD_MODE fromLabel(String label) {
+            for (DOWNLOAD_MODE mode : DOWNLOAD_MODE.values()) {
+                if (mode.getLabel().equals(label)) {
+                    return mode;
+                }
+            }
+            return DEFAULT;
+        }
+
+        /**
+         * Get the id for the mode
+         * @return the ID that is used to represent this download mode in the JOSM preferences
+         */
+        public String getPrefId() {
+            return prefId;
+        }
+
+        /**
+         * Get the label for the mode
+         * @return the (internationalized) label describing this download mode
+         */
+        public String getLabel() {
+            return label;
+        }
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/download/StreetsideSquareDownloadRunnable.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/download/StreetsideSquareDownloadRunnable.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/download/StreetsideSquareDownloadRunnable.java	(revision 36228)
@@ -8,36 +8,39 @@
 import org.openstreetmap.josm.plugins.streetside.utils.StreetsideUtils;
 
+/**
+ * Download Streetside images withing a specified bounds
+ */
 public class StreetsideSquareDownloadRunnable implements Runnable {
 
-  private final Bounds bounds;
+    private final Bounds bounds;
 
-  /**
-   * Main constructor.
-   *
-   * @param bounds the bounds of the area that should be downloaded
-   */
-  public StreetsideSquareDownloadRunnable(Bounds bounds) {
-    this.bounds = bounds;
-  }
-
-  @Override
-  public void run() {
-    PluginState.startDownload();
-    StreetsideUtils.updateHelpText();
-
-    // Download basic sequence data synchronously
-    new SequenceDownloadRunnable(StreetsideLayer.getInstance().getData(), bounds).run();
-
-    if (Thread.interrupted()) {
-      return;
+    /**
+     * Main constructor.
+     *
+     * @param bounds the bounds of the area that should be downloaded
+     */
+    public StreetsideSquareDownloadRunnable(Bounds bounds) {
+        this.bounds = bounds;
     }
 
-    // Image detections are not currently supported for Streetside (Mapillary code removed)
+    @Override
+    public void run() {
+        PluginState.startDownload();
+        StreetsideUtils.updateHelpText();
 
-    PluginState.finishDownload();
+        // Download basic sequence data synchronously
+        new SequenceDownloadRunnable(StreetsideLayer.getInstance().getData(), bounds).run();
 
-    StreetsideUtils.updateHelpText();
-    StreetsideLayer.invalidateInstance();
-    StreetsideMainDialog.getInstance().updateImage();
-  }
+        if (Thread.interrupted()) {
+            return;
+        }
+
+        // Image detections are not currently supported for Streetside (Mapillary code removed)
+
+        PluginState.finishDownload();
+
+        StreetsideUtils.updateHelpText();
+        StreetsideLayer.invalidateInstance();
+        StreetsideMainDialog.getInstance().updateImage();
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/export/StreetsideExportDownloadThread.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/export/StreetsideExportDownloadThread.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/export/StreetsideExportDownloadThread.java	(revision 36228)
@@ -16,5 +16,4 @@
 import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
 import org.openstreetmap.josm.plugins.streetside.cache.CacheUtils;
-import org.openstreetmap.josm.plugins.streetside.cache.StreetsideCache;
 import org.openstreetmap.josm.tools.Logging;
 
@@ -29,43 +28,44 @@
 public class StreetsideExportDownloadThread extends Thread implements ICachedLoaderListener {
 
-  private static final Logger LOGGER = Logger.getLogger(StreetsideExportDownloadThread.class.getCanonicalName());
+    private static final Logger LOGGER = Logger.getLogger(StreetsideExportDownloadThread.class.getCanonicalName());
 
-  private final ArrayBlockingQueue<BufferedImage> queue;
-  private final ArrayBlockingQueue<StreetsideAbstractImage> queueImages;
+    private final ArrayBlockingQueue<BufferedImage> queue;
+    private final ArrayBlockingQueue<StreetsideAbstractImage> queueImages;
 
-  private final StreetsideImage image;
+    private final StreetsideImage image;
 
-  /**
-   * Main constructor.
-   *
-   * @param image     Image to be downloaded.
-   * @param queue     Queue of {@link BufferedImage} objects for the
-   *          {@link StreetsideExportWriterThread}.
-   * @param queueImages Queue of {@link StreetsideAbstractImage} objects for the
-   *          {@link StreetsideExportWriterThread}.
-   */
-  public StreetsideExportDownloadThread(StreetsideImage image, ArrayBlockingQueue<BufferedImage> queue,
-      ArrayBlockingQueue<StreetsideAbstractImage> queueImages) {
-    this.queue = queue;
-    this.image = image;
-    this.queueImages = queueImages;
-  }
+    /**
+     * Main constructor.
+     *
+     * @param image     Image to be downloaded.
+     * @param queue     Queue of {@link BufferedImage} objects for the
+     *          {@link StreetsideExportWriterThread}.
+     * @param queueImages Queue of {@link StreetsideAbstractImage} objects for the
+     *          {@link StreetsideExportWriterThread}.
+     */
+    public StreetsideExportDownloadThread(StreetsideImage image, ArrayBlockingQueue<BufferedImage> queue,
+            ArrayBlockingQueue<StreetsideAbstractImage> queueImages) {
+        this.queue = queue;
+        this.image = image;
+        this.queueImages = queueImages;
+    }
 
-  @Override
-  public void run() {
-    // use "thumbnail" type here so that the tiles are not exported
-    CacheUtils.submit(image.getId(), StreetsideCache.Type.THUMBNAIL, this);
-  }
+    @Override
+    public void run() {
+        // use "thumbnail" type here so that the tiles are not exported
+        CacheUtils.downloadPicture(image, CacheUtils.PICTURE.THUMBNAIL, this);
+        CacheUtils.submit(image.id(), this);
+    }
 
-  @Override
-  public synchronized void loadingFinished(CacheEntry data, CacheEntryAttributes attributes, LoadResult result) {
-    try {
-      synchronized (StreetsideExportDownloadThread.class) {
-        queue.put(ImageIO.read(new ByteArrayInputStream(data.getContent())));
-        queueImages.put(image);
-      }
-    } catch (InterruptedException | IOException e) {
-      LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
+    @Override
+    public synchronized void loadingFinished(CacheEntry data, CacheEntryAttributes attributes, LoadResult result) {
+        try {
+            synchronized (StreetsideExportDownloadThread.class) {
+                queue.put(ImageIO.read(new ByteArrayInputStream(data.getContent())));
+                queueImages.put(image);
+            }
+        } catch (InterruptedException | IOException e) {
+            LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
+        }
     }
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/export/StreetsideExportManager.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/export/StreetsideExportManager.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/export/StreetsideExportManager.java	(revision 36228)
@@ -33,76 +33,76 @@
 public class StreetsideExportManager extends PleaseWaitRunnable {
 
-  private static final Logger LOGGER = Logger.getLogger(StreetsideExportManager.class.getCanonicalName());
+    private static final Logger LOGGER = Logger.getLogger(StreetsideExportManager.class.getCanonicalName());
 
-  private final ArrayBlockingQueue<BufferedImage> queue = new ArrayBlockingQueue<>(10);
-  private final ArrayBlockingQueue<StreetsideAbstractImage> queueImages = new ArrayBlockingQueue<>(10);
+    private final ArrayBlockingQueue<BufferedImage> queue = new ArrayBlockingQueue<>(10);
+    private final ArrayBlockingQueue<StreetsideAbstractImage> queueImages = new ArrayBlockingQueue<>(10);
 
-  private final int amount;
-  private final Set<StreetsideAbstractImage> images;
-  private final String path;
+    private final int amount;
+    private final Set<StreetsideAbstractImage> images;
+    private final String path;
 
-  private Thread writer;
-  private ThreadPoolExecutor ex;
+    private Thread writer;
+    private ThreadPoolExecutor ex;
 
-  /**
-   * Main constructor.
-   *
-   * @param images Set of {@link StreetsideAbstractImage} objects to be exported.
-   * @param path   Export path.
-   */
-  public StreetsideExportManager(Set<StreetsideAbstractImage> images, String path) {
-    super(tr("Downloading") + "…", new PleaseWaitProgressMonitor(tr("Exporting Streetside Images")), true);
-    this.images = images == null ? new HashSet<>() : images;
-    this.path = path;
-    amount = this.images.size();
-  }
+    /**
+     * Main constructor.
+     *
+     * @param images Set of {@link StreetsideAbstractImage} objects to be exported.
+     * @param path   Export path.
+     */
+    public StreetsideExportManager(Set<StreetsideAbstractImage> images, String path) {
+        super(tr("Downloading") + "…", new PleaseWaitProgressMonitor(tr("Exporting Streetside Images")), true);
+        this.images = images == null ? new HashSet<>() : images;
+        this.path = path;
+        amount = this.images.size();
+    }
 
-  @Override
-  protected void cancel() {
-    writer.interrupt();
-    ex.shutdown();
-  }
+    @Override
+    protected void cancel() {
+        writer.interrupt();
+        ex.shutdown();
+    }
 
-  @Override
-  protected void realRun() throws IOException {
-    // Starts a writer thread in order to write the pictures on the disk.
-    writer = new StreetsideExportWriterThread(path, queue, queueImages, amount, getProgressMonitor());
-    writer.start();
-    if (path == null) {
-      try {
-        writer.join();
-      } catch (InterruptedException e) {
-        LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
-      }
-      return;
+    @Override
+    protected void realRun() throws IOException {
+        // Starts a writer thread in order to write the pictures on the disk.
+        writer = new StreetsideExportWriterThread(path, queue, queueImages, amount, getProgressMonitor());
+        writer.start();
+        if (path == null) {
+            try {
+                writer.join();
+            } catch (InterruptedException e) {
+                LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
+            }
+            return;
+        }
+        ex = new ThreadPoolExecutor(20, 35, 25, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
+        for (StreetsideAbstractImage image : images) {
+            if (image instanceof StreetsideImage) {
+                try {
+                    ex.execute(new StreetsideExportDownloadThread((StreetsideImage) image, queue, queueImages));
+                } catch (Exception e) {
+                    LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
+                }
+            }
+            try {
+                // If the queue is full, waits for it to have more space
+                // available before executing anything else.
+                while (ex.getQueue().remainingCapacity() == 0) {
+                    Thread.sleep(100);
+                }
+            } catch (Exception e) {
+                LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
+            }
+        }
+        try {
+            writer.join();
+        } catch (InterruptedException e) {
+            LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
+        }
     }
-    ex = new ThreadPoolExecutor(20, 35, 25, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
-    for (StreetsideAbstractImage image : images) {
-      if (image instanceof StreetsideImage) {
-        try {
-          ex.execute(new StreetsideExportDownloadThread((StreetsideImage) image, queue, queueImages));
-        } catch (Exception e) {
-          LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
-        }
-      }
-      try {
-        // If the queue is full, waits for it to have more space
-        // available before executing anything else.
-        while (ex.getQueue().remainingCapacity() == 0) {
-          Thread.sleep(100);
-        }
-      } catch (Exception e) {
-        LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
-      }
+
+    @Override
+    protected void finish() {
     }
-    try {
-      writer.join();
-    } catch (InterruptedException e) {
-      LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
-    }
-  }
-
-  @Override
-  protected void finish() {
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/export/StreetsideExportWriterThread.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/export/StreetsideExportWriterThread.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/io/export/StreetsideExportWriterThread.java	(revision 36228)
@@ -34,83 +34,77 @@
 public class StreetsideExportWriterThread extends Thread {
 
-  private static final Logger LOGGER = Logger.getLogger(StreetsideExportWriterThread.class.getCanonicalName());
+    private static final Logger LOGGER = Logger.getLogger(StreetsideExportWriterThread.class.getCanonicalName());
 
-  private final String path;
-  private final ArrayBlockingQueue<BufferedImage> queue;
-  private final ArrayBlockingQueue<StreetsideAbstractImage> queueImages;
-  private final int amount;
-  private final ProgressMonitor monitor;
+    private final ArrayBlockingQueue<BufferedImage> queue;
+    private final ArrayBlockingQueue<StreetsideAbstractImage> queueImages;
+    private final int amount;
+    private final ProgressMonitor monitor;
 
-  /**
-   * Main constructor.
-   *
-   * @param path    Path to write the pictures.
-   * @param queue     Queue of {@link StreetsideAbstractImage} objects.
-   * @param queueImages Queue of {@link BufferedImage} objects.
-   * @param amount    Amount of images that are going to be exported.
-   * @param monitor   Progress monitor.
-   */
-  public StreetsideExportWriterThread(String path, ArrayBlockingQueue<BufferedImage> queue,
-      ArrayBlockingQueue<StreetsideAbstractImage> queueImages, int amount, ProgressMonitor monitor) {
-    this.path = path;
-    this.queue = queue;
-    this.queueImages = queueImages;
-    this.amount = amount;
-    this.monitor = monitor;
-  }
+    /**
+     * Main constructor.
+     *
+     * @param ignored    Path to write the pictures.
+     * @param queue     Queue of {@link StreetsideAbstractImage} objects.
+     * @param queueImages Queue of {@link BufferedImage} objects.
+     * @param amount    Amount of images that are going to be exported.
+     * @param monitor   Progress monitor.
+     */
+    public StreetsideExportWriterThread(String ignored, ArrayBlockingQueue<BufferedImage> queue,
+            ArrayBlockingQueue<StreetsideAbstractImage> queueImages, int amount, ProgressMonitor monitor) {
+        this.queue = queue;
+        this.queueImages = queueImages;
+        this.amount = amount;
+        this.monitor = monitor;
+    }
 
-  @Override
-  public void run() {
-    monitor.setCustomText("Downloaded 0/" + amount);
-    BufferedImage img;
-    StreetsideAbstractImage mimg;
-    String finalPath = "";
-    for (int i = 0; i < amount; i++) {
-      try {
-        img = queue.take();
-        mimg = queueImages.take();
+    @Override
+    public void run() {
+        monitor.setCustomText("Downloaded 0/" + amount);
+        BufferedImage img;
+        StreetsideAbstractImage mimg;
+        var finalPath = "";
+        for (var i = 0; i < amount; i++) {
+            try {
+                img = queue.take();
+                mimg = queueImages.take();
 
-        // Transforms the image into a byte array.
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        ImageIO.write(img, "jpg", outputStream);
-        byte[] imageBytes = outputStream.toByteArray();
+                // Transforms the image into a byte array.
+                final var outputStream = new ByteArrayOutputStream();
+                ImageIO.write(img, "jpg", outputStream);
+                byte[] imageBytes = outputStream.toByteArray();
 
-        // Write EXIF tags
-        TiffOutputSet outputSet = null;
-        TiffOutputDirectory exifDirectory;
-        TiffOutputDirectory gpsDirectory;
-        // If the image is imported, loads the rest of the EXIF data.
+                // Write EXIF tags
+                final var outputSet = new TiffOutputSet();
+                TiffOutputDirectory exifDirectory;
+                TiffOutputDirectory gpsDirectory;
 
-        if (null == outputSet) {
-          outputSet = new TiffOutputSet();
+                exifDirectory = outputSet.getOrCreateExifDirectory();
+                gpsDirectory = outputSet.getOrCreateGPSDirectory();
+
+                gpsDirectory.removeField(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION_REF);
+                gpsDirectory.add(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION_REF,
+                        GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION_REF_VALUE_TRUE_NORTH);
+
+                gpsDirectory.removeField(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION);
+                gpsDirectory.add(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION, RationalNumber.valueOf(mimg.heading()));
+
+                exifDirectory.removeField(ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL);
+
+                outputSet.setGPSInDegrees(mimg.lon(), mimg.lat());
+                try (OutputStream os = new BufferedOutputStream(new FileOutputStream(finalPath + ".jpg"))) {
+                    new ExifRewriter().updateExifMetadataLossless(imageBytes, os, outputSet);
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                LOGGER.info("Streetside export cancelled");
+                return;
+            } catch (IOException | ImageReadException | ImageWriteException e) {
+                LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
+            }
+
+            // Increases the progress bar.
+            monitor.worked(PleaseWaitProgressMonitor.PROGRESS_BAR_MAX / amount);
+            monitor.setCustomText("Downloaded " + (i + 1) + "/" + amount);
         }
-        exifDirectory = outputSet.getOrCreateExifDirectory();
-        gpsDirectory = outputSet.getOrCreateGPSDirectory();
-
-        gpsDirectory.removeField(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION_REF);
-        gpsDirectory.add(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION_REF,
-            GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION_REF_VALUE_TRUE_NORTH);
-
-        gpsDirectory.removeField(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION);
-        gpsDirectory.add(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION, RationalNumber.valueOf(mimg.getMovingHe()));
-
-        exifDirectory.removeField(ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL);
-
-        outputSet.setGPSInDegrees(mimg.getMovingLatLon().lon(), mimg.getMovingLatLon().lat());
-        OutputStream os = new BufferedOutputStream(new FileOutputStream(finalPath + ".jpg"));
-        new ExifRewriter().updateExifMetadataLossless(imageBytes, os, outputSet);
-
-        os.close();
-      } catch (InterruptedException e) {
-        LOGGER.info("Streetside export cancelled");
-        return;
-      } catch (IOException | ImageReadException | ImageWriteException e) {
-        LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e);
-      }
-
-      // Increases the progress bar.
-      monitor.worked(PleaseWaitProgressMonitor.PROGRESS_BAR_MAX / amount);
-      monitor.setCustomText("Downloaded " + (i + 1) + "/" + amount);
     }
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/mode/AbstractMode.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/mode/AbstractMode.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/mode/AbstractMode.java	(revision 36228)
@@ -3,14 +3,11 @@
 
 import java.awt.Cursor;
-import java.awt.Graphics2D;
 import java.awt.Point;
 import java.awt.event.MouseAdapter;
 import java.util.Calendar;
 
-import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener;
-import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
+import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
 import org.openstreetmap.josm.plugins.streetside.StreetsideLayer;
 import org.openstreetmap.josm.plugins.streetside.io.download.StreetsideDownloader;
@@ -24,83 +21,74 @@
 public abstract class AbstractMode extends MouseAdapter implements ZoomChangeListener {
 
-  private static final int DOWNLOAD_COOLDOWN = 2000;
-  private static SemiautomaticThread semiautomaticThread = new SemiautomaticThread();
-
-  /**
-   * Cursor that should become active when this mode is activated.
-   */
-  public int cursor = Cursor.DEFAULT_CURSOR;
-
-  /**
-   * Resets the semiautomatic mode thread.
-   */
-  public static void resetThread() {
-    semiautomaticThread.interrupt();
-    semiautomaticThread = new SemiautomaticThread();
-  }
-
-  protected StreetsideAbstractImage getClosest(Point clickPoint) {
-    double snapDistance = 10;
-    double minDistance = Double.MAX_VALUE;
-    StreetsideAbstractImage closest = null;
-    for (StreetsideAbstractImage image : StreetsideLayer.getInstance().getData().getImages()) {
-      Point imagePoint = MainApplication.getMap().mapView.getPoint(image.getMovingLatLon());
-      imagePoint.setLocation(imagePoint.getX(), imagePoint.getY());
-      double dist = clickPoint.distanceSq(imagePoint);
-      if (minDistance > dist && clickPoint.distance(imagePoint) < snapDistance && image.isVisible()) {
-        minDistance = dist;
-        closest = image;
-      }
-    }
-    return closest;
-  }
-
-  /**
-   * Paint the dataset using the engine set.
-   *
-   * @param g   {@link Graphics2D} used for painting
-   * @param mv  The object that can translate GeoPoints to screen coordinates.
-   * @param box Area where painting is going to be performed
-   */
-  public abstract void paint(Graphics2D g, MapView mv, Bounds box);
-
-  @Override
-  public void zoomChanged() {
-    if (StreetsideDownloader.getMode() == StreetsideDownloader.DOWNLOAD_MODE.VISIBLE_AREA) {
-      if (!semiautomaticThread.isAlive())
-        semiautomaticThread.start();
-      semiautomaticThread.moved();
-    }
-  }
-
-  private static class SemiautomaticThread extends Thread {
+    private static final int DOWNLOAD_COOLDOWN = 2000;
+    private static SemiautomaticThread semiautomaticThread = new SemiautomaticThread();
 
     /**
-     * If in semiautomatic mode, the last Epoch time when there was a download
+     * Cursor that should become active when this mode is activated.
      */
-    private long lastDownload;
+    public final int cursor = Cursor.DEFAULT_CURSOR;
 
-    private boolean moved;
+    /**
+     * Resets the semiautomatic mode thread.
+     */
+    public static void resetThread() {
+        semiautomaticThread.interrupt();
+        semiautomaticThread = new SemiautomaticThread();
+    }
+
+    protected StreetsideImage getClosest(Point clickPoint) {
+        double snapDistance = 10;
+        double minDistance = Double.MAX_VALUE;
+        StreetsideImage closest = null;
+        for (StreetsideImage image : StreetsideLayer.getInstance().getData().getImages()) {
+            Point imagePoint = MainApplication.getMap().mapView.getPoint(image);
+            imagePoint.setLocation(imagePoint.getX(), imagePoint.getY());
+            double dist = clickPoint.distanceSq(imagePoint);
+            if (minDistance > dist && clickPoint.distance(imagePoint) < snapDistance && image.visible()) {
+                minDistance = dist;
+                closest = image;
+            }
+        }
+        return closest;
+    }
 
     @Override
-    public void run() {
-      while (true) {
-        if (this.moved && Calendar.getInstance().getTimeInMillis() - this.lastDownload >= DOWNLOAD_COOLDOWN) {
-          this.lastDownload = Calendar.getInstance().getTimeInMillis();
-          StreetsideDownloader.downloadVisibleArea();
-          this.moved = false;
-          StreetsideLayer.invalidateInstance();
+    public void zoomChanged() {
+        if (StreetsideDownloader.getMode() == StreetsideDownloader.DOWNLOAD_MODE.VISIBLE_AREA) {
+            if (!semiautomaticThread.isAlive())
+                semiautomaticThread.start();
+            semiautomaticThread.moved();
         }
-        try {
-          Thread.sleep(100);
-        } catch (InterruptedException e) {
-          return;
-        }
-      }
     }
 
-    public void moved() {
-      this.moved = true;
+    private static class SemiautomaticThread extends Thread {
+
+        /**
+         * If in semiautomatic mode, the last Epoch time when there was a download
+         */
+        private long lastDownload;
+
+        private boolean moved;
+
+        @Override
+        public void run() {
+            while (true) {
+                if (this.moved && Calendar.getInstance().getTimeInMillis() - this.lastDownload >= DOWNLOAD_COOLDOWN) {
+                    this.lastDownload = Calendar.getInstance().getTimeInMillis();
+                    StreetsideDownloader.downloadVisibleArea();
+                    this.moved = false;
+                    StreetsideLayer.invalidateInstance();
+                }
+                try {
+                    Thread.sleep(100);
+                } catch (InterruptedException e) {
+                    return;
+                }
+            }
+        }
+
+        public void moved() {
+            this.moved = true;
+        }
     }
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/mode/SelectMode.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/mode/SelectMode.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/mode/SelectMode.java	(revision 36228)
@@ -4,27 +4,12 @@
 import static org.openstreetmap.josm.tools.I18n.tr;
 
-import java.awt.Graphics2D;
-import java.awt.Point;
-import java.awt.event.InputEvent;
 import java.awt.event.MouseEvent;
-import java.util.Objects;
-import java.util.concurrent.ConcurrentSkipListSet;
 
-import javax.swing.SwingUtilities;
-
-import org.openstreetmap.josm.data.Bounds;
-import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
-import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
-import org.openstreetmap.josm.plugins.streetside.StreetsideData;
 import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
 import org.openstreetmap.josm.plugins.streetside.StreetsideLayer;
 import org.openstreetmap.josm.plugins.streetside.gui.StreetsideMainDialog;
-import org.openstreetmap.josm.plugins.streetside.history.StreetsideRecord;
-import org.openstreetmap.josm.plugins.streetside.history.commands.CommandMove;
-import org.openstreetmap.josm.plugins.streetside.history.commands.CommandTurn;
 import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
 
@@ -35,165 +20,70 @@
  */
 public class SelectMode extends AbstractMode {
-  private final StreetsideRecord record;
-  private StreetsideAbstractImage closest;
-  private StreetsideAbstractImage lastClicked;
-  private boolean nothingHighlighted;
-  private boolean imageHighlighted;
+    private boolean nothingHighlighted;
+    private boolean imageHighlighted;
 
-  /**
-   * Main constructor.
-   */
-  public SelectMode() {
-    record = StreetsideRecord.getInstance();
-  }
-
-  @Override
-  public void mousePressed(MouseEvent e) {
-    if (e.getButton() != MouseEvent.BUTTON1) {
-      return;
-    }
-    StreetsideAbstractImage closest = getClosest(e.getPoint());
-    if (!(MainApplication.getLayerManager().getActiveLayer() instanceof StreetsideLayer) && closest != null
-        && MainApplication.getMap().mapMode == MainApplication.getMap().mapModeSelect) {
-      lastClicked = this.closest;
-      StreetsideLayer.getInstance().getData().setSelectedImage(closest);
-      return;
-    } else if (MainApplication.getLayerManager().getActiveLayer() != StreetsideLayer.getInstance()) {
-      return;
-    }
-    // Double click
-    if (e.getClickCount() == 2 && StreetsideLayer.getInstance().getData().getSelectedImage() != null
-        && closest != null) {
-      closest.getSequence().getImages().forEach(StreetsideLayer.getInstance().getData()::addMultiSelectedImage);
-    }
-    lastClicked = this.closest;
-    this.closest = closest;
-    if (closest != null && StreetsideLayer.getInstance().getData().getMultiSelectedImages().contains(closest)) {
-      return;
-    }
-    // ctrl+click
-    if (e.getModifiers() == (InputEvent.BUTTON1_MASK | InputEvent.CTRL_MASK) && closest != null) {
-      StreetsideLayer.getInstance().getData().addMultiSelectedImage(closest);
-      // shift + click
-    } else if (e.getModifiers() == (InputEvent.BUTTON1_MASK | InputEvent.SHIFT_MASK)
-        && lastClicked instanceof StreetsideImage) {
-      if (this.closest != null && this.closest.getSequence() == lastClicked.getSequence()) {
-        int i = this.closest.getSequence().getImages().indexOf(this.closest);
-        int j = lastClicked.getSequence().getImages().indexOf(lastClicked);
-        StreetsideLayer.getInstance().getData().addMultiSelectedImage(i < j
-            ? new ConcurrentSkipListSet<>(this.closest.getSequence().getImages().subList(i, j + 1))
-            : new ConcurrentSkipListSet<>(this.closest.getSequence().getImages().subList(j, i + 1)));
-      }
-      // click
-    } else {
-      StreetsideLayer.getInstance().getData().setSelectedImage(closest);
-    }
-  }
-
-  @Override
-  public void mouseDragged(MouseEvent e) {
-    StreetsideAbstractImage highlightImg = StreetsideLayer.getInstance().getData().getHighlightedImage();
-    if (MainApplication.getLayerManager().getActiveLayer() == StreetsideLayer.getInstance()
-        && SwingUtilities.isLeftMouseButton(e) && highlightImg != null && highlightImg.getLatLon() != null) {
-      Point highlightImgPoint = MainApplication.getMap().mapView.getPoint(highlightImg.getTempLatLon());
-      if (e.isShiftDown()) { // turn
-        StreetsideLayer.getInstance().getData().getMultiSelectedImages().parallelStream()
-            .filter(img -> !(img instanceof StreetsideImage) || StreetsideProperties.DEVELOPER.get())
-            .forEach(img -> img.turn(Math.toDegrees(
-                Math.atan2(e.getX() - highlightImgPoint.getX(), -e.getY() + highlightImgPoint.getY()))
-                - highlightImg.getTempHe()));
-      } else { // move
-        LatLon eventLatLon = MainApplication.getMap().mapView.getLatLon(e.getX(), e.getY());
-        LatLon imgLatLon = MainApplication.getMap().mapView.getLatLon(highlightImgPoint.getX(),
-            highlightImgPoint.getY());
-        StreetsideLayer.getInstance().getData().getMultiSelectedImages().parallelStream()
-            .filter(img -> !(img instanceof StreetsideImage) || StreetsideProperties.DEVELOPER.get())
-            .forEach(img -> img.move(eventLatLon.getX() - imgLatLon.getX(),
-                eventLatLon.getY() - imgLatLon.getY()));
-      }
-      StreetsideLayer.invalidateInstance();
-    }
-  }
-
-  @Override
-  public void mouseReleased(MouseEvent e) {
-    final StreetsideData data = StreetsideLayer.getInstance().getData();
-    if (data.getSelectedImage() == null) {
-      return;
-    }
-    if (!Objects.equals(data.getSelectedImage().getTempHe(), data.getSelectedImage().getMovingHe())) {
-      double from = data.getSelectedImage().getTempHe();
-      double to = data.getSelectedImage().getMovingHe();
-      record.addCommand(new CommandTurn(data.getMultiSelectedImages(), to - from));
-    } else if (!Objects.equals(data.getSelectedImage().getTempLatLon(),
-        data.getSelectedImage().getMovingLatLon())) {
-      LatLon from = data.getSelectedImage().getTempLatLon();
-      LatLon to = data.getSelectedImage().getMovingLatLon();
-      record.addCommand(
-          new CommandMove(data.getMultiSelectedImages(), to.getX() - from.getX(), to.getY() - from.getY()));
-    }
-    data.getMultiSelectedImages().parallelStream().filter(Objects::nonNull)
-        .forEach(StreetsideAbstractImage::stopMoving);
-    StreetsideLayer.invalidateInstance();
-  }
-
-  /**
-   * Checks if the mouse is over pictures.
-   */
-  @Override
-  public void mouseMoved(MouseEvent e) {
-    if (MainApplication.getLayerManager().getActiveLayer() instanceof OsmDataLayer
-        && MainApplication.getMap().mapMode != MainApplication.getMap().mapModeSelect) {
-      return;
-    }
-    if (Boolean.FALSE.equals(StreetsideProperties.HOVER_ENABLED.get())) {
-      return;
+    @Override
+    public void mousePressed(MouseEvent e) {
+        if (e.getButton() != MouseEvent.BUTTON1) {
+            return;
+        }
+        // click
+        StreetsideLayer.getInstance().getData().setSelectedImage(getClosest(e.getPoint()));
     }
 
-    StreetsideAbstractImage closestTemp = getClosest(e.getPoint());
+    /**
+     * Checks if the mouse is over pictures.
+     */
+    @Override
+    public void mouseMoved(MouseEvent e) {
+        if (MainApplication.getLayerManager().getActiveLayer() instanceof OsmDataLayer
+                && MainApplication.getMap().mapMode != MainApplication.getMap().mapModeSelect) {
+            return;
+        }
+        if (Boolean.FALSE.equals(StreetsideProperties.HOVER_ENABLED.get())) {
+            return;
+        }
 
-    final OsmDataLayer editLayer = MainApplication.getLayerManager().getEditLayer();
-    if (editLayer != null) {
-      if (closestTemp != null && !imageHighlighted) {
-        if (MainApplication.getMap().mapMode != null) {
-          MainApplication.getMap().mapMode.putValue("active", Boolean.FALSE);
+        StreetsideImage closestTemp = getClosest(e.getPoint());
+
+        final OsmDataLayer editLayer = MainApplication.getLayerManager().getEditLayer();
+        if (editLayer != null) {
+            if (closestTemp != null && !imageHighlighted) {
+                if (MainApplication.getMap().mapMode != null) {
+                    MainApplication.getMap().mapMode.putValue("active", Boolean.FALSE);
+                }
+                imageHighlighted = true;
+            } else if (closestTemp == null && imageHighlighted && nothingHighlighted) {
+                if (MainApplication.getMap().mapMode != null) {
+                    MainApplication.getMap().mapMode.putValue("active", Boolean.TRUE);
+                }
+                nothingHighlighted = false;
+            } else if (imageHighlighted && !nothingHighlighted && editLayer.data != null) {
+                for (OsmPrimitive primivitive : MainApplication.getLayerManager().getEditLayer().data.allPrimitives()) {
+                    primivitive.setHighlighted(false);
+                }
+                imageHighlighted = false;
+                nothingHighlighted = true;
+            }
         }
-        imageHighlighted = true;
-      } else if (closestTemp == null && imageHighlighted && nothingHighlighted) {
-        if (MainApplication.getMap().mapMode != null) {
-          MainApplication.getMap().mapMode.putValue("active", Boolean.TRUE);
+
+        if (StreetsideLayer.getInstance().getData().getHighlightedImage() != closestTemp && closestTemp != null) {
+            StreetsideLayer.getInstance().getData().setHighlightedImage(closestTemp);
+            StreetsideMainDialog.getInstance().setImage(closestTemp);
+            StreetsideMainDialog.getInstance().updateImage(false);
+
+        } else if (StreetsideLayer.getInstance().getData().getHighlightedImage() != closestTemp
+                && closestTemp == null) {
+            StreetsideLayer.getInstance().getData().setHighlightedImage(null);
+            StreetsideMainDialog.getInstance().setImage(StreetsideLayer.getInstance().getData().getSelectedImage());
+            StreetsideMainDialog.getInstance().updateImage();
         }
-        nothingHighlighted = false;
-      } else if (imageHighlighted && !nothingHighlighted && editLayer.data != null) {
-        for (OsmPrimitive primivitive : MainApplication.getLayerManager().getEditLayer().data.allPrimitives()) {
-          primivitive.setHighlighted(false);
-        }
-        imageHighlighted = false;
-        nothingHighlighted = true;
-      }
+
+        StreetsideLayer.invalidateInstance();
     }
 
-    if (StreetsideLayer.getInstance().getData().getHighlightedImage() != closestTemp && closestTemp != null) {
-      StreetsideLayer.getInstance().getData().setHighlightedImage(closestTemp);
-      StreetsideMainDialog.getInstance().setImage(closestTemp);
-      StreetsideMainDialog.getInstance().updateImage(false);
-
-    } else if (StreetsideLayer.getInstance().getData().getHighlightedImage() != closestTemp
-        && closestTemp == null) {
-      StreetsideLayer.getInstance().getData().setHighlightedImage(null);
-      StreetsideMainDialog.getInstance().setImage(StreetsideLayer.getInstance().getData().getSelectedImage());
-      StreetsideMainDialog.getInstance().updateImage();
+    @Override
+    public String toString() {
+        return tr("Select mode");
     }
-
-    StreetsideLayer.invalidateInstance();
-  }
-
-  @Override
-  public void paint(Graphics2D g, MapView mv, Bounds box) {
-  }
-
-  @Override
-  public String toString() {
-    return tr("Select mode");
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/mode/package-info.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/mode/package-info.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/mode/package-info.java	(revision 36228)
@@ -3,5 +3,5 @@
  * The different modes that the {@link org.openstreetmap.josm.plugins.streetside.StreetsideLayer} can be in.
  * <br>
- * Currently there are two of them:
+ * Currently, there are two of them:
  * <ul>
  *  <li><strong>{@link org.openstreetmap.josm.plugins.streetside.mode.SelectMode SelectMode}</strong> for selecting pictures in the layer</li>
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/GraphicsUtils.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/GraphicsUtils.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/GraphicsUtils.java	(revision 36228)
@@ -5,142 +5,156 @@
 import java.awt.image.AffineTransformOp;
 import java.awt.image.BufferedImage;
-import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
 import java.util.logging.Logger;
 
+import org.openstreetmap.josm.plugins.streetside.CubeMapTileXY;
 import org.openstreetmap.josm.tools.Logging;
 
 import javafx.application.Platform;
-import javafx.scene.image.PixelWriter;
-import javafx.scene.image.WritableImage;
 
-public class GraphicsUtils {
+/**
+ * Various graphic utilities, mostly for images.
+ */
+public final class GraphicsUtils {
 
-  private static final Logger LOGGER = Logger.getLogger(GraphicsUtils.class.getCanonicalName());
+    private static final Logger LOGGER = Logger.getLogger(GraphicsUtils.class.getCanonicalName());
 
-  private GraphicsUtils() {
-    // Private constructor to avoid instantiation
-  }
-
-  public static javafx.scene.image.Image convertBufferedImage2JavaFXImage(BufferedImage bf) {
-    WritableImage res = null;
-    if (bf != null) {
-      res = new WritableImage(bf.getWidth(), bf.getHeight());
-      PixelWriter pw = res.getPixelWriter();
-      for (int x = 0; x < bf.getWidth(); x++) {
-        for (int y = 0; y < bf.getHeight(); y++) {
-          pw.setArgb(x, y, bf.getRGB(x, y));
-        }
-      }
-    }
-    return res;
-  }
-
-  public static BufferedImage buildMultiTiledCubemapFaceImage(final BufferedImage[] tiles) {
-
-    long start = System.currentTimeMillis();
-
-    BufferedImage res = null;
-
-    int pixelBuffer = Boolean.TRUE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get()) ? 2 : 1;
-
-    BufferedImage[] croppedTiles = cropMultiTiledImages(tiles, pixelBuffer);
-
-    // we assume the no. of rows and cols are known and each chunk has equal width and height
-    int rows = Boolean.TRUE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get()) ? 4 : 2;
-    int cols = Boolean.TRUE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get()) ? 4 : 2;
-
-    int chunkWidth;
-    int chunkHeight;
-
-    chunkWidth = croppedTiles[0].getWidth();
-    chunkHeight = croppedTiles[0].getHeight();
-
-    //Initializing the final image
-    BufferedImage img = new BufferedImage(chunkWidth * cols, chunkHeight * rows, BufferedImage.TYPE_INT_ARGB);
-
-    int num = 0;
-    for (int i = 0; i < rows; i++) {
-      for (int j = 0; j < cols; j++) {
-        // TODO: unintended mirror image created with draw call - requires
-        // extra reversal step - fix!
-        img.createGraphics().drawImage(croppedTiles[num], chunkWidth * j, (chunkHeight * i), null);
-
-        int width = Boolean.TRUE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get()) ? 1014
-            : 510;
-        int height = width;
-
-        // BufferedImage for mirror image
-        res = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
-
-        // Create mirror image pixel by pixel
-        for (int y = 0; y < height; y++) {
-          for (int lx = 0, rx = width - 1; lx < width; lx++, rx--) {
-            // lx starts from the left side of the image
-            // rx starts from the right side of the image
-            // lx is used since we are getting pixel from left side
-            // rx is used to set from right side
-            // get source pixel value
-            int p = img.getRGB(lx, y);
-
-            // set mirror image pixel value
-            res.setRGB(rx, y, p);
-          }
-        }
-        num++;
-      }
+    private GraphicsUtils() {
+        // Private constructor to avoid instantiation
     }
 
-    if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-      LOGGER.log(Logging.LEVEL_DEBUG,
-          MessageFormat.format("Image concatenated in {0} millisecs.", (System.currentTimeMillis() - start)));
-    }
-    return res;
-  }
-
-  public static BufferedImage rotateImage(BufferedImage bufImg) {
-    AffineTransform tx = AffineTransform.getScaleInstance(-1, -1);
-    tx.translate(-bufImg.getWidth(null), -bufImg.getHeight(null));
-    AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
-    bufImg = op.filter(bufImg, null);
-    return bufImg;
-  }
-
-  private static BufferedImage[] cropMultiTiledImages(BufferedImage[] tiles, int pixelBuffer) {
-
-    long start = System.currentTimeMillis();
-
-    BufferedImage[] res = new BufferedImage[tiles.length];
-
-    for (int i = 0; i < tiles.length; i++) {
-      if (Boolean.TRUE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get())) {
-        res[i] = tiles[i].getSubimage(pixelBuffer, pixelBuffer, 256 - pixelBuffer, 256 - pixelBuffer);
-      } else {
-        res[i] = tiles[i].getSubimage(pixelBuffer, pixelBuffer, 256 - pixelBuffer, 256 - pixelBuffer);
-      }
+    /**
+     * Build the face given the tiles
+     * @param tiles The tile images
+     * @param zoom The zoom level
+     * @return The rendered image
+     */
+    public static BufferedImage buildMultiTiledCubemapFaceImage(Map<CubeMapTileXY, BufferedImage> tiles, int zoom) {
+        var faceTileImages = new BufferedImage[tiles.size()];
+        for (var entry : tiles.entrySet()) {
+            final var index = cubeMapTileToIndex(entry.getKey(), zoom);
+            if (index >= faceTileImages.length) { // Due to some null tiles during loading... TODO: How is this happening?
+                faceTileImages = Arrays.copyOf(faceTileImages, index + 1);
+            }
+            faceTileImages[index] = entry.getValue();
+        }
+        return buildMultiTiledCubemapFaceImage(faceTileImages);
     }
 
-    if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-      LOGGER.log(Logging.LEVEL_DEBUG,
-          MessageFormat.format("Images cropped in {0} millisecs.", (System.currentTimeMillis() - start)));
+    /**
+     * Convert a tile to an index for painting
+     * @param tile The tile to convert
+     * @param zoom The zoom
+     * @return The index for the array
+     */
+    static int cubeMapTileToIndex(CubeMapTileXY tile, int zoom) {
+        return (1 << zoom) * tile.y() + tile.x();
     }
 
-    return res;
-  }
+    /**
+     * Build an image given a list of tiles
+     * @param tiles The tiles to use (note: there should be a factor of 4 images, e.g. 1, 4, 16, 64, ...)
+     * @return The rendered image
+     */
+    public static BufferedImage buildMultiTiledCubemapFaceImage(final BufferedImage[] tiles) {
 
-  public static class PlatformHelper {
+        long start = System.currentTimeMillis();
 
-    private PlatformHelper() {
-      // Private constructor to avoid instantiation
+        final int zoom = Math.toIntExact(Math.round(Math.log(tiles.length) / Math.log(4)));
+        int pixelBuffer = zoom >= 1 ? 2 : 1;
+
+        BufferedImage[] croppedTiles = cropMultiTiledImages(tiles, pixelBuffer);
+
+        // we assume the no. of rows and cols are known and each chunk has equal width and height
+        final int rows = Math.toIntExact(Math.round(Math.pow(2, zoom)));
+        final int cols = rows;
+
+        int chunkWidth;
+        int chunkHeight;
+
+        chunkWidth = Arrays.stream(croppedTiles).filter(Objects::nonNull).findFirst().orElseThrow().getWidth();
+        chunkHeight = Arrays.stream(croppedTiles).filter(Objects::nonNull).findFirst().orElseThrow().getHeight();
+
+        //Initializing the final image
+        final var img = new BufferedImage(chunkWidth * cols, chunkHeight * rows, BufferedImage.TYPE_INT_ARGB);
+
+        int num = 0;
+        final var g2d = img.createGraphics();
+        for (var i = 0; i < rows; i++) {
+            for (var j = 0; j < cols; j++) {
+                final var tile = croppedTiles[num++];
+                if (tile != null) {
+                    // TODO: unintended mirror image created with draw call - requires
+                    // extra reversal step - fix!
+                    g2d.drawImage(tile, chunkWidth * j, (chunkHeight * i), null);
+                }
+            }
+        }
+        g2d.dispose();
+
+        if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
+            LOGGER.log(Logging.LEVEL_DEBUG, "Image concatenated in {0} millisecs.",
+                    (System.currentTimeMillis() - start));
+        }
+        return img;
     }
 
-    public static void run(Runnable treatment) {
-      if (treatment == null)
-        throw new IllegalArgumentException("The treatment to perform can not be null");
+    /**
+     * Rotate an image by 180 degrees
+     * @param bufImg The image to rotate
+     * @return The rotated image
+     */
+    public static BufferedImage rotateImage(BufferedImage bufImg) {
+        // FIXME: Does AffineTransform.getRotateInstance(Math.PI) work? (docs indicate that this is optimized)
+        final var tx = AffineTransform.getScaleInstance(-1, -1);
+        tx.translate(-bufImg.getWidth(null), -bufImg.getHeight(null));
+        final var op = new AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
+        bufImg = op.filter(bufImg, null);
+        return bufImg;
+    }
 
-      if (Platform.isFxApplicationThread())
-        treatment.run();
-      else
-        Platform.runLater(treatment);
+    private static BufferedImage[] cropMultiTiledImages(BufferedImage[] tiles, int pixelBuffer) {
+
+        final long start = System.currentTimeMillis();
+
+        final var res = new BufferedImage[tiles.length];
+
+        for (var i = 0; i < tiles.length; i++) {
+            if (tiles[i] != null) {
+                res[i] = tiles[i].getSubimage(pixelBuffer, pixelBuffer, 256 - pixelBuffer, 256 - pixelBuffer);
+            }
+        }
+
+        if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
+            LOGGER.log(Logging.LEVEL_DEBUG, "Images cropped in {0} millisecs.", (System.currentTimeMillis() - start));
+        }
+
+        return res;
     }
-  }
+
+    /**
+     * Utilities for running in the JavaFX platform thread
+     */
+    public static final class PlatformHelper {
+
+        private PlatformHelper() {
+            // Private constructor to avoid instantiation
+        }
+
+        /**
+         * Run a job in the JavaFX UI thread
+         * @param treatment The runnable to run
+         */
+        public static void run(Runnable treatment) {
+            if (treatment == null)
+                throw new IllegalArgumentException("The treatment to perform can not be null");
+
+            if (Platform.isFxApplicationThread())
+                treatment.run();
+            else
+                Platform.runLater(treatment);
+        }
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/ImageUtil.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/ImageUtil.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/ImageUtil.java	(revision 36228)
@@ -7,37 +7,39 @@
 import javax.swing.ImageIcon;
 
-import org.openstreetmap.josm.tools.I18n;
 import org.openstreetmap.josm.tools.Logging;
 
+/**
+ * Various utility methods for images
+ */
 public final class ImageUtil {
 
-  private static final Logger LOGGER = Logger.getLogger(ImageUtil.class.getCanonicalName());
+    private static final Logger LOGGER = Logger.getLogger(ImageUtil.class.getCanonicalName());
 
-  private ImageUtil() {
-    // Private constructor to avoid instantiation
-  }
+    private ImageUtil() {
+        // Private constructor to avoid instantiation
+    }
 
-  /**
-   * Scales an {@link ImageIcon} to the desired size
-   *
-   * @param icon the icon, which should be resized
-   * @param size the desired length of the longest edge of the icon
-   * @return the resized {@link ImageIcon}. It is the same object that you put in,
-   * only the contained {@link Image} is exchanged.
-   */
-  public static ImageIcon scaleImageIcon(final ImageIcon icon, int size) {
-    if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-      LOGGER.log(Logging.LEVEL_DEBUG, I18n.tr("Scale icon {0} → {1}", icon.getIconWidth(), size));
+    /**
+     * Scales an {@link ImageIcon} to the desired size
+     *
+     * @param icon the icon, which should be resized
+     * @param size the desired length of the longest edge of the icon
+     * @return the resized {@link ImageIcon}. It is the same object that you put in,
+     * only the contained {@link Image} is exchanged.
+     */
+    public static ImageIcon scaleImageIcon(final ImageIcon icon, int size) {
+        if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
+            LOGGER.log(Logging.LEVEL_DEBUG, "Scale icon {0} → {1}", new Object[] { icon.getIconWidth(), size });
+        }
+        return new ImageIcon(
+                icon.getImage()
+                        .getScaledInstance(
+                                icon.getIconWidth() >= icon.getIconHeight() ? size
+                                        : Math.max(1,
+                                                Math.round(icon.getIconWidth() / (float) icon.getIconHeight() * size)),
+                                icon.getIconHeight() >= icon.getIconWidth() ? size
+                                        : Math.max(1,
+                                                Math.round(icon.getIconHeight() / (float) icon.getIconWidth() * size)),
+                                Image.SCALE_SMOOTH));
     }
-    return new ImageIcon(
-        icon.getImage()
-            .getScaledInstance(
-                icon.getIconWidth() >= icon.getIconHeight() ? size
-                    : Math.max(1,
-                        Math.round(icon.getIconWidth() / (float) icon.getIconHeight() * size)),
-                icon.getIconHeight() >= icon.getIconWidth() ? size
-                    : Math.max(1,
-                        Math.round(icon.getIconHeight() / (float) icon.getIconWidth() * size)),
-                Image.SCALE_SMOOTH));
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/MapViewGeometryUtil.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/MapViewGeometryUtil.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/MapViewGeometryUtil.java	(revision 36228)
@@ -7,65 +7,41 @@
 import java.awt.Shape;
 import java.awt.geom.Area;
-import java.awt.geom.Path2D;
 
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.gui.MapView;
-import org.openstreetmap.josm.gui.NavigatableComponent;
-import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
-import org.openstreetmap.josm.plugins.streetside.StreetsideSequence;
 
 /**
- * Utility class to convert entities like {@link Bounds} and {@link StreetsideSequence} into {@link Shape}s that
+ * Utility class to convert entities like {@link Bounds}  into {@link Shape}s that
  * can then easily be drawn on a {@link MapView}s {@link Graphics2D}-context.
  */
 public final class MapViewGeometryUtil {
-  private MapViewGeometryUtil() {
-    // Private constructor to avoid instantiation
-  }
+    private MapViewGeometryUtil() {
+        // Private constructor to avoid instantiation
+    }
 
-  /**
-   * Subtracts the download bounds from the rectangular bounds of the map view.
-   *
-   * @param mv       the MapView that is used for the LatLon-to-Point-conversion and that determines
-   *             the Bounds from which the downloaded Bounds are subtracted
-   * @param downloadBounds multiple {@link Bounds} objects that represent the downloaded area
-   * @return the difference between the {@link MapView}s bounds and the downloaded area
-   */
-  public static Area getNonDownloadedArea(MapView mv, Iterable<Bounds> downloadBounds) {
-    Rectangle b = mv.getBounds();
-    // on some platforms viewport bounds seem to be offset from the left,
-    // over-grow it just to be sure
-    b.grow(100, 100);
-    Area a = new Area(b);
-    // now successively subtract downloaded areas
-    for (Bounds bounds : downloadBounds) {
-      Point p1 = mv.getPoint(bounds.getMin());
-      Point p2 = mv.getPoint(bounds.getMax());
-      Rectangle r = new Rectangle(Math.min(p1.x, p2.x), Math.min(p1.y, p2.y), Math.abs(p2.x - p1.x),
-          Math.abs(p2.y - p1.y));
-      a.subtract(new Area(r));
+    /**
+     * Subtracts the download bounds from the rectangular bounds of the map view.
+     *
+     * @param mv       the MapView that is used for the LatLon-to-Point-conversion and that determines
+     *             the Bounds from which the downloaded Bounds are subtracted
+     * @param downloadBounds multiple {@link Bounds} objects that represent the downloaded area
+     * @return the difference between the {@link MapView}s bounds and the downloaded area
+     */
+    public static Area getNonDownloadedArea(MapView mv, Iterable<Bounds> downloadBounds) {
+        Rectangle b = mv.getBounds();
+        // on some platforms viewport bounds seem to be offset from the left,
+        // over-grow it just to be sure
+        b.grow(100, 100);
+        Area a = new Area(b);
+        // now successively subtract downloaded areas
+        for (Bounds bounds : downloadBounds) {
+            Point p1 = mv.getPoint(bounds.getMin());
+            Point p2 = mv.getPoint(bounds.getMax());
+            Rectangle r = new Rectangle(Math.min(p1.x, p2.x), Math.min(p1.y, p2.y), Math.abs(p2.x - p1.x),
+                    Math.abs(p2.y - p1.y));
+            a.subtract(new Area(r));
+        }
+        return a;
     }
-    return a;
-  }
 
-  /**
-   * Converts a {@link StreetsideSequence} into a {@link Path2D} that can be drawn
-   * on the specified {@link NavigatableComponent}'s {@link Graphics2D}-context.
-   *
-   * @param nc  the {@link NavigatableComponent} for which this conversion should be performed, typically a {@link MapView}
-   * @param seq the sequence to convert
-   * @return the {@link Path2D} object to which the {@link StreetsideSequence} has been converted
-   */
-  public static Path2D getSequencePath(NavigatableComponent nc, StreetsideSequence seq) {
-    final Path2D.Double path = new Path2D.Double();
-    seq.getImages().stream().filter(StreetsideAbstractImage::isVisible).forEach(img -> {
-      Point p = nc.getPoint(img.getMovingLatLon());
-      if (path.getCurrentPoint() == null) {
-        path.moveTo(p.getX(), p.getY());
-      } else {
-        path.lineTo(p.getX(), p.getY());
-      }
-    });
-    return path;
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/PluginState.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/PluginState.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/PluginState.java	(revision 36228)
@@ -2,137 +2,48 @@
 package org.openstreetmap.josm.plugins.streetside.utils;
 
-import static org.openstreetmap.josm.tools.I18n.tr;
-
 import java.util.logging.Logger;
 
-import javax.swing.JOptionPane;
-
-import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.tools.I18n;
 import org.openstreetmap.josm.tools.Logging;
 
 /**
+ * The current state of the plugin (used for uploads and downloads)
  * @author nokutu
- *
  */
 public final class PluginState {
 
-  private static final Logger LOGGER = Logger.getLogger(PluginState.class.getCanonicalName());
+    private static final Logger LOGGER = Logger.getLogger(PluginState.class.getCanonicalName());
 
-  private static boolean submittingChangeset;
+    private static int runningDownloads;
 
-  private static int runningDownloads;
-  /**
-   * Images that have to be uploaded.
-   */
-  private static int imagesToUpload;
-  /**
-   * Images that have been uploaded.
-   */
-  private static int imagesUploaded;
+    private PluginState() {
+        // Empty constructor to avoid instantiation
+    }
 
-  private PluginState() {
-    // Empty constructor to avoid instantiation
-  }
+    /**
+     * Called when a download is started.
+     */
+    public static void startDownload() {
+        runningDownloads++;
+    }
 
-  /**
-   * Called when a download is started.
-   */
-  public static void startDownload() {
-    runningDownloads++;
-  }
+    /**
+     * Called when a download is finished.
+     */
+    public static void finishDownload() {
+        if (runningDownloads == 0) {
+            LOGGER.log(Logging.LEVEL_WARN, () -> I18n.tr("The amount of running downloads is equal to 0"));
+            return;
+        }
+        runningDownloads--;
+    }
 
-  /**
-   * Called when a download is finished.
-   */
-  public static void finishDownload() {
-    if (runningDownloads == 0) {
-      LOGGER.log(Logging.LEVEL_WARN, I18n.tr("The amount of running downloads is equal to 0"));
-      return;
+    /**
+     * Checks if there is any running download.
+     *
+     * @return true if the plugin is downloading; false otherwise.
+     */
+    public static boolean isDownloading() {
+        return runningDownloads > 0;
     }
-    runningDownloads--;
-  }
-
-  /**
-   * Checks if there is any running download.
-   *
-   * @return true if the plugin is downloading; false otherwise.
-   */
-  public static boolean isDownloading() {
-    return runningDownloads > 0;
-  }
-
-  /**
-   * Checks if there is a changeset being submitted.
-   *
-   * @return true if the plugin is submitting a changeset false otherwise.
-   */
-  public static boolean isSubmittingChangeset() {
-    return submittingChangeset;
-  }
-
-  public static void setSubmittingChangeset(boolean isSubmitting) {
-    submittingChangeset = isSubmitting;
-  }
-
-  /**
-   * Checks if there is any running upload.
-   *
-   * @return true if the plugin is uploading; false otherwise.
-   */
-  public static boolean isUploading() {
-    return imagesToUpload > imagesUploaded;
-  }
-
-  /**
-   * Sets the amount of images that are going to be uploaded.
-   *
-   * @param amount The amount of images that are going to be uploaded.
-   */
-  public static void addImagesToUpload(int amount) {
-    if (imagesToUpload <= imagesUploaded) {
-      imagesToUpload = 0;
-      imagesUploaded = 0;
-    }
-    imagesToUpload += amount;
-  }
-
-  public static int getImagesToUpload() {
-    return imagesToUpload;
-  }
-
-  public static int getImagesUploaded() {
-    return imagesUploaded;
-  }
-
-  /**
-   * Called when an image is uploaded.
-   */
-  public static void imageUploaded() {
-    imagesUploaded++;
-    if (imagesToUpload == imagesUploaded) {
-      finishedUploadDialog(imagesUploaded);
-    }
-  }
-
-  private static void finishedUploadDialog(int numImages) {
-    JOptionPane.showMessageDialog(MainApplication.getMainFrame(),
-        tr("You have successfully uploaded {0} images to Bing.com", numImages), tr("Finished upload"),
-        JOptionPane.INFORMATION_MESSAGE);
-  }
-
-  public static void notLoggedInToMapillaryDialog() {
-    JOptionPane.showMessageDialog(MainApplication.getMainFrame(),
-        tr("You are not logged in, please log in to Streetside in the preferences"),
-        tr("Not Logged in to Streetside"), JOptionPane.WARNING_MESSAGE);
-  }
-
-  /**
-   * Returns the text to be written in the status bar.
-   *
-   * @return The {@code String} that is going to be written in the status bar.
-   */
-  public static String getUploadString() {
-    return tr("Uploading: {0}", "(" + imagesUploaded + "/" + imagesToUpload + ")");
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/StreetsideChangesetListener.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/StreetsideChangesetListener.java	(revision 36194)
+++ 	(revision )
@@ -1,16 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.streetside.utils;
-
-import org.openstreetmap.josm.plugins.streetside.StreetsideData;
-
-/**
- * Interface for listeners of the class {@link StreetsideData}.
- */
-@FunctionalInterface
-public interface StreetsideChangesetListener {
-
-  /**
-   * Fired when the an image is added or removed from the changeset.
-   */
-  void changesetChanged();
-}
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/StreetsideColorScheme.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/StreetsideColorScheme.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/StreetsideColorScheme.java	(revision 36228)
@@ -6,59 +6,52 @@
 import javax.swing.JComponent;
 
+/**
+ * The color scheme for Streetside
+ */
 public final class StreetsideColorScheme {
-  /**
-   * Color for unselected images
-   */
-  public static final Color SEQ_UNSELECTED = new Color(0x00ccd1);
-  /**
-   * Color for the camera angle indicator of images in unselected sequences
-   */
-  public static final Color SEQ_UNSELECTED_CA = new Color(0x4169e1);
-  /**
-   * Color for the marker of images in a selected sequence
-   */
-  public static final Color SEQ_SELECTED = new Color(0x00b5f5);
-  /**
-   * Color for the camera angle indicator of images in selected sequences
-   */
-  public static final Color SEQ_SELECTED_CA = new Color(0x8b008b);
-  /**
-   * Color for the marker of currently selected images
-   */
-  public static final Color SEQ_HIGHLIGHTED = new Color(0xf5811a);
-  /**
-   * Color for the camera angle indicator of the currently selected images
-   */
-  public static final Color SEQ_HIGHLIGHTED_CA = new Color(0xf5b81a);
+    /**
+     * Color for unselected images
+     */
+    public static final Color SEQ_UNSELECTED = new Color(0x00ccd1);
+    /**
+     * Color for the camera angle indicator of images in unselected sequences
+     */
+    public static final Color SEQ_UNSELECTED_CA = new Color(0x4169e1);
+    /**
+     * Color for the marker of images in a selected sequence
+     */
+    public static final Color SEQ_SELECTED = new Color(0x00b5f5);
+    /**
+     * Color for the camera angle indicator of images in selected sequences
+     */
+    public static final Color SEQ_SELECTED_CA = new Color(0x8b008b);
+    /**
+     * Color for the marker of currently selected images
+     */
+    public static final Color SEQ_HIGHLIGHTED = new Color(0xf5811a);
+    /**
+     * Color for the camera angle indicator of the currently selected images
+     */
+    public static final Color SEQ_HIGHLIGHTED_CA = new Color(0xf5b81a);
 
-  public static final Color SEQ_IMPORTED_SELECTED = new Color(0xdddddd);
-  public static final Color SEQ_IMPORTED_SELECTED_CA = SEQ_IMPORTED_SELECTED.brighter();
-  public static final Color SEQ_IMPORTED_UNSELECTED = new Color(0x999999);
-  public static final Color SEQ_IMPORTED_UNSELECTED_CA = SEQ_IMPORTED_UNSELECTED.brighter();
-  public static final Color SEQ_IMPORTED_HIGHLIGHTED = new Color(0xbb2222);
-  public static final Color SEQ_IMPORTED_HIGHLIGHTED_CA = SEQ_IMPORTED_HIGHLIGHTED.brighter();
+    public static final Color STREETSIDE_BLUE = new Color(0x0000ff);
+    public static final Color TOOLBAR_DARK_GREY = new Color(0x242528);
 
-  public static final Color STREETSIDE_BLUE = new Color(0x0000ff);
-  public static final Color TOOLBAR_DARK_GREY = new Color(0x242528);
+    private StreetsideColorScheme() {
+        // Private constructor to avoid instantiation
+    }
 
-  public static final Color IMAGEDETECTION_TRAFFICSIGN = new Color(0xffc01b);
-  public static final Color IMAGEDETECTION_UNKNOWN = new Color(0x33bb44);
-
-  private StreetsideColorScheme() {
-    // Private constructor to avoid instantiation
-  }
-
-  /**
-   * Styles the given components as default panels (currently only the background is set to white)
-   *
-   * @param components the components to style as default panels (e.g. checkboxes also, that's why
-   *           not only JPanels are accepted)
-   */
-  public static void styleAsDefaultPanel(JComponent... components) {
-    if (components != null && components.length >= 1) {
-      for (JComponent component : components) {
-        component.setBackground(Color.WHITE);
-      }
+    /**
+     * Styles the given components as default panels (currently only the background is set to white)
+     *
+     * @param components the components to style as default panels (e.g. checkboxes also, that's why
+     *           not only JPanels are accepted)
+     */
+    public static void styleAsDefaultPanel(JComponent... components) {
+        if (components != null && components.length >= 1) {
+            for (JComponent component : components) {
+                component.setBackground(Color.WHITE);
+            }
+        }
     }
-  }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/StreetsideProperties.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/StreetsideProperties.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/StreetsideProperties.java	(revision 36228)
@@ -10,102 +10,77 @@
 import org.openstreetmap.josm.data.preferences.StringProperty;
 import org.openstreetmap.josm.plugins.streetside.gui.imageinfo.ImageInfoPanel;
-import org.openstreetmap.josm.plugins.streetside.gui.imageinfo.StreetsideViewerPanel;
 import org.openstreetmap.josm.plugins.streetside.io.download.StreetsideDownloader;
 
+/**
+ * Properties for Streetside
+ */
 public final class StreetsideProperties {
-  public static final BooleanProperty DELETE_AFTER_UPLOAD = new BooleanProperty("streetside.delete-after-upload",
-      true);
-  public static final BooleanProperty DEVELOPER = new BooleanProperty("streetside.developer", false);
-  public static final BooleanProperty DISPLAY_HOUR = new BooleanProperty("streetside.display-hour", true);
-  public static final BooleanProperty HOVER_ENABLED = new BooleanProperty("streetside.hover-enabled", true);
-  public static final BooleanProperty MOVE_TO_IMG = new BooleanProperty("streetside.move-to-picture", true);
-  public static final BooleanProperty TIME_FORMAT_24 = new BooleanProperty("streetside.format-24", false);
-  public static final BooleanProperty IMAGE_LINK_TO_BLUR_EDITOR = new BooleanProperty(
-      "streetside.image-link-to-blur-editor", true);
-  public static final BooleanProperty CUBEMAP_LINK_TO_BLUR_EDITOR = new BooleanProperty(
-      "streetside.cubemap-link-to-blur-editor", true);
-  public static final IntegerProperty TILE_DOWNLOAD_THREAD_PAUSE_LEN_SEC = new IntegerProperty(
-      "streetside.tile-download-thread-pause-len-sec", 60);
-  public static final BooleanProperty PREDOWNLOAD_CUBEMAPS = new BooleanProperty("streetside.predownload-cubemaps",
-      false);
-  public static final BooleanProperty DEBUGING_ENABLED = new BooleanProperty("streetside.debugging-enabled", false);
-  public static final BooleanProperty DOWNLOAD_CUBEFACE_TILES_TOGETHER = new BooleanProperty(
-      "streetside.download-cubeface-tiles-together", false);
+    public static final BooleanProperty DEVELOPER = new BooleanProperty("streetside.developer", false);
+    public static final BooleanProperty DISPLAY_HOUR = new BooleanProperty("streetside.display-hour", true);
+    public static final BooleanProperty HOVER_ENABLED = new BooleanProperty("streetside.hover-enabled", true);
+    public static final BooleanProperty MOVE_TO_IMG = new BooleanProperty("streetside.move-to-picture", true);
+    public static final BooleanProperty TIME_FORMAT_24 = new BooleanProperty("streetside.format-24", false);
+    public static final BooleanProperty IMAGE_LINK_TO_BLUR_EDITOR = new BooleanProperty(
+            "streetside.image-link-to-blur-editor", true);
+    public static final BooleanProperty CUBEMAP_LINK_TO_BLUR_EDITOR = new BooleanProperty(
+            "streetside.cubemap-link-to-blur-editor", true);
+    public static final BooleanProperty PREDOWNLOAD_CUBEMAPS = new BooleanProperty("streetside.predownload-cubemaps",
+            false);
+    public static final BooleanProperty DEBUGING_ENABLED = new BooleanProperty("streetside.debugging-enabled", false);
+    public static final BooleanProperty DOWNLOAD_CUBEFACE_TILES_TOGETHER = new BooleanProperty(
+            "streetside.download-cubeface-tiles-together", false);
 
-  /**
-   * If false, all sequences that cross the download bounds are put completely into the StreetsideData object.
-   * Otherwise only all images (!) inside the download bounds are added, the others are discarded.
-   */
-  public static final BooleanProperty CUT_OFF_SEQUENCES_AT_BOUNDS = new BooleanProperty(
-      "streetside.cut-off-sequences-at-bounds", false);
-  public static final IntegerProperty MAPOBJECT_ICON_SIZE = new IntegerProperty("streetside.mapobjects.iconsize", 32);
-  public static final IntegerProperty MAX_MAPOBJECTS = new IntegerProperty("streetside.mapobjects.maximum-number",
-      200);
-  public static final BooleanProperty SHOW_DETECTED_SIGNS = new BooleanProperty("streetside.show-detected-signs",
-      true);
-  public static final BooleanProperty SHOW_HIGH_RES_STREETSIDE_IMAGERY = new BooleanProperty(
-      "streetside.show-high-res-streetside-imagery", true);
+    /**
+     * If false, all sequences that cross the download bounds are put completely into the StreetsideData object.
+     * Otherwise only all images (!) inside the download bounds are added, the others are discarded.
+     */
+    public static final BooleanProperty CUT_OFF_SEQUENCES_AT_BOUNDS = new BooleanProperty(
+            "streetside.cut-off-sequences-at-bounds", false);
+    public static final BooleanProperty SHOW_DETECTED_SIGNS = new BooleanProperty("streetside.show-detected-signs",
+            true);
+    public static final BooleanProperty SHOW_HIGH_RES_STREETSIDE_IMAGERY = new BooleanProperty(
+            "streetside.show-high-res-streetside-imagery", true);
 
-  /**
-   * See {@code OsmDataLayer#PROPERTY_BACKGROUND_COLOR}
-   */
-  public static final NamedColorProperty BACKGROUND = new NamedColorProperty("background", Color.BLACK);
-  /**
-   * See {@code OsmDataLayer#PROPERTY_OUTSIDE_COLOR}
-   */
-  public static final NamedColorProperty OUTSIDE_DOWNLOADED_AREA = new NamedColorProperty("outside downloaded area",
-      Color.YELLOW);
+    /**
+     * See {@code OsmDataLayer#PROPERTY_BACKGROUND_COLOR}
+     */
+    public static final NamedColorProperty BACKGROUND = new NamedColorProperty("background", Color.BLACK);
+    /**
+     * See {@code OsmDataLayer#PROPERTY_OUTSIDE_COLOR}
+     */
+    public static final NamedColorProperty OUTSIDE_DOWNLOADED_AREA = new NamedColorProperty("outside downloaded area",
+            Color.YELLOW);
 
-  public static final DoubleProperty MAX_DOWNLOAD_AREA = new DoubleProperty("streetside.max-download-area", 0.015);
+    public static final DoubleProperty MAX_DOWNLOAD_AREA = new DoubleProperty("streetside.max-download-area", 0.015);
 
-  public static final IntegerProperty PICTURE_DRAG_BUTTON = new IntegerProperty("streetside.picture-drag-button", 3);
-  public static final IntegerProperty PICTURE_OPTION_BUTTON = new IntegerProperty("streetside.picture-option-button",
-      2);
-  public static final IntegerProperty PICTURE_ZOOM_BUTTON = new IntegerProperty("streetside.picture-zoom-button", 1);
-  public static final IntegerProperty SEQUENCE_MAX_JUMP_DISTANCE = new IntegerProperty(
-      "streetside.sequence-max-jump-distance", 100);
+    public static final IntegerProperty PICTURE_DRAG_BUTTON = new IntegerProperty("streetside.picture-drag-button", 3);
+    public static final IntegerProperty PICTURE_OPTION_BUTTON = new IntegerProperty("streetside.picture-option-button",
+            2);
+    public static final IntegerProperty PICTURE_ZOOM_BUTTON = new IntegerProperty("streetside.picture-zoom-button", 1);
+    public static final IntegerProperty SEQUENCE_MAX_JUMP_DISTANCE = new IntegerProperty(
+            "streetside.sequence-max-jump-distance", 100);
 
-  public static final StringProperty ACCESS_TOKEN = new StringProperty("streetside.access-token", null);
-  public static final StringProperty DOWNLOAD_MODE = new StringProperty("streetside.download-mode",
-      StreetsideDownloader.DOWNLOAD_MODE.DEFAULT.getPrefId());
-  public static final StringProperty START_DIR = new StringProperty("streetside.start-directory",
-      System.getProperty("user.home"));
-  public static final StringProperty URL_CLIENT_ID = new StringProperty("streetside.url-clientid",
-      System.getProperty("streetside.url-clientid", "T1Fzd20xZjdtR0s1VDk5OFNIOXpYdzoxNDYyOGRkYzUyYTFiMzgz"));
-  public static final StringProperty BING_MAPS_KEY = new StringProperty("streetside.bing-maps-key",
-      System.getProperty("streetside.bing-maps-key", "AuftgJsO0Xs8Ts4M1xZUQJQXJNsvmh3IV8DkNieCiy3tCwCUMq76-WpkrBtNAuEm"));
-  public static final StringProperty TEST_BUBBLE_ID = new StringProperty("streetside.test-bubble-id",
-      System.getProperty("streetside.test-bubble-id", "80848005"));
+    public static final StringProperty DOWNLOAD_MODE = new StringProperty("streetside.download-mode",
+            StreetsideDownloader.DOWNLOAD_MODE.DEFAULT.getPrefId());
+    public static final StringProperty BING_MAPS_KEY = new StringProperty("streetside.bing-maps-key",
+            System.getProperty("streetside.bing-maps-key",
+                    "Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU"));
 
-  /**
-   * The number of times the help popup for the {@link ImageInfoPanel} will be displayed.
-   * But regardless of this number, the popup will only show up at most once between two (re)starts of JOSM.
-   * Or opening the {@link ImageInfoPanel} immediately brings this number down to zero.
-   */
-  public static final IntegerProperty IMAGEINFO_HELP_COUNTDOWN = new IntegerProperty(
-      "streetside.imageInfo.helpDisplayedCountdown", 4);
+    /**
+     * The number of times the help popup for the {@link ImageInfoPanel} will be displayed.
+     * But regardless of this number, the popup will only show up at most once between two (re)starts of JOSM.
+     * Or opening the {@link ImageInfoPanel} immediately brings this number down to zero.
+     */
+    public static final IntegerProperty IMAGEINFO_HELP_COUNTDOWN = new IntegerProperty(
+            "streetside.imageInfo.helpDisplayedCountdown", 4);
 
-  /**
-   * The number of times the help popup for the {@link StreetsideViewerPanel} will be displayed.
-   * But regardless of this number, the popup will only show up at most once between two (re)starts of JOSM.
-   * Or opening the {@link StreetsideViewerPanel} immediately brings this number down to zero.
-   */
-  public static final IntegerProperty STREETSIDE_VIEWER_HELP_COUNTDOWN = new IntegerProperty(
-      "streetside.streetsideViewer.helpDisplayedCountdown", 4);
+    /**
+     * The number of images to be prefetched when a streetside image is selected
+     */
+    public static final IntegerProperty PRE_FETCH_IMAGE_COUNT = new IntegerProperty("streetside.prefetch-image-count",
+            2);
 
-  /**
-   * The number of images to be prefetched when a streetside image is selected
-   */
-  public static final IntegerProperty PRE_FETCH_IMAGE_COUNT = new IntegerProperty("streetside.prefetch-image-count",
-      2);
-
-  /**
-   * The number of images to be prefetched when a streetside image is selected
-   */
-  public static final IntegerProperty PRE_FETCH_CUBEMAP_COUNT = new IntegerProperty("streetside.prefetch-image-count",
-      2);
-
-  private StreetsideProperties() {
-    // Private constructor to avoid instantiation
-  }
+    private StreetsideProperties() {
+        // Private constructor to avoid instantiation
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/StreetsideSequenceIdGenerator.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/StreetsideSequenceIdGenerator.java	(revision 36194)
+++ 	(revision )
@@ -1,23 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.streetside.utils;
-
-import java.util.UUID;
-
-/**
- * Utility class to generated unique ids for Streetside "sequences".
- * Due to the functionality inherited from Mapillary the plugin is structured to
- * handle sequences of contiguous imagery, but Streetside only has implicit
- * sequences defined by the "pre" and "ne" attributes.
- *
- * @see org.openstreetmap.josm.plugins.streetside.StreetsideSequence
- */
-public class StreetsideSequenceIdGenerator {
-
-  private StreetsideSequenceIdGenerator() {
-    // private constructor to avoid instantiation
-  }
-
-  public static String generateId() {
-    return UUID.randomUUID().toString();
-  }
-}
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/StreetsideURL.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/StreetsideURL.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/StreetsideURL.java	(revision 36228)
@@ -2,344 +2,199 @@
 package org.openstreetmap.josm.plugins.streetside.utils;
 
-import java.io.UnsupportedEncodingException;
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.EnumSet;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.logging.Logger;
 
 import org.openstreetmap.josm.data.Bounds;
-import org.openstreetmap.josm.plugins.streetside.cubemap.CubemapUtils;
-import org.openstreetmap.josm.tools.I18n;
+import org.openstreetmap.josm.data.coor.ILatLon;
 import org.openstreetmap.josm.tools.Logging;
 
+/**
+ * A class for building the base URLs for Streetside objects
+ */
 public final class StreetsideURL {
 
-  private static final Logger LOGGER = Logger.getLogger(StreetsideURL.class.getCanonicalName());
-
-  /**
-   * Base URL of the Bing Bubble API.
-   */
-  private static final String STREETSIDE_BASE_URL = "https://dev.virtualearth.net/mapcontrol/HumanScaleServices/GetBubbles.ashx";
-  /**
-   * Base URL for Streetside privacy concerns.
-   */
-  private static final String STREETSIDE_PRIVACY_URL = "https://www.bing.com/maps/privacyreport/streetsideprivacyreport?bubbleid=";
-
-  private static final int OSM_BBOX_NORTH = 3;
-  private static final int OSM_BBOX_SOUTH = 1;
-  private static final int OSM_BBOXEAST = 2;
-  private static final int OSM_BBOX_WEST = 0;
-
-  private StreetsideURL() {
-    // Private constructor to avoid instantiation
-  }
-
-  public static URL[] string2URLs(String baseUrlPrefix, String cubemapImageId, String baseUrlSuffix) {
-    List<URL> res = new ArrayList<>();
-
-    switch (Boolean.TRUE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get()) ? 16 : 4) {
-
-    case 16:
-
-      EnumSet.allOf(CubemapUtils.CubemapFaces.class).forEach(face -> {
-        for (int i = 0; i < 4; i++) {
-          for (int j = 0; j < 4; j++) {
-            try {
-              final String urlStr = baseUrlPrefix + cubemapImageId
-                  + CubemapUtils.rowCol2StreetsideCellAddressMap.get(String.valueOf(i) + j)
-                  + baseUrlSuffix;
-              res.add(new URL(urlStr));
-            } catch (final MalformedURLException e) {
-              LOGGER.log(Logging.LEVEL_ERROR, "Error creating URL String for cubemap " + cubemapImageId);
-              e.printStackTrace();
-            }
-
-          }
-        }
-      });
-      break;
-
-    case 4:
-      EnumSet.allOf(CubemapUtils.CubemapFaces.class).forEach(face -> {
-        for (int i = 0; i < 4; i++) {
-
-          try {
-            final String urlStr = baseUrlPrefix + cubemapImageId
-                + CubemapUtils.rowCol2StreetsideCellAddressMap.get(String.valueOf(i)) + baseUrlSuffix;
-            res.add(new URL(urlStr));
-          } catch (final MalformedURLException e) {
-            LOGGER.log(Logging.LEVEL_WARN, "Error creating URL String for cubemap " + cubemapImageId);
-            e.printStackTrace();
-          }
-
-        }
-      });
-      break; // break is optional
-    default:
-      // Statements
-    }
-    return res.toArray(new URL[0]);
-  }
-
-  /**
-   * Builds a query string from it's parts that are supplied as a {@link Map}
-   *
-   * @param parts the parts of the query string
-   * @return the constructed query string (including a leading ?)
-   */
-  static String queryString(Map<String, String> parts) {
-    final StringBuilder ret = new StringBuilder("?client_id=").append(StreetsideProperties.URL_CLIENT_ID.get());
-    if (parts != null) {
-      for (final Entry<String, String> entry : parts.entrySet()) {
+    private static final Logger LOGGER = Logger.getLogger(StreetsideURL.class.getCanonicalName());
+
+    /**
+     * Base URL of the Bing Bubble API.
+     */
+    private static final String STREETSIDE_BASE_URL = "https://dev.virtualearth.net/REST/v1/Imagery/MetaData/Streetside";
+    /**
+     * Base URL for Streetside privacy concerns.
+     */
+    private static final String STREETSIDE_PRIVACY_URL = "https://www.bing.com/maps/privacyreport/streetsideprivacyreport?bubbleid=";
+
+    private static final int OSM_BBOX_NORTH = 3;
+    private static final int OSM_BBOX_SOUTH = 1;
+    private static final int OSM_BBOXEAST = 2;
+    private static final int OSM_BBOX_WEST = 0;
+
+    private StreetsideURL() {
+        // Private constructor to avoid instantiation
+    }
+
+    /**
+     * Convert a map of query parameters to a string
+     * @param parts The query parameters
+     * @return The string to send the server
+     */
+    static String queryStreetsideBoundsString(Map<String, String> parts) {
+        final var ret = new StringBuilder(100);
+        if (parts != null) {
+            ret.append("?count=500").append("&key=").append(StreetsideProperties.BING_MAPS_KEY.get());
+            if (parts.containsKey("bbox")) {
+                final String[] bbox = parts.get("bbox").split(",");
+                ret.append("&mapArea=").append(bbox[OSM_BBOX_SOUTH]).append(',').append(bbox[OSM_BBOX_WEST]).append(',')
+                        .append(bbox[OSM_BBOX_NORTH]).append(',').append(bbox[OSM_BBOXEAST]);
+            }
+        }
+
+        return ret.toString();
+    }
+
+    /**
+     * Converts a {@link String} into a {@link URL} without throwing a {@link MalformedURLException}.
+     * Instead, such an exception will lead to an {@link Logger}.
+     * So you should be very confident that your URL is well-formed when calling this method.
+     *
+     * @param strings the Strings describing the URL
+     * @return the URL that is constructed from the given string
+     */
+    static URL string2URL(String... strings) {
+        final var builder = new StringBuilder();
+        for (var i = 0; strings != null && i < strings.length; i++) {
+            builder.append(strings[i]);
+        }
         try {
-          ret.append('&').append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8.name())).append('=')
-              .append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.name()));
-        } catch (final UnsupportedEncodingException e) {
-          LOGGER.log(Logging.LEVEL_WARN, e.getMessage(), e); // This should not happen, as the encoding is hard-coded
-        }
-      }
-    }
-
-    if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-      LOGGER.log(Logging.LEVEL_DEBUG, MessageFormat.format("queryString result: {0}", ret));
-    }
-
-    return ret.toString();
-  }
-
-  static String queryStreetsideBoundsString(Map<String, String> parts) {
-    final StringBuilder ret = new StringBuilder("?n=");
-    if (parts != null) {
-      final List<String> bbox = new ArrayList<>(Arrays.asList(parts.get("bbox").split(",")));
-      try {
-        ret.append(URLEncoder.encode(bbox.get(StreetsideURL.OSM_BBOX_NORTH), StandardCharsets.UTF_8.name()))
-            .append("&s=")
-            .append(URLEncoder.encode(bbox.get(StreetsideURL.OSM_BBOX_SOUTH),
-                StandardCharsets.UTF_8.name()))
-            .append("&e=")
-            .append(URLEncoder.encode(bbox.get(StreetsideURL.OSM_BBOXEAST), StandardCharsets.UTF_8.name()))
-            .append("&w=")
-            .append(URLEncoder.encode(bbox.get(StreetsideURL.OSM_BBOX_WEST), StandardCharsets.UTF_8.name()))
-            .append("&c=1000").append("&appkey=").append(StreetsideProperties.BING_MAPS_KEY.get());
-      } catch (final UnsupportedEncodingException e) {
-        LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e); // This should not happen, as the encoding is hard-coded
-      }
-    }
-
-    return ret.toString();
-  }
-
-  static String queryByIdString(Map<String, String> parts) {
-    final StringBuilder ret = new StringBuilder("?id=");
-    try {
-      ret.append(URLEncoder.encode(StreetsideProperties.TEST_BUBBLE_ID.get(), StandardCharsets.UTF_8.name()));
-      ret.append('&').append(URLEncoder.encode("appkey=", StandardCharsets.UTF_8.name())).append('=')
-          .append(URLEncoder.encode(StreetsideProperties.BING_MAPS_KEY.get(), StandardCharsets.UTF_8.name()));
-    } catch (final UnsupportedEncodingException e) {
-      LOGGER.log(Logging.LEVEL_ERROR, e.getMessage(), e); // This should not happen, as the encoding is hard-coded
-    }
-
-    if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-      LOGGER.info("queryById result: " + ret);
-    }
-    return ret.toString();
-  }
-
-  /**
-   * Converts a {@link String} into a {@link URL} without throwing a {@link MalformedURLException}.
-   * Instead such an exception will lead to an {@link Logger}.
-   * So you should be very confident that your URL is well-formed when calling this method.
-   *
-   * @param strings the Strings describing the URL
-   * @return the URL that is constructed from the given string
-   */
-  static URL string2URL(String... strings) {
-    final StringBuilder builder = new StringBuilder();
-    for (int i = 0; strings != null && i < strings.length; i++) {
-      builder.append(strings[i]);
-    }
-    try {
-      return new URL(builder.toString());
-    } catch (final MalformedURLException e) {
-      LOGGER.log(Logging.LEVEL_ERROR, I18n.tr(String.format("The class '%s' produces malformed URLs like '%s'!",
-          StreetsideURL.class.getName(), builder), e));
-      return null;
-    }
-  }
-
-  public static final class APIv3 {
-
-    private APIv3() {
-      // Private constructor to avoid instantiation
-    }
-
-    public static URL searchStreetsideImages(Bounds bounds) {
-      return StreetsideURL.string2URL(StreetsideURL.STREETSIDE_BASE_URL, APIv3.queryStreetsideString(bounds));
-    }
-
-    /**
-     * The APIv3 returns a Link header for each request. It contains a URL for requesting more results.
-     * If you supply the value of the Link header, this method returns the next URL,
-     * if such a URL is defined in the header.
-     *
-     * @param value the value of the HTTP-header with key "Link"
-     * @return the {@link URL} for the next result page, or <code>null</code> if no such URL could be found
-     */
-    public static URL parseNextFromLinkHeaderValue(String value) {
-      if (value != null) {
-        // Iterate over the different entries of the Link header
-        for (final String link : value.split(",", Integer.MAX_VALUE)) {
-          boolean isNext = false;
-          URL url = null;
-          // Iterate over the parts of each entry (typically it's one `rel="‹linkType›"` and one like `<https://URL>`)
-          for (String linkPart : link.split(";", Integer.MAX_VALUE)) {
-            linkPart = linkPart.trim();
-            isNext |= linkPart.matches("rel\\s*=\\s*\"next\"");
-            if (linkPart.length() >= 1 && linkPart.charAt(0) == '<' && linkPart.endsWith(">")) {
-              try {
-                url = new URL(linkPart.substring(1, linkPart.length() - 1));
-              } catch (final MalformedURLException e) {
-                Logging.log(Logging.LEVEL_WARN,
-                    "Mapillary API v3 returns a malformed URL in the Link header.", e);
-              }
-            }
-          }
-          // If both a URL and the rel=next attribute are present, return the URL. Otherwise null is returned
-          if (url != null && isNext) {
-            return url;
-          }
-        }
-      }
-      return null;
-    }
-
-    public static String queryString(final Bounds bounds) {
-      if (bounds != null) {
-        final Map<String, String> parts = new HashMap<>();
-        parts.put("bbox", bounds.toBBox().toStringCSV(","));
-        return StreetsideURL.queryString(parts);
-      }
-      return StreetsideURL.queryString(null);
-    }
-
-    public static String queryStreetsideString(final Bounds bounds) {
-      if (bounds != null) {
-        final Map<String, String> parts = new HashMap<>();
-        parts.put("bbox", bounds.toBBox().toStringCSV(","));
-        return StreetsideURL.queryStreetsideBoundsString(parts);
-      }
-      return StreetsideURL.queryStreetsideBoundsString(null);
-    }
-
-  }
-
-  public static final class VirtualEarth {
-    private static final String BASE_URL_PREFIX = "https://t.ssl.ak.tiles.virtualearth.net/tiles/hs";
-    private static final String BASE_URL_SUFFIX = ".jpg?g=6528&n=z";
-
-    private VirtualEarth() {
-      // Private constructor to avoid instantiation
-    }
-
-    public static URL streetsideTile(final String id, boolean thumbnail) {
-      StringBuilder modifiedId = new StringBuilder();
-
-      if (thumbnail) {
-        // pad thumbnail imagery with leading zeros
-        if (id.length() < 16) {
-          for (int i = 0; i < 16 - id.length(); i++) {
-            modifiedId.append("0");
-          }
-        }
-        modifiedId.append(id).append("01");
-      } else {
-          if (Boolean.TRUE.equals(StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get())) {
-              // pad 16-tiled imagery with leading zeros
-              if (id.length() < 20) {
-                  for (int i = 0; i < 20 - id.length(); i++) {
-                      modifiedId.append("0");
-                  }
-              }
-          } else {
-              // pad 4-tiled imagery with leading zeros
-              if (id.length() < 19) {
-                  for (int i = 0; i < 19 - id.length(); i++) {
-                      modifiedId.append("0");
-                  }
-              }
-          }
-          modifiedId.append(id);
-      }
-      URL url = StreetsideURL
-          .string2URL(VirtualEarth.BASE_URL_PREFIX + modifiedId + VirtualEarth.BASE_URL_SUFFIX);
-      if (Boolean.TRUE.equals(StreetsideProperties.DEBUGING_ENABLED.get())) {
-        LOGGER.log(Logging.LEVEL_DEBUG, MessageFormat.format("Tile task URL {0} invoked.", url));
-      }
-      return url;
-    }
-  }
-
-  public static final class MainWebsite {
-
-    private MainWebsite() {
-      // Private constructor to avoid instantiation
-    }
-
-    /**
-     * Gives you the URL for the online viewer of a specific Streetside image.
-     *
-     * @param id the id of the image to which you want to link
-     * @return the URL of the online viewer for the image with the given image key
-     * @throws IllegalArgumentException if the image key is <code>null</code>
-     */
-    public static URL browseImage(String id) {
-      if (id == null) {
-        throw new IllegalArgumentException("The image id may not be null!");
-      }
-
-      StringBuilder modifiedId = new StringBuilder();
-
-      // pad thumbnail imagery with leading zeros
-      if (id.length() < 16) {
-        for (int i = 0; i < 16 - id.length(); i++) {
-          modifiedId.append("0");
-        }
-      }
-      modifiedId.append(id).append("01");
-
-      return StreetsideURL.string2URL(MessageFormat.format("{0}{1}{2}", VirtualEarth.BASE_URL_PREFIX, modifiedId,
-          VirtualEarth.BASE_URL_SUFFIX));
-    }
-
-    /**
-     * Gives you the URL for the blur editor of the image with the given key.
-     *
-     * @param id the key of the image for which you want to open the blur editor
-     * @return the URL of the blur editor
-     * @throws IllegalArgumentException if the image key is <code>null</code>
-     */
-    public static URL streetsidePrivacyLink(final String id) {
-      if (id == null) {
-        throw new IllegalArgumentException("The image id must not be null!");
-      }
-      String urlEncodedId;
-      try {
-        urlEncodedId = URLEncoder.encode(id, StandardCharsets.UTF_8.name());
-      } catch (final UnsupportedEncodingException e) {
-        LOGGER.log(Logging.LEVEL_ERROR, I18n.tr("Unsupported encoding when URL encoding", e), e);
-        urlEncodedId = id;
-      }
-      return StreetsideURL.string2URL(StreetsideURL.STREETSIDE_PRIVACY_URL, urlEncodedId);
-    }
-
-  }
+            return new URI(builder.toString()).toURL();
+        } catch (final IllegalArgumentException | URISyntaxException | MalformedURLException e) {
+            LOGGER.log(Logging.LEVEL_ERROR, e,
+                    () -> MessageFormat.format("The class ''{0}'' produces malformed URLs like ''{1}''!",
+                            StreetsideURL.class.getName(), builder));
+            return null;
+        }
+    }
+
+    /**
+     * A class for the current Streetside API
+     */
+    public static final class APIv3 {
+
+        private APIv3() {
+            // Private constructor to avoid instantiation
+        }
+
+        /**
+         * Search for images inside some bounds
+         * @param bounds The bounds to search
+         * @return The URL to get
+         */
+        public static URL searchStreetsideImages(Bounds bounds) {
+            return StreetsideURL.string2URL(StreetsideURL.STREETSIDE_BASE_URL, APIv3.queryStreetsideString(bounds));
+        }
+
+        /**
+         * The APIv3 returns a Link header for each request. It contains a URL for requesting more results.
+         * If you supply the value of the Link header, this method returns the next URL,
+         * if such a URL is defined in the header.
+         *
+         * @param value the value of the HTTP-header with key "Link"
+         * @return the {@link URL} for the next result page, or <code>null</code> if no such URL could be found
+         */
+        public static URL parseNextFromLinkHeaderValue(String value) {
+            if (value != null) {
+                // Iterate over the different entries of the Link header
+                for (final String link : value.split(",", Integer.MAX_VALUE)) {
+                    var isNext = false;
+                    URL url = null;
+                    // Iterate over the parts of each entry (typically it's one `rel="‹linkType›"` and one like `<https://URL>`)
+                    for (String linkPart : link.split(";", Integer.MAX_VALUE)) {
+                        linkPart = linkPart.trim();
+                        isNext |= linkPart.matches("rel\\s*=\\s*\"next\"");
+                        if (linkPart.length() >= 1 && linkPart.charAt(0) == '<' && linkPart.endsWith(">")) {
+                            try {
+                                url = new URI(linkPart.substring(1, linkPart.length() - 1)).toURL();
+                            } catch (final URISyntaxException | MalformedURLException e) {
+                                Logging.log(Logging.LEVEL_WARN,
+                                        "Mapillary API v3 returns a malformed URL in the Link header.", e);
+                            }
+                        }
+                    }
+                    // If both a URL and the rel=next attribute are present, return the URL. Otherwise null is returned
+                    if (url != null && isNext) {
+                        return url;
+                    }
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Query for images inside some bounds
+         * @param bounds The bounds to query
+         * @return The URL to GET
+         */
+        public static String queryStreetsideString(final Bounds bounds) {
+            if (bounds != null) {
+                final Map<String, String> parts = new HashMap<>();
+                parts.put("bbox", bounds.toBBox().toStringCSV(","));
+                return StreetsideURL.queryStreetsideBoundsString(parts);
+            }
+            return StreetsideURL.queryStreetsideBoundsString(null);
+        }
+
+    }
+
+    /**
+     * A class for Microsoft Streetside website pages
+     */
+    public static final class MainWebsite {
+
+        private MainWebsite() {
+            // Private constructor to avoid instantiation
+        }
+
+        /**
+         * Gives you the URL for the online viewer of a specific Streetside image.
+         *
+         * @param image The image to show
+         * @return the URL of the online viewer for the image with the given image key
+         * @throws IllegalArgumentException if the image key is <code>null</code>
+         */
+        public static URL browseImage(ILatLon image) {
+            if (image == null || !image.isLatLonKnown()) {
+                throw new IllegalArgumentException("The image and image lat/lon may not be null!");
+            }
+            // The online viewer only takes lat/lon for args
+            // lvl == zoom level (needs to be high enough for MS to show streetside imagery)
+            // style=x -- needed to show the streetside imagery
+            return StreetsideURL.string2URL("https://www.bing.com/maps?lvl=16&style=x&cp=",
+                    Double.toString(image.lat()), "~", Double.toString(image.lon()));
+        }
+
+        /**
+         * Gives you the URL for the blur editor of the image with the given key.
+         *
+         * @param id the key of the image for which you want to open the blur editor
+         * @return the URL of the blur editor
+         * @throws IllegalArgumentException if the image key is <code>null</code>
+         */
+        public static URL streetsidePrivacyLink(final String id) {
+            if (id == null) {
+                throw new IllegalArgumentException("The image id must not be null!");
+            }
+            String urlEncodedId;
+            urlEncodedId = URLEncoder.encode(id, StandardCharsets.UTF_8);
+            return StreetsideURL.string2URL(StreetsideURL.STREETSIDE_PRIVACY_URL, urlEncodedId);
+        }
+
+    }
 }
Index: applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/StreetsideUtils.java
===================================================================
--- applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/StreetsideUtils.java	(revision 36194)
+++ applications/editors/josm/plugins/MicrosoftStreetside/src/org/openstreetmap/josm/plugins/streetside/utils/StreetsideUtils.java	(revision 36228)
@@ -2,24 +2,6 @@
 package org.openstreetmap.josm.plugins.streetside.utils;
 
-import java.awt.Desktop;
-import java.io.IOException;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Locale;
-import java.util.Set;
-
-import javax.swing.SwingUtilities;
-
-import org.apache.commons.imaging.common.RationalNumber;
-import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants;
-import org.openstreetmap.josm.data.Bounds;
-import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
 import org.openstreetmap.josm.plugins.streetside.StreetsideLayer;
-import org.openstreetmap.josm.plugins.streetside.StreetsideSequence;
 import org.openstreetmap.josm.tools.I18n;
 
@@ -31,257 +13,27 @@
 public final class StreetsideUtils {
 
-  private static final double MIN_ZOOM_SQUARE_SIDE = 0.002;
-
-  private StreetsideUtils() {
-    // Private constructor to avoid instantiation
-  }
-
-  /**
-   * Open the default browser in the given URL.
-   *
-   * @param url The (not-null) URL that is going to be opened.
-   * @throws IOException when the URL could not be opened
-   */
-  public static void browse(URL url) throws IOException {
-    if (url == null) {
-      throw new IllegalArgumentException();
-    }
-    Desktop desktop = Desktop.getDesktop();
-    if (desktop.isSupported(Desktop.Action.BROWSE)) {
-      try {
-        desktop.browse(url.toURI());
-      } catch (URISyntaxException e1) {
-        throw new IOException(e1);
-      }
-    } else {
-      Runtime runtime = Runtime.getRuntime();
-      runtime.exec("xdg-open " + url);
-    }
-  }
-
-  /**
-   * Returns the current date formatted as EXIF timestamp.
-   * As timezone the default timezone of the JVM is used ({@link java.util.TimeZone#getDefault()}).
-   *
-   * @return A {@code String} object containing the current date.
-   */
-  public static String currentDate() {
-    return new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.UK).format(Calendar.getInstance().getTime());
-  }
-
-  /**
-   * Returns current time in Epoch format (milliseconds since 1970-01-01T00:00:00+0000)
-   *
-   * @return The current date in Epoch format.
-   */
-  public static long currentTime() {
-    return Calendar.getInstance().getTimeInMillis();
-  }
-
-  /**
-   * Parses a string with a given format and returns the Epoch time.
-   * If no timezone information is given, the default timezone of the JVM is used
-   * ({@link java.util.TimeZone#getDefault()}).
-   *
-   * @param date   The string containing the date.
-   * @param format The format of the date.
-   * @return The date in Epoch format.
-   * @throws ParseException if the date cannot be parsed with the given format
-   */
-  public static long getEpoch(String date, String format) throws ParseException {
-    return new SimpleDateFormat(format, Locale.UK).parse(date).getTime();
-  }
-
-  /**
-   * Calculates the decimal degree-value from a degree value given in
-   * degrees-minutes-seconds-format
-   *
-   * @param degMinSec an array of length 3, the values in there are (in this order)
-   *          degrees, minutes and seconds
-   * @param ref     the latitude or longitude reference determining if the given value
-   *          is:
-   *          <ul>
-   *          <li>north (
-   *          {@link GpsTagConstants#GPS_TAG_GPS_LATITUDE_REF_VALUE_NORTH}) or
-   *          south (
-   *          {@link GpsTagConstants#GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH}) of
-   *          the equator</li>
-   *          <li>east (
-   *          {@link GpsTagConstants#GPS_TAG_GPS_LONGITUDE_REF_VALUE_EAST}) or
-   *          west ({@link GpsTagConstants#GPS_TAG_GPS_LONGITUDE_REF_VALUE_WEST}
-   *          ) of the equator</li>
-   *          </ul>
-   * @return the decimal degree-value for the given input, negative when west of
-   * 0-meridian or south of equator, positive otherwise
-   * @throws IllegalArgumentException if {@code degMinSec} doesn't have length 3 or if {@code ref} is
-   *                  not one of the values mentioned above
-   */
-  public static double degMinSecToDouble(RationalNumber[] degMinSec, String ref) {
-    if (degMinSec == null || degMinSec.length != 3) {
-      throw new IllegalArgumentException("Array's length must be 3.");
-    }
-    for (int i = 0; i < 3; i++) {
-      if (degMinSec[i] == null)
-        throw new IllegalArgumentException("Null value in array.");
+    private StreetsideUtils() {
+        // Private constructor to avoid instantiation
     }
 
-    switch (ref) {
-    case GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_NORTH:
-    case GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH:
-    case GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF_VALUE_EAST:
-    case GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF_VALUE_WEST:
-      break;
-    default:
-      throw new IllegalArgumentException("Invalid ref.");
+    /**
+     * Updates the help text at the bottom of the window.
+     */
+    public static void updateHelpText() {
+        if (MainApplication.getMap() == null || MainApplication.getMap().statusLine == null) {
+            return;
+        }
+        final var ret = new StringBuilder();
+        if (PluginState.isDownloading()) {
+            ret.append(I18n.tr("Downloading Streetside images"));
+        } else if (StreetsideLayer.hasInstance() && !StreetsideLayer.getInstance().getData().getImages().isEmpty()) {
+            ret.append(I18n.tr("Total Streetside images: {0}", StreetsideLayer.getInstance().getToolTipText()));
+        } else {
+            ret.append(I18n.tr("No images found"));
+        }
+        if (StreetsideLayer.hasInstance() && StreetsideLayer.getInstance().mode != null) {
+            ret.append(" — ").append(I18n.tr(StreetsideLayer.getInstance().mode.toString()));
+        }
+        MainApplication.getMap().statusLine.setHelpText(ret.toString());
     }
-
-    double result = degMinSec[0].doubleValue(); // degrees
-    result += degMinSec[1].doubleValue() / 60; // minutes
-    result += degMinSec[2].doubleValue() / 3600; // seconds
-
-    if (GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH.equals(ref)
-        || GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF_VALUE_WEST.equals(ref)) {
-      result *= -1;
-    }
-
-    result = 360 * ((result + 180) / 360 - Math.floor((result + 180) / 360)) - 180;
-    return result;
-  }
-
-  /**
-   * Joins two images into the same sequence. One of them must be the last image of a sequence, the other one the beginning of a different one.
-   *
-   * @param imgA the first image, into whose sequence the images from the sequence of the second image are merged
-   * @param imgB the second image, whose sequence is merged into the sequence of the first image
-   */
-  public static synchronized void join(StreetsideAbstractImage imgA, StreetsideAbstractImage imgB) {
-    if (imgA == null || imgB == null) {
-      throw new IllegalArgumentException("Both images must be non-null for joining.");
-    }
-    if (imgA.getSequence() == imgB.getSequence()) {
-      throw new IllegalArgumentException("You can only join images of different sequences.");
-    }
-    if ((imgA.next() != null || imgB.previous() != null) && (imgB.next() != null || imgA.previous() != null)) {
-      throw new IllegalArgumentException(
-          "You can only join an image at the end of a sequence with one at the beginning of another sequence.");
-    }
-    if (imgA.next() != null || imgB.previous() != null) {
-      join(imgB, imgA);
-    } else {
-      for (StreetsideAbstractImage img : imgB.getSequence().getImages()) {
-        imgA.getSequence().add(img);
-      }
-      StreetsideLayer.invalidateInstance();
-    }
-  }
-
-  /**
-   * Zooms to fit all the {@link StreetsideAbstractImage} objects stored in the
-   * database.
-   */
-  public static void showAllPictures() {
-    showPictures(StreetsideLayer.getInstance().getData().getImages(), false);
-  }
-
-  /**
-   * Zooms to fit all the given {@link StreetsideAbstractImage} objects.
-   *
-   * @param images The images your are zooming to.
-   * @param select Whether the added images must be selected or not.
-   */
-  public static void showPictures(final Set<StreetsideAbstractImage> images, final boolean select) {
-    if (!SwingUtilities.isEventDispatchThread()) {
-      SwingUtilities.invokeLater(() -> showPictures(images, select));
-    } else {
-      Bounds zoomBounds;
-      if (images.isEmpty()) {
-        zoomBounds = new Bounds(new LatLon(0, 0));
-      } else {
-        zoomBounds = new Bounds(images.iterator().next().getMovingLatLon());
-        for (StreetsideAbstractImage img : images) {
-          zoomBounds.extend(img.getMovingLatLon());
-        }
-      }
-
-      // The zoom rectangle must have a minimum size.
-      double latExtent = Math.max(zoomBounds.getMaxLat() - zoomBounds.getMinLat(), MIN_ZOOM_SQUARE_SIDE);
-      double lonExtent = Math.max(zoomBounds.getMaxLon() - zoomBounds.getMinLon(), MIN_ZOOM_SQUARE_SIDE);
-      zoomBounds = new Bounds(zoomBounds.getCenter(), latExtent, lonExtent);
-
-      MainApplication.getMap().mapView.zoomTo(zoomBounds);
-      StreetsideLayer.getInstance().getData().setSelectedImage(null);
-      if (select) {
-        StreetsideLayer.getInstance().getData().addMultiSelectedImage(images);
-      }
-      StreetsideLayer.invalidateInstance();
-    }
-
-  }
-
-  /**
-   * Separates two images belonging to the same sequence. The two images have to be consecutive in the same sequence.
-   * Two new sequences are created and all images up to (and including) either {@code imgA} or {@code imgB}
-   * (whichever appears first in the sequence) are put into the first of the two sequences.
-   * All others are put into the second new sequence.
-   *
-   * @param imgA one of the images marking where to split the sequence
-   * @param imgB the other image marking where to split the sequence, needs to be a direct neighbour of {@code imgA} in the sequence.
-   */
-  public static synchronized void unjoin(StreetsideAbstractImage imgA, StreetsideAbstractImage imgB) {
-    if (imgA == null || imgB == null) {
-      throw new IllegalArgumentException("Both images must be non-null for unjoining.");
-    }
-    if (imgA.getSequence() != imgB.getSequence()) {
-      throw new IllegalArgumentException("You can only unjoin with two images from the same sequence.");
-    }
-    if (imgB.equals(imgA.next()) && imgA.equals(imgB.next())) {
-      throw new IllegalArgumentException(
-          "When unjoining with two images these must be consecutive in one sequence.");
-    }
-
-    if (imgA.equals(imgB.next())) {
-      unjoin(imgB, imgA);
-    } else {
-      StreetsideSequence seqA = new StreetsideSequence();
-      StreetsideSequence seqB = new StreetsideSequence();
-      boolean insideFirstHalf = true;
-      for (StreetsideAbstractImage img : imgA.getSequence().getImages()) {
-        if (insideFirstHalf) {
-          seqA.add(img);
-        } else {
-          seqB.add(img);
-        }
-        if (img.equals(imgA)) {
-          insideFirstHalf = false;
-        }
-      }
-      StreetsideLayer.invalidateInstance();
-    }
-  }
-
-  /**
-   * Updates the help text at the bottom of the window.
-   */
-  public static void updateHelpText() {
-    if (MainApplication.getMap() == null || MainApplication.getMap().statusLine == null) {
-      return;
-    }
-    StringBuilder ret = new StringBuilder();
-    if (PluginState.isDownloading()) {
-      ret.append(I18n.tr("Downloading Streetside images"));
-    } else if (StreetsideLayer.hasInstance() && !StreetsideLayer.getInstance().getData().getImages().isEmpty()) {
-      ret.append(I18n.tr("Total Streetside images: {0}", StreetsideLayer.getInstance().getToolTipText()));
-    } else if (PluginState.isSubmittingChangeset()) {
-      ret.append(I18n.tr("Submitting Streetside Changeset"));
-    } else {
-      ret.append(I18n.tr("No images found"));
-    }
-    if (StreetsideLayer.hasInstance() && StreetsideLayer.getInstance().mode != null) {
-      ret.append(" — ").append(I18n.tr(StreetsideLayer.getInstance().mode.toString()));
-    }
-    if (PluginState.isUploading()) {
-      ret.append(" — ").append(PluginState.getUploadString());
-    }
-    MainApplication.getMap().statusLine.setHelpText(ret.toString());
-  }
 }
