Index: /applications/editors/josm/plugins/MicrosoftStreetside/.classpath
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/.classpath	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/.classpath	(revision 36228)
@@ -28,5 +28,5 @@
 		</attributes>
 	</classpathentry>
-	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17/"/>
 	<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/JOSM"/>
@@ -35,5 +35,4 @@
 	<classpathentry combineaccessrules="false" kind="src" path="/JOSM-apache-commons"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/JOSM-apache-http"/>
-	<classpathentry combineaccessrules="false" kind="src" path="/JOSM-utilsplugin2"/>
 	<classpathentry kind="output" path="bin/default"/>
 </classpath>
Index: /applications/editors/josm/plugins/MicrosoftStreetside/.gitignore
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/.gitignore	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/.gitignore	(revision 36228)
@@ -9,5 +9,4 @@
 javadoc/
 build/
-bin/
 doc/
 logs/
@@ -21,5 +20,2 @@
 # OS X metadata
 .DS_Store
-
-
-/bin/
Index: /applications/editors/josm/plugins/MicrosoftStreetside/CONTRIBUTING.md
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/CONTRIBUTING.md	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/CONTRIBUTING.md	(revision 36228)
@@ -14,5 +14,5 @@
 
 The following format of source code files is preferred in this repository:
-* Indentation with 2 spaces per indentation level
+* Indentation with 4 spaces per indentation level
 * line endings should be UNIX-style line endings (LF)
 * one newline (LF) at the end of the file
Index: /applications/editors/josm/plugins/MicrosoftStreetside/README.md
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/README.md	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/README.md	(revision 36228)
@@ -26,5 +26,5 @@
     gradle build
     
-Now Restart JOSM and activate the MicrosoftStreeside plugin in your preferences. 
+Now Restart JOSM and activate the MicrosoftStreetside plugin in your preferences. 
 The MicrosoftStreetside menu items will appear in the JOSM main menu after JOSM is
 restarted.
@@ -34,5 +34,5 @@
 ## License
 
-This plugin is based on the Mapilary developed by developed and maintained by nokutu (nokutu@openmailbox.org) and extended to display Streetside imagery by Rene Rhodes (renerr18) You can contact Rene on github.
+This plugin is based on the Mapillary developed by developed and maintained by nokutu (nokutu@openmailbox.org) and extended to display Streetside imagery by Rene Rhodes (renerr18) You can contact Rene on GitHub.
 
 This software is licensed under [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). 
@@ -43,3 +43,3 @@
 Java SE 1.8 specification, and may not function properly with alternative JDKs (e.g. OpenJDK is not currently supported). JavaFX is licensed under the same terms as Java SE (http://www.oracle.com/technetwork/java/javase/terms/license/index.html).
 
-The plugin also makes use of the compact Resty libary for communicating with RESTful web services from Java (https://beders.github.io/Resty/Resty/Overview.html). Resty is licensed under the MIT license (https://github.com/go-resty/resty/blob/master/LICENSE).
+Third-party JDKs such as Azul have versions with JavaFX included. Please use those.
Index: /applications/editors/josm/plugins/MicrosoftStreetside/build.gradle
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/build.gradle	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/build.gradle	(revision 36228)
@@ -1,4 +1,6 @@
+import com.github.spotbugs.snom.Confidence
+import com.github.spotbugs.snom.Effort
 import com.github.spotbugs.snom.SpotBugsTask
-import net.ltgt.gradle.errorprone.CheckSeverity
+import org.gradle.api.tasks.testing.logging.TestLogEvent
 
 plugins {
@@ -7,11 +9,12 @@
   id 'jacoco'
   id 'pmd'
-  id("com.github.ben-manes.versions").version("0.49.0")
-  id("net.ltgt.errorprone").version("3.1.0")
-  id("org.kordamp.markdown.convert").version("1.2.0")
-  id("org.sonarqube").version("3.3")
-  id('com.github.spotbugs').version('5.2.3')
-  id('org.openstreetmap.josm').version("0.8.2")
-  id("com.diffplug.spotless").version("6.22.0")
+  alias(libs.plugins.spotless)
+  alias(libs.plugins.versions)
+  alias(libs.plugins.errorprone)
+  alias(libs.plugins.markdown)
+  alias(libs.plugins.javafx)
+  alias(libs.plugins.sonarqube)
+  alias(libs.plugins.spotbugs)
+  alias(libs.plugins.josm)
 }
 
@@ -21,5 +24,5 @@
 //apply from: 'gradle/markdown.gradle'
 
-sourceCompatibility = '1.8'
+sourceCompatibility = '21'
 
 def versionProcess = new ProcessBuilder("git", "describe", "--always", "--dirty").start()
@@ -40,23 +43,17 @@
 }
 
-def versions = [
-  awaitility: "4.2.0",
-  jmockit: "1.49.a",
-  junit: "5.10.1",
-  wiremock: "2.27.2"
-]
+javafx {
+    modules = [ 'javafx.graphics', 'javafx.controls', 'javafx.swing' ]
+}
 
 dependencies {
-  if (!JavaVersion.current().isJava9Compatible()) {
-    errorproneJavac("com.google.errorprone:javac:9+181-r4173-1")
-  }
-  testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${versions.junit}")
-  testImplementation("org.junit.jupiter:junit-jupiter-params:${versions.junit}")
-  testImplementation("org.junit.jupiter:junit-jupiter-api:${versions.junit}")
-  testImplementation("org.junit.vintage:junit-vintage-engine:${versions.junit}")
-  testImplementation ("org.openstreetmap.josm:josm-unittest"){changing=true}
-  testImplementation "com.github.tomakehurst:wiremock:${versions.wiremock}"
-  testImplementation("org.jmockit:jmockit:${versions.jmockit}")
-  testImplementation("org.awaitility:awaitility:${versions.awaitility}")
+  errorprone(libs.errorprone)
+  spotbugsPlugins(libs.findsecbugsPlugin)
+  testRuntimeOnly libs.junit.jupiter.engine
+  testImplementation libs.bundles.junit
+  testImplementation libs.josm.unittest
+  testImplementation libs.wiremock
+  testImplementation libs.jmockit
+  testImplementation libs.awaitility
 }
 
@@ -82,4 +79,13 @@
     }
   }
+}
+
+// Spotbugs config
+spotbugs {
+    toolVersion = libs.versions.spotbugs.get()
+    ignoreFailures = true
+    effort = Effort.valueOf('MAX')
+    reportLevel = Confidence.valueOf('LOW')
+    //sourceSets = [sourceSets.main, sourceSets.test]
 }
 
@@ -133,6 +139,4 @@
 }
 
-import org.gradle.api.tasks.testing.logging.TestLogEvent
-
 test {
   project.afterEvaluate {
Index: /applications/editors/josm/plugins/MicrosoftStreetside/build.xml
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/build.xml	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/build.xml	(revision 36228)
@@ -10,13 +10,12 @@
   <property name="josm" location="../../core/dist/josm-custom.jar"/>
   <property name="plugin.dist.dir" value="../../dist"/>
+  <property name="java.lang.version" value="21"/>
   <!--** include targets that all plugins have in common **-->
   <import file="../build-common.xml"/>
   <fileset id="plugin.requires.jars" dir="${plugin.dist.dir}">
     <include name="apache-commons.jar"/>
-    <include name="apache-http.jar"/>
     <include name="javafx-osx.jar" if:set="isMac"/>
     <include name="javafx-unixoid.jar" if:set="isUnix"/>
     <include name="javafx-windows.jar" if:set="isWindows"/>
-    <include name="utilsplugin2.jar"/>
   </fileset>
   <target name="pre-compile" depends="fetch_dependencies">
Index: /applications/editors/josm/plugins/MicrosoftStreetside/data/streetside.schema.json
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/data/streetside.schema.json	(revision 36228)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/data/streetside.schema.json	(revision 36228)
@@ -0,0 +1,112 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://learn.microsoft.com/en-us/bingmaps/rest-services/imagery/imagery-metadata",
+  "title": "Microsoft Streetside",
+  "description": "Microsoft Streetside Imagery",
+  "type": "object",
+  "properties": {
+    "authenticationResultCode": {
+      "description": "Status code for additional information about authentication success or failure",
+      "type": "string",
+      "enum": [
+        "ValidCredentials",
+        "InvalidCredentials",
+        "CredentialsExpired",
+        "NotAuthorized",
+        "NoCredentials",
+        "None"
+      ]
+    },
+    "brandLogoUri": {
+      "description": "The brand image to use for branding requirements",
+      "type": "string"
+    },
+    "copyright": {
+      "description": "Copyright notice",
+      "type": "string"
+    },
+    "resourceSets": {
+      "description": "A collection of resource set objects",
+      "type": "array",
+      "items": {
+        "type": "object",
+        "properties": {
+          "estimatedTotal": {
+            "description": "An estimate of the total number of resources in the resource set",
+            "type": "integer"
+          },
+          "resources": {
+            "type": "array",
+            "items": {
+              "type": "object",
+              "properties": {
+                "imageUrl": {
+                  "type": "string"
+                },
+                "imageUrlSubdomains": {
+                  "type": "array",
+                  "items": {
+                    "type": "string"
+                  }
+                },
+                "imageWidth": {
+                  "type": "integer"
+                },
+                "imageHeight": {
+                  "type": "integer"
+                },
+                "vintageStart": {
+                  "type": "string"
+                },
+                "vintageEnd": {
+                  "type": "string"
+                },
+                "zoomMin": {
+                  "type": "integer"
+                },
+                "zoomMax": {
+                  "type": "integer"
+                },
+                "lat": {
+                  "description": "The latitude of the image",
+                  "type": "number"
+                },
+                "lon": {
+                  "description": "The longitude of the image",
+                  "type": "number"
+                },
+                "pi": {
+                  "description": "The pitch of the image",
+                  "type": "number"
+                },
+                "ro": {
+                  "description": "The roll of the image",
+                  "type": "number"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "statusCode": {
+      "description": "HTTP status code for the request",
+      "type": "integer"
+    },
+    "statusDescription": {
+      "description": "A description of the HTTP status code",
+      "type": "string"
+    },
+    "traceId": {
+      "description": "Unique identifier for the request",
+      "type": "string"
+    },
+    "errorDetails": {
+      "description": "A collection of error descriptions",
+      "type": "array",
+      "items": {
+        "type": "string"
+      }
+    }
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/gradle.properties
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/gradle.properties	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/gradle.properties	(revision 36228)
@@ -5,14 +5,15 @@
 plugin.icon=images/streetside-logo.svg
 plugin.link=https://github.com/Microsoft/MicrosoftStreetsidePlugin
+plugin.minimum.java.version=21
 
 # Minimum required JOSM version to run this plugin, choose the lowest version possible that is compatible.
 # You can check if the plugin compiles against this version by executing `./gradlew compileJava_minJosm`.
-plugin.main.version=18723
+plugin.main.version=18877
 #plugin.version=
 # Version of JOSM against which the plugin is compiled
 # Please check, if the specified version is available for download from https://josm.openstreetmap.de/download/ .
 # If not, choose the next higher number that is available, or the gradle build will break.
-plugin.compile.version=18724
-plugin.requires=apache-commons;apache-http;javafx;utilsplugin2
+plugin.compile.version=18911
+plugin.requires=apache-commons;javafx
 
 # Character encoding of Gradle files
Index: /applications/editors/josm/plugins/MicrosoftStreetside/gradle/libs.versions.toml
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/gradle/libs.versions.toml	(revision 36228)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/gradle/libs.versions.toml	(revision 36228)
@@ -0,0 +1,44 @@
+[versions]
+awaitility = "4.2.0"
+errorprone = "2.26.1"
+errorpronePlugin = "3.1.0"
+jacoco = "0.8.11"
+javafxplugin = "0.1.0"
+jmockit = "1.49.a"
+josm = "0.8.2"
+junit = "5.10.2"
+markdown = "1.2.0"
+pmd = "6.55.0"
+sonarqube = "4.4.1.3373"
+spotbugs = "4.3.0"
+spotbugsPlugin = "6.0.8"
+spotless = "6.25.0"
+versions = "0.51.0"
+wiremock = "2.27.2"
+findsecbugs = "1.12.0"
+
+[libraries]
+awaitility = { group = "org.awaitility", name = "awaitility", version.ref = "awaitility"}
+errorprone = { group = "com.google.errorprone", name = "error_prone_core", version.ref = "errorprone"}
+findsecbugsPlugin = { group = "com.h3xstream.findsecbugs", name = "findsecbugs-plugin", version.ref = "findsecbugs" }
+jmockit = { group = "org.jmockit", name = "jmockit", version.ref = "jmockit" }
+josm-unittest = { group = "org.openstreetmap.josm", name = "josm-unittest", version = "SNAPSHOT" }
+junit-jupiter-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" }
+junit-jupiter-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" }
+junit-jupiter-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit" }
+junit-vintage-engine = { group = "org.junit.vintage", name = "junit-vintage-engine", version.ref = "junit" }
+wiremock = { group = "com.github.tomakehurst", name = "wiremock", version.ref = "wiremock" }
+
+[bundles]
+junit = ["junit-jupiter-engine", "junit-jupiter-params", "junit-jupiter-api", "junit-vintage-engine"]
+
+
+[plugins]
+errorprone = { id = "net.ltgt.errorprone", version.ref = "errorpronePlugin" }
+javafx = { id  = "org.openjfx.javafxplugin", version.ref = "javafxplugin" }
+josm = { id = "org.openstreetmap.josm", version.ref = "josm" }
+markdown = { id = "org.kordamp.markdown.convert", version.ref = "markdown" }
+sonarqube = { id = "org.sonarqube", version.ref = "sonarqube"}
+spotbugs = { id = "com.github.spotbugs", version.ref = "spotbugsPlugin"}
+spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
+versions = { id = "com.github.ben-manes.versions", version.ref = "versions" }
Index: /applications/editors/josm/plugins/MicrosoftStreetside/gradle/tool-config.gradle
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/gradle/tool-config.gradle	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/gradle/tool-config.gradle	(revision 36228)
@@ -1,41 +1,11 @@
-def pmdVersion = "6.36.0"
-def spotbugsVersion = "4.3.0"
-def jacocoVersion = "0.8.7"
-def errorproneVersion = "2.7.1"
-
-// Set up ErrorProne (currently only for JDK8, until JDK9 is supported)
-dependencies.errorprone "com.google.errorprone:error_prone_core:$errorproneVersion"
-/*
-tasks.withType(JavaCompile) {
-options.compilerArgs += ['-Xep:DefaultCharset:ERROR',
-  '-Xep:ClassCanBeStatic:ERROR',
-  '-Xep:StringEquality:ERROR',
-  '-Xep:MethodCanBeStatic:WARN',
-  '-Xep:RemoveUnusedImports:WARN',
-  '-Xep:PrivateConstructorForUtilityClass:WARN',
-  '-Xep:WildcardImport:WARN',
-  '-Xep:LambdaFunctionalInterface:WARN',
-  '-Xep:ConstantField:WARN']
-}
-*/
-
-// Spotbugs config
-spotbugs {
-  toolVersion = spotbugsVersion
-  ignoreFailures = true
-  effort = "max"
-  reportLevel = "low"
-  //sourceSets = [sourceSets.main, sourceSets.test]
-}
-
 // JaCoCo config
 jacoco {
-  toolVersion = jacocoVersion
+  toolVersion = libs.versions.jacoco.get()
 }
 jacocoTestReport {
   reports {
-    xml.enabled = true
-    html.destination file("$buildDir/reports/jacoco")
+    xml.required = true
   }
+  dependsOn test
 }
 build.dependsOn jacocoTestReport
@@ -43,5 +13,5 @@
 // PMD config
 pmd {
-  toolVersion pmdVersion
+  toolVersion libs.versions.pmd.get()
   ignoreFailures true
   ruleSetConfig = resources.text.fromFile('config/pmd/ruleset.xml')
@@ -50,5 +20,5 @@
 
 // SonarQube config
-sonarqube {
+sonar {
   properties {
     property 'sonar.forceAuthentication', 'true'
Index: /applications/editors/josm/plugins/MicrosoftStreetside/gradle/wrapper/gradle-wrapper.properties
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/gradle/wrapper/gradle-wrapper.properties	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/gradle/wrapper/gradle-wrapper.properties	(revision 36228)
@@ -1,6 +1,8 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionSha256Sum=740c2e472ee4326c33bf75a5c9f5cd1e69ecf3f9b580f6e236c86d1f3d98cfac
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-bin.zip
+distributionSha256Sum=9631d53cf3e74bfa726893aee1f8994fee4e060c401335946dba2156f440f24c
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
Index: /applications/editors/josm/plugins/MicrosoftStreetside/gradlew
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/gradlew	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/gradlew	(revision 36228)
@@ -1,6 +1,6 @@
-#!/usr/bin/env sh
-
-#
-# Copyright 2015 the original author or authors.
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,39 +18,79 @@
 
 ##############################################################################
-##
-##  Gradle start up script for UN*X
-##
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
 ##############################################################################
 
 # Attempt to set APP_HOME
+
 # Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
-    ls=`ls -ld "$PRG"`
-    link=`expr "$ls" : '.*-> \(.*\)$'`
-    if expr "$link" : '/.*' > /dev/null; then
-        PRG="$link"
-    else
-        PRG=`dirname "$PRG"`"/$link"
-    fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
 done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
 
 warn () {
     echo "$*"
-}
+} >&2
 
 die () {
@@ -59,5 +99,5 @@
     echo
     exit 1
-}
+} >&2
 
 # OS specific support (must be 'true' or 'false').
@@ -66,17 +106,9 @@
 darwin=false
 nonstop=false
-case "`uname`" in
-  CYGWIN* )
-    cygwin=true
-    ;;
-  Darwin* )
-    darwin=true
-    ;;
-  MSYS* | MINGW* )
-    msys=true
-    ;;
-  NONSTOP* )
-    nonstop=true
-    ;;
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
 esac
 
@@ -88,7 +120,7 @@
     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
         # IBM's JDK on AIX uses strange locations for the executables
-        JAVACMD="$JAVA_HOME/jre/sh/java"
+        JAVACMD=$JAVA_HOME/jre/sh/java
     else
-        JAVACMD="$JAVA_HOME/bin/java"
+        JAVACMD=$JAVA_HOME/bin/java
     fi
     if [ ! -x "$JAVACMD" ] ; then
@@ -99,87 +131,119 @@
     fi
 else
-    JAVACMD="java"
-    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+    JAVACMD=java
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
 
 Please set the JAVA_HOME variable in your environment to match the
 location of your Java installation."
+    fi
 fi
 
 # Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
-    MAX_FD_LIMIT=`ulimit -H -n`
-    if [ $? -eq 0 ] ; then
-        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
-            MAX_FD="$MAX_FD_LIMIT"
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
         fi
-        ulimit -n $MAX_FD
-        if [ $? -ne 0 ] ; then
-            warn "Could not set maximum file descriptor limit: $MAX_FD"
-        fi
-    else
-        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
-    fi
-fi
-
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
-    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
-
-# For Cygwin or MSYS, switch paths to Windows format before running java
-if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
-    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
-    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
-    JAVACMD=`cygpath --unix "$JAVACMD"`
-
-    # We build the pattern for arguments to be converted via cygpath
-    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
-    SEP=""
-    for dir in $ROOTDIRSRAW ; do
-        ROOTDIRS="$ROOTDIRS$SEP$dir"
-        SEP="|"
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
     done
-    OURCYGPATTERN="(^($ROOTDIRS))"
-    # Add a user-defined pattern to the cygpath arguments
-    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
-        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
-    fi
-    # Now convert the arguments - kludge to limit ourselves to /bin/sh
-    i=0
-    for arg in "$@" ; do
-        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
-        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
-
-        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
-            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
-        else
-            eval `echo args$i`="\"$arg\""
-        fi
-        i=`expr $i + 1`
-    done
-    case $i in
-        0) set -- ;;
-        1) set -- "$args0" ;;
-        2) set -- "$args0" "$args1" ;;
-        3) set -- "$args0" "$args1" "$args2" ;;
-        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
-        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
-        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
-        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
-        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
-        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
-    esac
-fi
-
-# Escape application args
-save () {
-    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
-    echo " "
-}
-APP_ARGS=`save "$@"`
-
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
 
 exec "$JAVACMD" "$@"
Index: /applications/editors/josm/plugins/MicrosoftStreetside/gradlew.bat
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/gradlew.bat	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/gradlew.bat	(revision 36228)
@@ -15,5 +15,5 @@
 @rem
 
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
 @rem ##########################################################################
 @rem
@@ -26,5 +26,6 @@
 
 set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
 set APP_BASE_NAME=%~n0
 set APP_HOME=%DIRNAME%
@@ -41,11 +42,11 @@
 set JAVA_EXE=java.exe
 %JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
+if %ERRORLEVEL% equ 0 goto execute
 
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
 
 goto fail
@@ -57,9 +58,9 @@
 if exist "%JAVA_EXE%" goto execute
 
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
 
 goto fail
@@ -76,11 +77,13 @@
 :end
 @rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
 
 :fail
 rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
 rem the _cmd.exe /c_ return code!
-if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
 
 :mainEnd
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 36227)
+++ /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: plications/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 36227)
+++ 	(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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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: plications/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 36227)
+++ 	(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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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: plications/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 36227)
+++ 	(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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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: plications/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 36227)
+++ 	(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 36227)
+++ /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: plications/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 36227)
+++ 	(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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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: plications/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 36227)
+++ 	(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: plications/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 36227)
+++ 	(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 36227)
+++ /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 36227)
+++ /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: plications/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 36227)
+++ 	(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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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 36227)
+++ /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: plications/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 36227)
+++ 	(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 36227)
+++ /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 36227)
+++ /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: plications/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 36227)
+++ 	(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 36227)
+++ /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 36227)
+++ /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());
-  }
 }
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideAbstractImageTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideAbstractImageTest.java	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideAbstractImageTest.java	(revision 36228)
@@ -2,27 +2,124 @@
 package org.openstreetmap.josm.plugins.streetside;
 
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
+import java.text.MessageFormat;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.data.coor.LatLon;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.openstreetmap.josm.plugins.streetside.cubemap.CubemapUtils;
+import org.openstreetmap.josm.plugins.streetside.utils.TestUtil;
 
 class StreetsideAbstractImageTest {
+    private StreetsideImage image;
+    private String imageUrlBase;
+
+    @BeforeEach
+    void setup() {
+        this.image = TestUtil.generateImage("1013203010232123", 39.065321, -108.553035);
+        this.imageUrlBase = "https://ecn.t0.tiles.virtualearth.net/tiles/hs1013203010232123{0}?"
+                + "g=14336&key=Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU";
+    }
+
     @Test
-    void testIsModified() {
-        StreetsideImage img = new StreetsideImage("key___________________", new LatLon(0, 0), 0);
-        assertFalse(img.isModified());
-        img.turn(1e-4);
-        img.stopMoving();
-        assertTrue(img.isModified());
-        img.turn(-1e-4);
-        img.stopMoving();
-        assertFalse(img.isModified());
-        img.move(1e-4, 1e-4);
-        img.stopMoving();
-        assertTrue(img.isModified());
-        img.move(-1e-4, -1e-4);
-        img.stopMoving();
-        assertFalse(img.isModified());
+    void testThumbnail() {
+        assertEquals(MessageFormat.format(this.imageUrlBase, "01"), this.image.getThumbnail());
+    }
+
+    private static CubeMapTileXY frontCube(int x, int y) {
+        return new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, x, y);
+    }
+
+    @Test
+    void testTilesZoom1() {
+        // 2x2 (4 total tiles)
+        final var z1 = image.getFaceTiles(CubemapUtils.CubemapFaces.FRONT, 1)
+                .collect(Collectors.toMap(p -> p.a, p -> p.b));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "010"), z1.get(frontCube(0, 0)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "011"), z1.get(frontCube(1, 0)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "012"), z1.get(frontCube(0, 1)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "013"), z1.get(frontCube(1, 1)));
+        assertEquals(4, z1.size());
+    }
+
+    @Test
+    void testTilesZoom2() {
+        // 4x4 (16 total tiles)
+        final var z2 = image.getFaceTiles(CubemapUtils.CubemapFaces.FRONT, 2)
+                .collect(Collectors.toMap(p -> p.a, p -> p.b));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "0100"), z2.get(frontCube(0, 0)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "0101"), z2.get(frontCube(1, 0)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "0102"), z2.get(frontCube(0, 1)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "0103"), z2.get(frontCube(1, 1)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "0110"), z2.get(frontCube(2, 0)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "0111"), z2.get(frontCube(3, 0)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "0112"), z2.get(frontCube(2, 1)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "0113"), z2.get(frontCube(3, 1)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "0120"), z2.get(frontCube(0, 2)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "0121"), z2.get(frontCube(1, 2)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "0122"), z2.get(frontCube(0, 3)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "0123"), z2.get(frontCube(1, 3)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "0130"), z2.get(frontCube(2, 2)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "0131"), z2.get(frontCube(3, 2)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "0132"), z2.get(frontCube(2, 3)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "0133"), z2.get(frontCube(3, 3)));
+        assertEquals(16, z2.size());
+    }
+
+    @Test
+    void testTilesZoom3() {
+        // 8x8 (64 total tiles)
+        final var z3 = image.getFaceTiles(CubemapUtils.CubemapFaces.FRONT, 3)
+                .collect(Collectors.toMap(p -> p.a, p -> p.b));
+        // Just check the first row to keep test size smallish
+        assertEquals(MessageFormat.format(this.imageUrlBase, "01000"), z3.get(frontCube(0, 0)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "01001"), z3.get(frontCube(1, 0)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "01010"), z3.get(frontCube(2, 0)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "01011"), z3.get(frontCube(3, 0)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "01100"), z3.get(frontCube(4, 0)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "01101"), z3.get(frontCube(5, 0)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "01110"), z3.get(frontCube(6, 0)));
+        assertEquals(MessageFormat.format(this.imageUrlBase, "01111"), z3.get(frontCube(7, 0)));
+        assertEquals(64, z3.size());
+    }
+
+    static Stream<Arguments> testQuadKeyToXY() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        // 2x2
+        builder.add(Arguments.of("0", 0, 0));
+        builder.add(Arguments.of("1", 1, 0));
+        builder.add(Arguments.of("2", 0, 1));
+        builder.add(Arguments.of("3", 1, 1));
+        // 4x4
+        builder.add(Arguments.of("00", 0, 0));
+        builder.add(Arguments.of("01", 1, 0));
+        builder.add(Arguments.of("02", 0, 1));
+        builder.add(Arguments.of("03", 1, 1));
+        builder.add(Arguments.of("10", 2, 0));
+        builder.add(Arguments.of("11", 3, 0));
+        builder.add(Arguments.of("12", 2, 1));
+        builder.add(Arguments.of("13", 3, 1));
+        builder.add(Arguments.of("20", 0, 2));
+        builder.add(Arguments.of("21", 1, 2));
+        builder.add(Arguments.of("22", 0, 3));
+        builder.add(Arguments.of("23", 1, 3));
+        builder.add(Arguments.of("30", 2, 2));
+        builder.add(Arguments.of("31", 3, 2));
+        builder.add(Arguments.of("32", 2, 3));
+        builder.add(Arguments.of("33", 3, 3));
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testQuadKeyToXY(String quadKey, int x, int y) {
+        final var xy = StreetsideAbstractImage.quadKeyToTile(quadKey);
+        assertAll(() -> assertEquals(x, xy.getXIndex(), "x"), () -> assertEquals(y, xy.getYIndex(), "y"));
     }
 }
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideDataTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideDataTest.java	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideDataTest.java	(revision 36228)
@@ -12,5 +12,5 @@
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.plugins.streetside.utils.TestUtil;
 import org.openstreetmap.josm.testutils.annotations.Main;
 
@@ -31,19 +31,16 @@
 
     /**
-     * Creates a sample {@link StreetsideData} objects, 4 {@link StreetsideImage}
-     * objects and a {@link StreetsideSequence} object.
+     * Creates a sample {@link StreetsideData} object and 4 {@link StreetsideImage}
+     * objects.
      */
     @BeforeEach
     public void setUp() {
-        img1 = new StreetsideImage("id1__________________", new LatLon(0.1, 0.1), 90);
-        img2 = new StreetsideImage("id2__________________", new LatLon(0.2, 0.2), 90);
-        img3 = new StreetsideImage("id3__________________", new LatLon(0.3, 0.3), 90);
-        img4 = new StreetsideImage("id4__________________", new LatLon(0.4, 0.4), 90);
-        final StreetsideSequence seq = new StreetsideSequence();
-
-        seq.add(Arrays.asList(img1, img2, img3, img4));
+        img1 = TestUtil.generateImage("1", 0.1, 0.1);
+        img2 = TestUtil.generateImage("2", 0.2, 0.2);
+        img3 = TestUtil.generateImage("3", 0.3, 0.3);
+        img4 = TestUtil.generateImage("4", 0.4, 0.4);
 
         data = new StreetsideData();
-        data.addAll(new ConcurrentSkipListSet<>(seq.getImages()));
+        data.addAll(Arrays.asList(img1, img2, img3, img4));
     }
 
@@ -72,10 +69,10 @@
     void testSize() {
         assertEquals(4, data.getImages().size());
-        data.add(new StreetsideImage("id5__________________", new LatLon(0.1, 0.1), 90));
+        data.add(TestUtil.generateImage("5", 0.1, 0.1));
         assertEquals(5, data.getImages().size());
     }
 
     /**
-     * Test the {@link StreetsideData#setHighlightedImage(StreetsideAbstractImage)}
+     * Test the {@link StreetsideData#setHighlightedImage(StreetsideImage)}
      * and {@link StreetsideData#getHighlightedImage()} methods.
      */
@@ -137,19 +134,3 @@
         assertThrows(IllegalStateException.class, data::selectPrevious);
     }
-
-    /**
-     * Test the multiselection of images. When a new image is selected, the
-     * multiselected List should reset.
-     */
-    @Disabled("The imgs have non-int identifiers while the code expects the identifiers to be int in string form")
-    @Test
-    void testMultiSelect() {
-        assertEquals(0, data.getMultiSelectedImages().size());
-        data.setSelectedImage(img1);
-        assertEquals(1, data.getMultiSelectedImages().size());
-        data.addMultiSelectedImage(img2);
-        assertEquals(2, data.getMultiSelectedImages().size());
-        data.setSelectedImage(img1);
-        assertEquals(1, data.getMultiSelectedImages().size());
-    }
 }
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideLayerTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideLayerTest.java	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideLayerTest.java	(revision 36228)
@@ -10,9 +10,7 @@
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.condition.DisabledIf;
-import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.imagery.ImageryInfo;
 import org.openstreetmap.josm.gui.layer.ImageryLayer;
 import org.openstreetmap.josm.gui.layer.Layer;
-import org.openstreetmap.josm.plugins.streetside.cubemap.CubemapUtils;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
@@ -46,25 +44,4 @@
 
     @Test
-    void testSetVisible() {
-        StreetsideLayer.getInstance().getData()
-                .add(new StreetsideImage(CubemapUtils.TEST_IMAGE_ID, new LatLon(0.0, 0.0), 0.0));
-        StreetsideLayer.getInstance().getData()
-                .add(new StreetsideImage(CubemapUtils.TEST_IMAGE_ID, new LatLon(0.0, 0.0), 0.0));
-        StreetsideImage invisibleImage = new StreetsideImage(CubemapUtils.TEST_IMAGE_ID, new LatLon(0.0, 0.0), 0.0);
-        invisibleImage.setVisible(false);
-        StreetsideLayer.getInstance().getData().add(invisibleImage);
-
-        StreetsideLayer.getInstance().setVisible(false);
-        for (StreetsideAbstractImage img : StreetsideLayer.getInstance().getData().getImages()) {
-            assertFalse(img.isVisible());
-        }
-
-        StreetsideLayer.getInstance().setVisible(true);
-        for (StreetsideAbstractImage img : StreetsideLayer.getInstance().getData().getImages()) {
-            assertTrue(img.isVisible());
-        }
-    }
-
-    @Test
     void testGetInfoComponent() {
         Object comp = StreetsideLayer.getInstance().getInfoComponent();
Index: plications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideSequenceTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideSequenceTest.java	(revision 36227)
+++ 	(revision )
@@ -1,65 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.streetside;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-import java.util.Arrays;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.data.coor.LatLon;
-
-/**
- * Tests for the {@link StreetsideSequence} class.
- *
- * @author nokutu
- * @see StreetsideSequence
- */
-class StreetsideSequenceTest {
-
-    private final StreetsideImage img1 = new StreetsideImage("key1", new LatLon(0.1, 0.1), 90);
-    private final StreetsideImage img2 = new StreetsideImage("key2", new LatLon(0.2, 0.2), 90);
-    private final StreetsideImage img3 = new StreetsideImage("key3", new LatLon(0.3, 0.3), 90);
-    private final StreetsideImage img4 = new StreetsideImage("key4", new LatLon(0.4, 0.4), 90);
-    private final StreetsideImage imgWithoutSeq = new StreetsideImage("key5", new LatLon(0.5, 0.5), 90);
-    private final StreetsideSequence seq = new StreetsideSequence();
-
-    /**
-     * Creates 4 {@link StreetsideImage} objects and puts them in a
-     * {@link StreetsideSequence} object.
-     */
-    @BeforeEach
-    public void setUp() {
-        seq.add(Arrays.asList(img1, img2, img3, img4));
-    }
-
-    /**
-     * Tests the {@link StreetsideSequence#next(StreetsideAbstractImage)} and
-     * {@link StreetsideSequence#previous(StreetsideAbstractImage)}.
-     */
-    @Test
-    void testNextAndPrevious() {
-        assertEquals(img2, img1.next());
-        assertEquals(img1, img2.previous());
-        assertEquals(img3, img2.next());
-        assertEquals(img2, img3.previous());
-        assertEquals(img4, img3.next());
-        assertEquals(img3, img4.previous());
-
-        assertNull(img4.next());
-        assertNull(img1.previous());
-
-        assertNull(imgWithoutSeq.next());
-        assertNull(imgWithoutSeq.previous());
-
-        // Test IllegalArgumentException when asking for the next image of an image
-        // that is not in the sequence.
-        assertThrows(IllegalArgumentException.class, () -> seq.next(imgWithoutSeq));
-
-        // Test IllegalArgumentException when asking for the previous image of an
-        // image that is not in the sequence.
-        assertThrows(IllegalArgumentException.class, () -> seq.previous(imgWithoutSeq));
-    }
-}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cache/StreetsideCacheTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cache/StreetsideCacheTest.java	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cache/StreetsideCacheTest.java	(revision 36228)
@@ -4,8 +4,6 @@
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
 
 import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.plugins.streetside.cache.StreetsideCache.Type;
 import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
 
@@ -15,5 +13,5 @@
     @Test
     void testCache() {
-        StreetsideCache cache = new StreetsideCache("00000", Type.FULL_IMAGE);
+        StreetsideCache cache = new StreetsideCache("https://ecn.t0.tiles.virtualearth.net/tiles/hs101320223333223201");
         assertNotNull(cache.getUrl());
         assertNotNull(cache.getCacheKey());
@@ -21,11 +19,7 @@
         assertFalse(cache.isObjectLoadable());
 
-        cache = new StreetsideCache("00000", Type.THUMBNAIL);
+        cache = new StreetsideCache("https://ecn.t0.tiles.virtualearth.net/tiles/hs101320223333223201");
         assertNotNull(cache.getCacheKey());
         assertNotNull(cache.getUrl());
-
-        cache = new StreetsideCache(null, null);
-        assertNull(cache.getCacheKey());
-        assertNull(cache.getUrl());
     }
 }
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapUtilsTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapUtilsTest.java	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapUtilsTest.java	(revision 36228)
@@ -4,5 +4,4 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
@@ -28,74 +27,3 @@
         assertEquals("680931568", res);
     }
-
-    @Disabled
-    @Test
-    void testGetFaceNumberForCount() {
-        String faceNrFront = CubemapUtils.getFaceNumberForCount(0);
-        String faceNrRight = CubemapUtils.getFaceNumberForCount(1);
-        String faceNrBack = CubemapUtils.getFaceNumberForCount(2);
-        String faceNrLeft = CubemapUtils.getFaceNumberForCount(3);
-        String faceNrUp = CubemapUtils.getFaceNumberForCount(4);
-        String faceNrDown = CubemapUtils.getFaceNumberForCount(5);
-        assertEquals(faceNrFront, "01");
-        assertEquals(faceNrRight, "02");
-        assertEquals(faceNrBack, "03");
-        assertEquals(faceNrLeft, "10");
-        assertEquals(faceNrUp, "11");
-        assertEquals(faceNrDown, "12");
-    }
-
-    @Disabled
-    @Test
-    void testGetCount4FaceNumber() {
-        int count4Front = CubemapUtils.getCount4FaceNumber("01");
-        int count4Right = CubemapUtils.getCount4FaceNumber("02");
-        int count4Back = CubemapUtils.getCount4FaceNumber("03");
-        int count4Left = CubemapUtils.getCount4FaceNumber("10");
-        int count4Up = CubemapUtils.getCount4FaceNumber("11");
-        int count4Down = CubemapUtils.getCount4FaceNumber("12");
-        assertEquals(count4Front, 0);
-        assertEquals(count4Right, 1);
-        assertEquals(count4Back, 2);
-        assertEquals(count4Left, 3);
-        assertEquals(count4Up, 4);
-        assertEquals(count4Down, 5);
-    }
-
-    @Test
-    void testConvertDoubleCountNrto16TileNr() {
-        String x0y0 = CubemapUtils.convertDoubleCountNrto16TileNr("00");
-        String x0y1 = CubemapUtils.convertDoubleCountNrto16TileNr("01");
-        String x0y2 = CubemapUtils.convertDoubleCountNrto16TileNr("02");
-        String x0y3 = CubemapUtils.convertDoubleCountNrto16TileNr("03");
-        String x1y0 = CubemapUtils.convertDoubleCountNrto16TileNr("10");
-        String x1y1 = CubemapUtils.convertDoubleCountNrto16TileNr("11");
-        String x1y2 = CubemapUtils.convertDoubleCountNrto16TileNr("12");
-        String x1y3 = CubemapUtils.convertDoubleCountNrto16TileNr("13");
-        String x2y0 = CubemapUtils.convertDoubleCountNrto16TileNr("20");
-        String x2y1 = CubemapUtils.convertDoubleCountNrto16TileNr("21");
-        String x2y2 = CubemapUtils.convertDoubleCountNrto16TileNr("22");
-        String x2y3 = CubemapUtils.convertDoubleCountNrto16TileNr("23");
-        String x3y0 = CubemapUtils.convertDoubleCountNrto16TileNr("30");
-        String x3y1 = CubemapUtils.convertDoubleCountNrto16TileNr("31");
-        String x3y2 = CubemapUtils.convertDoubleCountNrto16TileNr("32");
-        String x3y3 = CubemapUtils.convertDoubleCountNrto16TileNr("33");
-
-        assertEquals(x0y0, "00");
-        assertEquals(x0y1, "01");
-        assertEquals(x0y2, "10");
-        assertEquals(x0y3, "11");
-        assertEquals(x1y0, "02");
-        assertEquals(x1y1, "03");
-        assertEquals(x1y2, "12");
-        assertEquals(x1y3, "13");
-        assertEquals(x2y0, "20");
-        assertEquals(x2y1, "21");
-        assertEquals(x2y2, "30");
-        assertEquals(x2y3, "31");
-        assertEquals(x3y0, "22");
-        assertEquals(x3y1, "23");
-        assertEquals(x3y2, "32");
-        assertEquals(x3y3, "33");
-    }
 }
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/TileDownloadingTaskTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/TileDownloadingTaskTest.java	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/TileDownloadingTaskTest.java	(revision 36228)
@@ -7,4 +7,5 @@
 import java.util.List;
 import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -13,4 +14,6 @@
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.plugins.streetside.CubeMapTileXY;
+import org.openstreetmap.josm.plugins.streetside.utils.TestUtil;
 
 @Disabled
@@ -18,10 +21,12 @@
 
     @Test
-    final void testCall() throws InterruptedException {
-        ExecutorService pool = Executors.newFixedThreadPool(1);
-        List<Callable<List<String>>> tasks = new ArrayList<>(1);
-        tasks.add(new TileDownloadingTask("2202112030033001233"));
-        List<Future<List<String>>> results = pool.invokeAll(tasks);
-        assertEquals(results.get(0), "2202112030033001233");
+    final void testCall() throws InterruptedException, ExecutionException {
+        try (ExecutorService pool = Executors.newFixedThreadPool(1)) {
+            List<Callable<List<String>>> tasks = new ArrayList<>(1);
+            tasks.add(new TileDownloadingTask(TestUtil.generateImage("2202112030033001233", 0, 0),
+                    CubemapUtils.CubemapFaces.FRONT, new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 0, 0)));
+            List<Future<List<String>>> results = pool.invokeAll(tasks);
+            assertEquals("2202112030033001233", results.get(0).get().get(0));
+        }
     }
 }
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/gui/ImageDisplayTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/gui/ImageDisplayTest.java	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/gui/ImageDisplayTest.java	(revision 36228)
@@ -25,5 +25,5 @@
     void testImagePersistence() {
         StreetsideImageDisplay display = new StreetsideImageDisplay();
-        display.setImage(DUMMY_IMAGE, null);
+        display.setImage(DUMMY_IMAGE);
         assertEquals(DUMMY_IMAGE, display.getImage());
     }
@@ -44,5 +44,5 @@
         display.getMouseWheelListeners()[0].mouseWheelMoved(dummyScroll);
 
-        display.setImage(DUMMY_IMAGE, null);
+        display.setImage(DUMMY_IMAGE);
 
         display.getMouseWheelListeners()[0].mouseWheelMoved(dummyScroll);
@@ -72,5 +72,5 @@
             display.getMouseListeners()[0].mouseClicked(dummyClick);
 
-            display.setImage(DUMMY_IMAGE, null);
+            display.setImage(DUMMY_IMAGE);
 
             display.getMouseListeners()[0].mouseClicked(dummyClick);
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/gui/StreetsidePreferenceSettingTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/gui/StreetsidePreferenceSettingTest.java	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/gui/StreetsidePreferenceSettingTest.java	(revision 36228)
@@ -6,4 +6,5 @@
 
 import java.awt.GraphicsEnvironment;
+import java.util.Objects;
 
 import javax.swing.JCheckBox;
@@ -99,11 +100,12 @@
             settings.ok();
             assertEquals(new StringProperty("streetside.download-mode", "default").get(),
-                    DOWNLOAD_MODE.fromLabel(((JComboBox<String>) getPrivateFieldValue(settings, "downloadModeComboBox"))
-                            .getSelectedItem().toString()).getPrefId());
+                    DOWNLOAD_MODE.fromLabel(Objects.requireNonNull(((JComboBox<String>) getPrivateFieldValue(settings, "downloadModeComboBox"))
+                            .getSelectedItem()).toString()).getPrefId());
         }
     }
 
     /**
-     * Checks, if a certain {@link BooleanProperty} (identified by the {@code propName} attribute) matches the selected-state of the given {@link JCheckBox}
+     * Checks, if a certain {@link BooleanProperty} (identified by the {@code propName} attribute)
+     * matches the selected-state of the given {@link JCheckBox}
      * @param cb the {@link JCheckBox}, which should be checked against the {@link BooleanProperty}
      * @param propName the name of the property against which the selected-state of the given {@link JCheckBox} should be checked
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/io/download/SequenceDownloadRunnableTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/io/download/SequenceDownloadRunnableTest.java	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/io/download/SequenceDownloadRunnableTest.java	(revision 36228)
@@ -3,9 +3,4 @@
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import java.lang.reflect.Field;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.function.Function;
 
 import org.junit.jupiter.api.Disabled;
@@ -20,46 +15,34 @@
 class SequenceDownloadRunnableTest {
 
-    private static final Function<Bounds, URL> SEARCH_SEQUENCES_URL_GEN = b -> {
-        return SequenceDownloadRunnableTest.class.getResource("/api/v3/responses/searchSequences.json");
-    };
-    private Field urlGenField;
-
     @Test
-    void testRun1() throws IllegalArgumentException, IllegalAccessException {
-        testNumberOfDecodedImages(4, SEARCH_SEQUENCES_URL_GEN, new Bounds(7.246497, 16.432955, 7.249027, 16.432976));
+    void testRun1() throws IllegalArgumentException {
+        testNumberOfDecodedImages(4, new Bounds(7.246497, 16.432955, 7.249027, 16.432976));
     }
 
     @Test
-    void testRun2() throws IllegalArgumentException, IllegalAccessException {
-        testNumberOfDecodedImages(0, SEARCH_SEQUENCES_URL_GEN, new Bounds(0, 0, 0, 0));
+    void testRun2() throws IllegalArgumentException {
+        testNumberOfDecodedImages(0, new Bounds(0, 0, 0, 0));
     }
 
     @Test
-    void testRun3() throws IllegalArgumentException, IllegalAccessException {
-        testNumberOfDecodedImages(0, b -> {
-            try {
-                return new URL("https://streetside/nonexistentURL");
-            } catch (MalformedURLException e) {
-                return null;
-            }
-        }, new Bounds(0, 0, 0, 0));
+    void testRun3() throws IllegalArgumentException {
+        testNumberOfDecodedImages(0, new Bounds(0, 0, 0, 0));
     }
 
     @Test
-    void testRun4() throws IllegalArgumentException, IllegalAccessException {
+    void testRun4() throws IllegalArgumentException {
         StreetsideProperties.CUT_OFF_SEQUENCES_AT_BOUNDS.put(true);
-        testNumberOfDecodedImages(4, SEARCH_SEQUENCES_URL_GEN, new Bounds(7.246497, 16.432955, 7.249027, 16.432976));
+        testNumberOfDecodedImages(4, new Bounds(7.246497, 16.432955, 7.249027, 16.432976));
     }
 
     @Test
-    void testRun5() throws IllegalArgumentException, IllegalAccessException {
+    void testRun5() throws IllegalArgumentException {
         StreetsideProperties.CUT_OFF_SEQUENCES_AT_BOUNDS.put(true);
-        testNumberOfDecodedImages(0, SEARCH_SEQUENCES_URL_GEN, new Bounds(0, 0, 0, 0));
+        testNumberOfDecodedImages(0, new Bounds(0, 0, 0, 0));
     }
 
-    private void testNumberOfDecodedImages(int expectedNumImgs, Function<Bounds, URL> urlGen, Bounds bounds)
-            throws IllegalArgumentException, IllegalAccessException {
+    private void testNumberOfDecodedImages(int expectedNumImgs, Bounds bounds)
+            throws IllegalArgumentException {
         SequenceDownloadRunnable r = new SequenceDownloadRunnable(StreetsideLayer.getInstance().getData(), bounds);
-        urlGenField.set(null, urlGen);
         r.run();
         assertEquals(expectedNumImgs, StreetsideLayer.getInstance().getData().getImages().size());
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/GraphicsUtilsTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/GraphicsUtilsTest.java	(revision 36228)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/GraphicsUtilsTest.java	(revision 36228)
@@ -0,0 +1,42 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.utils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.openstreetmap.josm.plugins.streetside.CubeMapTileXY;
+import org.openstreetmap.josm.plugins.streetside.cubemap.CubemapUtils;
+
+class GraphicsUtilsTest {
+
+    static List<Arguments> testCubeMapTileToIndex() {
+        return Arrays.asList(Arguments.of(new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 0, 0), 3, 0),
+                Arguments.of(new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 1, 0), 3, 1),
+                Arguments.of(new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 2, 0), 3, 2),
+                Arguments.of(new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 3, 0), 3, 3),
+                Arguments.of(new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 4, 0), 3, 4),
+                Arguments.of(new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 5, 0), 3, 5),
+                Arguments.of(new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 6, 0), 3, 6),
+                Arguments.of(new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 7, 0), 3, 7),
+                Arguments.of(new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 0, 1), 3, 8),
+                Arguments.of(new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 1, 1), 3, 9),
+                Arguments.of(new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 2, 1), 3, 10),
+                Arguments.of(new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 3, 1), 3, 11),
+                Arguments.of(new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 4, 1), 3, 12),
+                Arguments.of(new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 5, 1), 3, 13),
+                Arguments.of(new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 6, 1), 3, 14),
+                Arguments.of(new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 7, 1), 3, 15),
+                Arguments.of(new CubeMapTileXY(CubemapUtils.CubemapFaces.FRONT, 0, 2), 3, 16));
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testCubeMapTileToIndex(CubeMapTileXY tile, int zoom, int expectedIndex) {
+        assertEquals(expectedIndex, GraphicsUtils.cubeMapTileToIndex(tile, zoom));
+    }
+}
Index: plications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/JsonUtil.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/JsonUtil.java	(revision 36227)
+++ 	(revision )
@@ -1,18 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.streetside.utils;
-
-import java.io.ByteArrayInputStream;
-import java.nio.charset.StandardCharsets;
-
-import jakarta.json.Json;
-import jakarta.json.JsonObject;
-
-public final class JsonUtil {
-    private JsonUtil() {
-        // Private constructor to avoid instantiation
-    }
-
-    public static JsonObject string2jsonObject(String s) {
-        return Json.createReader(new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8))).readObject();
-    }
-}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/PluginStateTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/PluginStateTest.java	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/PluginStateTest.java	(revision 36228)
@@ -2,13 +2,8 @@
 package org.openstreetmap.josm.plugins.streetside.utils;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import javax.swing.JOptionPane;
-
 import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.TestUtils;
-import org.openstreetmap.josm.testutils.mockers.JOptionPaneSimpleMocker;
 
 /**
@@ -35,25 +30,3 @@
         assertFalse(PluginState.isDownloading());
     }
-
-    /**
-     * Tests the methods related to the upload.
-     */
-    @Test
-    void testUpload() {
-        TestUtils.assumeWorkingJMockit();
-        JOptionPaneSimpleMocker jopsMocker = new JOptionPaneSimpleMocker();
-        jopsMocker.getMockResultMap().put("You have successfully uploaded 2 images to Bing.com", JOptionPane.OK_OPTION);
-        assertFalse(PluginState.isUploading());
-        PluginState.addImagesToUpload(2);
-        assertEquals(2, PluginState.getImagesToUpload());
-        assertEquals(0, PluginState.getImagesUploaded());
-        assertTrue(PluginState.isUploading());
-        PluginState.imageUploaded();
-        assertEquals(1, PluginState.getImagesUploaded());
-        assertTrue(PluginState.isUploading());
-        PluginState.imageUploaded();
-        assertFalse(PluginState.isUploading());
-        assertEquals(2, PluginState.getImagesToUpload());
-        assertEquals(2, PluginState.getImagesUploaded());
-    }
 }
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsideURLTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsideURLTest.java	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsideURLTest.java	(revision 36228)
@@ -5,9 +5,11 @@
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.net.MalformedURLException;
-import java.net.URL;
+import java.net.URI;
+import java.net.URISyntaxException;
 
 import org.junit.jupiter.api.Assertions;
@@ -16,46 +18,22 @@
 
 class StreetsideURLTest {
-    // TODO: replace with Streetside URL @rrh
-    private static final String CLIENT_ID_QUERY_PART = "client_id=T1Fzd20xZjdtR0s1VDk5OFNIOXpYdzoxNDYyOGRkYzUyYTFiMzgz";
-
-    public static class APIv3 {
-
-        /*@Ignore
-        @Test
-        public void testSearchDetections() {
-          assertUrlEquals(StreetsideURL.APIv3.searchDetections(null), "https://a.streetside.com/v3/detections", CLIENT_ID_QUERY_PART);
-        }
-
-        @Ignore
-        @Test
-        public void testSearchImages() {
-          assertUrlEquals(StreetsideURL.APIv3.searchImages(null), "https://a.streetside.com/v3/images", CLIENT_ID_QUERY_PART);
-        }
-
-        @Ignore
-        @Test
-        public void testSubmitChangeset() throws MalformedURLException {
-          assertEquals(
-        new URL("https://a.streetside.com/v3/changesets?" + CLIENT_ID_QUERY_PART),
-        StreetsideURL.APIv3.submitChangeset()
-          );
-        }*/
+    @Test
+    void testParseNextFromHeaderValue() throws MalformedURLException {
+        String headerVal = "<https://a.streetside.com/v3/sequences?page=1&per_page=200"
+                + "&client_id=TG1sUUxGQlBiYWx2V05NM0pQNUVMQTo2NTU3NTBiNTk1NzM1Y2U2>; rel=\"first\", "
+                + "<https://a.streetside.com/v3/sequences?page=2&per_page=200"
+                + "&client_id=TG1sUUxGQlBiYWx2V05NM0pQNUVMQTo2NTU3NTBiNTk1NzM1Y2U2>; rel=\"prev\", "
+                + "<https://a.streetside.com/v3/sequences?page=4&per_page=200"
+                + "&client_id=TG1sUUxGQlBiYWx2V05NM0pQNUVMQTo2NTU3NTBiNTk1NzM1Y2U2>; rel=\"next\"";
+        assertEquals(URI.create(
+                "https://a.streetside.com/v3/sequences?page=4&per_page=200&client_id=TG1sUUxGQlBiYWx2V05NM0pQNUVMQTo2NTU3NTBiNTk1NzM1Y2U2")
+                .toURL(), StreetsideURL.APIv3.parseNextFromLinkHeaderValue(headerVal));
     }
 
     @Test
-    void testParseNextFromHeaderValue() throws MalformedURLException {
-        String headerVal = "<https://a.streetside.com/v3/sequences?page=1&per_page=200&client_id=TG1sUUxGQlBiYWx2V05NM0pQNUVMQTo2NTU3NTBiNTk1NzM1Y2U2>; rel=\"first\", "
-                + "<https://a.streetside.com/v3/sequences?page=2&per_page=200&client_id=TG1sUUxGQlBiYWx2V05NM0pQNUVMQTo2NTU3NTBiNTk1NzM1Y2U2>; rel=\"prev\", "
-                + "<https://a.streetside.com/v3/sequences?page=4&per_page=200&client_id=TG1sUUxGQlBiYWx2V05NM0pQNUVMQTo2NTU3NTBiNTk1NzM1Y2U2>; rel=\"next\"";
-        assertEquals(new URL(
-                "https://a.streetside.com/v3/sequences?page=4&per_page=200&client_id=TG1sUUxGQlBiYWx2V05NM0pQNUVMQTo2NTU3NTBiNTk1NzM1Y2U2"),
-                StreetsideURL.APIv3.parseNextFromLinkHeaderValue(headerVal));
-    }
-
-    @Test
-    void testParseNextFromHeaderValue2() throws MalformedURLException {
+    void testParseNextFromHeaderValue2() throws MalformedURLException, URISyntaxException {
         String headerVal = "<https://urlFirst>; rel=\"first\", " + "rel = \"next\" ; < ; , "
                 + "rel = \"next\" ; <https://urlNext> , " + "<https://urlPrev>; rel=\"prev\"";
-        assertEquals(new URL("https://urlNext"), StreetsideURL.APIv3.parseNextFromLinkHeaderValue(headerVal));
+        assertEquals(new URI("https://urlNext").toURL(), StreetsideURL.APIv3.parseNextFromLinkHeaderValue(headerVal));
     }
 
@@ -70,18 +48,10 @@
     }
 
-    /*public static class Cloudfront {
-    @Ignore
-    @Test
-    public void testThumbnail() {
-      assertUrlEquals(StreetsideURL.VirtualEarth.streetsideTile("arbitrary_key", true), "https://d1cuyjsrcm0gby.cloudfront.net/arbitrary_key/thumb-2048.jpg");
-      assertUrlEquals(StreetsideURL.VirtualEarth.streetsideTile("arbitrary_key2", false), "https://d1cuyjsrcm0gby.cloudfront.net/arbitrary_key2/thumb-320.jpg");
-    }
-    }*/
-
     @Disabled
     @Test
     void testBrowseImageURL() throws MalformedURLException {
-        assertEquals(new URL("https://www.streetside.com/map/im/1234567890123456789012"),
-                StreetsideURL.MainWebsite.browseImage("1234567890123456789012"));
+        fail("Needs editing for MS Streetside");
+        // assertEquals(new URL("https://www.streetside.com/map/im/1234567890123456789012"),
+        // StreetsideURL.MainWebsite.browseImage("1234567890123456789012"));
     }
 
@@ -91,51 +61,4 @@
     }
 
-    @Disabled
-    @Test
-    void testConnectURL() {
-        /*assertUrlEquals(
-        StreetsideURL.MainWebsite.connect("http://redirect-host/ä"),
-        "https://www.streetside.com/connect",
-        CLIENT_ID_QUERY_PART,
-        "scope=user%3Aread+public%3Aupload+public%3Awrite",
-        "response_type=token",
-        "redirect_uri=http%3A%2F%2Fredirect-host%2F%C3%A4"
-        );
-
-        assertUrlEquals(
-        StreetsideURL.MainWebsite.connect(null),
-        "https://www.streetside.com/connect",
-        CLIENT_ID_QUERY_PART,
-        "scope=user%3Aread+public%3Aupload+public%3Awrite",
-        "response_type=token"
-        );
-
-        assertUrlEquals(
-        StreetsideURL.MainWebsite.connect(""),
-        "https://www.streetside.com/connect",
-        CLIENT_ID_QUERY_PART,
-        "scope=user%3Aread+public%3Aupload+public%3Awrite",
-        "response_type=token"
-        );*/
-    }
-
-    @Disabled
-    @Test
-    void testUploadSecretsURL() throws MalformedURLException {
-        /*assertEquals(
-        new URL("https://a.streetside.com/v2/me/uploads/secrets?"+CLIENT_ID_QUERY_PART),
-        StreetsideURL.uploadSecretsURL()
-        );*/
-    }
-
-    @Disabled
-    @Test
-    void testUserURL() throws MalformedURLException {
-        /*assertEquals(
-        new URL("https://a.streetside.com/v3/me?"+CLIENT_ID_QUERY_PART),
-        StreetsideURL.APIv3.userURL()
-        );*/
-    }
-
     @Test
     void testString2MalformedURL() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
@@ -143,6 +66,8 @@
         Method method = StreetsideURL.class.getDeclaredMethod("string2URL", String[].class);
         method.setAccessible(true);
-        Assertions.assertNull(method.invoke(null, new Object[] { new String[] { "malformed URL" } })); // this simply invokes string2URL("malformed URL")
-        Assertions.assertNull(method.invoke(null, new Object[] { null })); // invokes string2URL(null)
+        // this simply invokes string2URL("malformed URL")
+        Assertions.assertNull(method.invoke(null, (Object) new String[] { "malformed URL" }));
+        // invokes string2URL(null)
+        Assertions.assertNull(method.invoke(null, (Object) null));
     }
 
@@ -151,23 +76,5 @@
         TestUtil.testUtilityClass(StreetsideURL.class);
         TestUtil.testUtilityClass(StreetsideURL.APIv3.class);
-        TestUtil.testUtilityClass(StreetsideURL.VirtualEarth.class);
         TestUtil.testUtilityClass(StreetsideURL.MainWebsite.class);
     }
-
-    private static void assertUrlEquals(URL actualUrl, String expectedBaseUrl, String... expectedParams) {
-        final String actualUrlString = actualUrl.toString();
-        assertEquals(expectedBaseUrl,
-                actualUrlString.contains("?") ? actualUrlString.substring(0, actualUrlString.indexOf('?'))
-                        : actualUrlString);
-        String[] actualParams = actualUrl.getQuery() == null ? new String[0] : actualUrl.getQuery().split("&");
-        assertEquals(expectedParams.length, actualParams.length);
-        for (int exIndex = 0; exIndex < expectedParams.length; exIndex++) {
-            boolean parameterIsPresent = false;
-            for (int acIndex = 0; !parameterIsPresent && acIndex < actualParams.length; acIndex++) {
-                parameterIsPresent |= actualParams[acIndex].equals(expectedParams[exIndex]);
-            }
-            Assertions.assertTrue(parameterIsPresent, expectedParams[exIndex] + " was expected in the query string of "
-                    + actualUrl + " but wasn't there.");
-        }
-    }
 }
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsideUtilsTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsideUtilsTest.java	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsideUtilsTest.java	(revision 36228)
@@ -2,8 +2,4 @@
 package org.openstreetmap.josm.plugins.streetside.utils;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.apache.commons.imaging.common.RationalNumber;
-import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants;
 import org.junit.jupiter.api.Test;
 
@@ -21,22 +17,3 @@
     }
 
-    /**
-     * Test {@link StreetsideUtils#degMinSecToDouble(RationalNumber[], String)}
-     * method.
-     */
-    @Test
-    void testDegMinSecToDouble() {
-        RationalNumber[] num = new RationalNumber[3];
-        num[0] = new RationalNumber(1, 1);
-        num[1] = new RationalNumber(0, 1);
-        num[2] = new RationalNumber(0, 1);
-        String ref = GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_NORTH;
-        assertEquals(1, StreetsideUtils.degMinSecToDouble(num, ref), 0.01);
-        ref = GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH;
-        assertEquals(-1, StreetsideUtils.degMinSecToDouble(num, ref), 0.01);
-        num[0] = new RationalNumber(180, 1);
-        assertEquals(-180, StreetsideUtils.degMinSecToDouble(num, ref), 0.01);
-        num[0] = new RationalNumber(190, 1);
-        assertEquals(170, StreetsideUtils.degMinSecToDouble(num, ref), 0.01);
-    }
 }
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/TestUtil.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/TestUtil.java	(revision 36227)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/TestUtil.java	(revision 36228)
@@ -10,5 +10,8 @@
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
+import java.time.Instant;
+import java.util.Arrays;
 
+import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
 import org.openstreetmap.josm.tools.JosmRuntimeException;
 
@@ -76,3 +79,19 @@
         }
     }
+
+    /**
+     * Generate a valid image
+     * @param id The id of the image
+     * @param lat The latitude of the image
+     * @param lon The longitude of the image
+     * @return The new image
+     */
+    public static StreetsideImage generateImage(String id, double lat, double lon) {
+        return new StreetsideImage("https://ecn.{subdomain}.tiles.virtualearth.net/tiles/hs" + id
+                + "{faceId}{tileId}?g=14336&key=Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU", lat,
+                lon, 268.811, 1.395, -4.875, Instant.ofEpochMilli(1614556800000L), Instant.ofEpochMilli(1614643199999L),
+                "https://dev.virtualearth.net/Branding/logo_powered_by.png",
+                "Copyright © 2024 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.",
+                1, 3, 256, 256, Arrays.asList("t0", "t1", "t2", "t3"));
+    }
 }
Index: /applications/editors/josm/plugins/build.xml
===================================================================
--- /applications/editors/josm/plugins/build.xml	(revision 36227)
+++ /applications/editors/josm/plugins/build.xml	(revision 36228)
@@ -14,4 +14,5 @@
     <property name="java21_plugins" value="FIT/build.xml" />
     <property name="java17_plugins" value="maproulette/build.xml
+                                            MicrosoftStreetside/build.xml
                                             imageio/build.xml
                                             pmtiles/build.xml
@@ -32,6 +33,5 @@
                                             apache-http/build.xml
                                             austriaaddresshelper/build.xml"/>
-    <property name="javafx_plugins" value="javafx/build.xml
-                                           MicrosoftStreetside/build.xml"/>
+    <property name="javafx_plugins" value="javafx/build.xml"/>
 
     <!-- We are dropping Java 8 support in January 2024 - these have issues compiling with Java 8, but not with Java 11+ targeting Java 8 -->
