From 377c68987af7ebcc664c40252db6866de5eb2f0c Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Mon, 27 Apr 2020 09:02:59 -0600
Subject: [PATCH 01/20] Don't redownload images that are already obtained.

This fixes #72 (pre-fetching does work, but the code forced a download
anyway)

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 .../plugins/mapillary/cache/CacheUtils.java   | 21 ++++++-------------
 .../mapillary/gui/MapillaryMainDialog.java    |  6 ++++--
 2 files changed, 10 insertions(+), 17 deletions(-)

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/CacheUtils.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/CacheUtils.java
index 79c8f935f..ee498bd05 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/CacheUtils.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/CacheUtils.java
@@ -55,21 +55,12 @@ public static void downloadPicture(MapillaryImage img) {
    *          both.)
    */
   public static void downloadPicture(MapillaryImage img, PICTURE pic) {
-    switch (pic) {
-      case BOTH:
-        if (new MapillaryCache(img.getKey(), MapillaryCache.Type.THUMBNAIL).get() == null)
-          submit(img.getKey(), MapillaryCache.Type.THUMBNAIL, IGNORE_DOWNLOAD);
-        if (new MapillaryCache(img.getKey(), MapillaryCache.Type.FULL_IMAGE).get() == null)
-          submit(img.getKey(), MapillaryCache.Type.FULL_IMAGE, IGNORE_DOWNLOAD);
-        break;
-      case THUMBNAIL:
-        submit(img.getKey(), MapillaryCache.Type.THUMBNAIL, IGNORE_DOWNLOAD);
-        break;
-      case FULL_IMAGE:
-      default:
-        submit(img.getKey(), MapillaryCache.Type.FULL_IMAGE, IGNORE_DOWNLOAD);
-        break;
-    }
+    boolean thumbnail = new MapillaryCache(img.getKey(), MapillaryCache.Type.THUMBNAIL).get() == null && (PICTURE.BOTH.equals(pic) || PICTURE.THUMBNAIL.equals(pic));
+    boolean fullImage = new MapillaryCache(img.getKey(), MapillaryCache.Type.FULL_IMAGE).get() == null && (PICTURE.BOTH.equals(pic) || PICTURE.FULL_IMAGE.equals(pic));
+    if (thumbnail)
+      submit(img.getKey(), MapillaryCache.Type.THUMBNAIL, IGNORE_DOWNLOAD);
+    if (fullImage)
+      submit(img.getKey(), MapillaryCache.Type.FULL_IMAGE, IGNORE_DOWNLOAD);
   }
 
   /**
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryMainDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryMainDialog.java
index 95939ded8..d757e2312 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryMainDialog.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryMainDialog.java
@@ -334,7 +334,8 @@ public synchronized void updateImage(boolean fullQuality) {
           this.thumbnailCache.cancelOutstandingTasks();
         this.thumbnailCache = new MapillaryCache(mapillaryImage.getKey(), MapillaryCache.Type.THUMBNAIL);
         try {
-          this.thumbnailCache.submit(this, false);
+          if (this.thumbnailCache.get() == null)
+            this.thumbnailCache.submit(this, false);
         } catch (IOException e) {
           Logging.error(e);
         }
@@ -345,7 +346,8 @@ public synchronized void updateImage(boolean fullQuality) {
             this.imageCache.cancelOutstandingTasks();
           this.imageCache = new MapillaryCache(mapillaryImage.getKey(), MapillaryCache.Type.FULL_IMAGE);
           try {
-            this.imageCache.submit(this, false);
+            if (this.imageCache.get() == null)
+              this.imageCache.submit(this, false);
           } catch (IOException e) {
             Logging.error(e);
           }

From 325c1fe0213128897da8ae2ce0892d8b068a97c6 Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Tue, 28 Apr 2020 09:28:49 -0600
Subject: [PATCH 02/20] Modify actions to use the PointObjectLayer

* Traffic signs and point features use the same base layer
* The Point Object layer can now merge with other Point Object layers,
  if they are of the same type (traffic signs/point features)

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 .../actions/MapObjectLayerAction.java         |  27 ++--
 .../actions/MapPointObjectLayerAction.java    |  27 ++--
 .../mapillary/gui/MapillaryFilterDialog.java  |   4 +-
 .../mapillary/gui/layer/PointObjectLayer.java | 131 +++++++++++-------
 .../plugins/mapillary/utils/MapillaryURL.java |   2 +-
 .../actions/MapObjectLayerActionTest.java     |  22 +--
 6 files changed, 135 insertions(+), 78 deletions(-)

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapObjectLayerAction.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapObjectLayerAction.java
index 78acd66b4..df89293ea 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapObjectLayerAction.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapObjectLayerAction.java
@@ -2,28 +2,33 @@
 package org.openstreetmap.josm.plugins.mapillary.actions;
 
 import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
 
 import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
-import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapObjectLayer;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.PointObjectLayer;
 import org.openstreetmap.josm.tools.I18n;
 import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
+import org.openstreetmap.josm.tools.Shortcut;
 
 public class MapObjectLayerAction extends JosmAction {
   private static final long serialVersionUID = -8388752916891634738L;
+  private static final String ACTION_NAME = I18n.marktr("Mapillary traffic signs layer");
+  private static final String DESCRIPTION = I18n
+    .marktr("Displays the layer displaying the traffic sign objects detected by Mapillary");
 
   public MapObjectLayerAction() {
     super(
-      I18n.tr("Mapillary object layer"),
+      I18n.tr(ACTION_NAME),
       MapillaryPlugin.LOGO.setSize(ImageSizes.DEFAULT),
-      I18n.tr("Displays the layer displaying the map objects detected by Mapillary"),
-      null,
+      I18n.tr(DESCRIPTION),
+      Shortcut.registerShortcut("mapillary:trafficSignLayer", ACTION_NAME, KeyEvent.CHAR_UNDEFINED, Shortcut.NONE),
       false,
-      "mapillaryObjectLayer",
-      false
-    );
+      "mapillary:trafficSignLayer",
+      false);
   }
 
   @Override
@@ -32,8 +37,12 @@ public void actionPerformed(ActionEvent e) {
       // Synchronization lock must be held by EDT thread
       // See {@link LayerManager#addLayer(org.openstreetmap.josm.gui.layer.Layer, boolean)}.
       synchronized (MainApplication.getLayerManager()) {
-        if (!MainApplication.getLayerManager().containsLayer(MapObjectLayer.getInstance())) {
-          MainApplication.getLayerManager().addLayer(MapObjectLayer.getInstance());
+        DataSet followDataSet = MainApplication.getLayerManager().getActiveDataSet();
+        if (MainApplication.getLayerManager().getActiveDataSet() != null
+          && MainApplication.getLayerManager().getLayersOfType(PointObjectLayer.class).parallelStream()
+            .filter(PointObjectLayer::hasTrafficSigns)
+            .noneMatch(p -> p.followDataSet != null && p.followDataSet.equals(followDataSet))) {
+          MainApplication.getLayerManager().addLayer(new PointObjectLayer(true), false);
         }
       }
     });
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapPointObjectLayerAction.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapPointObjectLayerAction.java
index cacb99c26..17a5974c3 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapPointObjectLayerAction.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapPointObjectLayerAction.java
@@ -8,6 +8,7 @@
 import java.awt.event.KeyEvent;
 
 import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.gui.util.GuiHelper;
@@ -21,15 +22,16 @@
  */
 public class MapPointObjectLayerAction extends JosmAction {
   private static final long serialVersionUID = 5780337309290262545L;
-  private static final String ACTION_NAME = marktr("Mapillary point object layer");
+  private static final String ACTION_NAME = marktr("Mapillary point features layer");
   private static final String TOOLTIP = marktr(
-    "Displays the layer displaying the map point objects detected by Mapillary"
-  );
+    "Displays the layer displaying the map point objects detected by Mapillary");
 
   public MapPointObjectLayerAction() {
     super(
-      tr(ACTION_NAME), MapillaryPlugin.LOGO.setSize(ImageSizes.DEFAULT), tr(TOOLTIP), Shortcut.registerShortcut(tr(ACTION_NAME), tr(TOOLTIP), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE), false, "mapillary:pointObjectLayer", false
-    );
+      tr(ACTION_NAME), MapillaryPlugin.LOGO.setSize(ImageSizes.DEFAULT), tr(TOOLTIP),
+      Shortcut.registerShortcut("mapillary:pointFeaturesLayer", tr(ACTION_NAME), KeyEvent.CHAR_UNDEFINED,
+        Shortcut.NONE),
+      false, "mapillary:pointFeaturesLayer", false);
   }
 
   @Override
@@ -40,13 +42,18 @@ public void actionPerformed(ActionEvent e) {
         // See {@link LayerManager#addLayer(org.openstreetmap.josm.gui.layer.Layer, boolean)}.
         synchronized (MainApplication.getLayerManager()) {
           Layer layer = MainApplication.getLayerManager().getActiveLayer();
-          MainApplication.getLayerManager().addLayer(new PointObjectLayer(), false);
-          if (layer != null) {
-            MainApplication.getLayerManager().setActiveLayer(layer);
+          DataSet followDataSet = MainApplication.getLayerManager().getActiveDataSet();
+          if (MainApplication.getLayerManager().getActiveDataSet() != null
+            && MainApplication.getLayerManager().getLayersOfType(PointObjectLayer.class).parallelStream()
+              .filter(PointObjectLayer::hasPointFeatures)
+              .noneMatch(p -> p.followDataSet != null && p.followDataSet.equals(followDataSet))) {
+            MainApplication.getLayerManager().addLayer(new PointObjectLayer(false), false);
+            if (layer != null) {
+              MainApplication.getLayerManager().setActiveLayer(layer);
+            }
           }
         }
-      }
-    );
+      });
   }
 
 }
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryFilterDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryFilterDialog.java
index 6fc6caec0..b891ec7cd 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryFilterDialog.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryFilterDialog.java
@@ -89,8 +89,8 @@ public final class MapillaryFilterDialog extends ToggleDialog implements Mapilla
 
   final JComboBox<OrganizationRecord> organizations = new JComboBox<>();
 
-  private IDatePicker<?> startDate;
-  private IDatePicker<?> endDate;
+  private final IDatePicker<?> startDate;
+  private final IDatePicker<?> endDate;
 
   private boolean destroyed;
 
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/PointObjectLayer.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/PointObjectLayer.java
index d3fcd4f48..472ea8232 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/PointObjectLayer.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/PointObjectLayer.java
@@ -76,6 +76,7 @@
 import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
 import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
+import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
 import org.openstreetmap.josm.gui.layer.AbstractOsmDataLayer;
 import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
@@ -101,9 +102,11 @@
 import org.openstreetmap.josm.plugins.mapillary.data.osm.event.FilterEventListener;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryMainDialog;
 import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryExpertFilterDialog;
+import org.openstreetmap.josm.plugins.mapillary.io.download.MapillaryDownloader;
 import org.openstreetmap.josm.plugins.mapillary.model.ImageDetection;
 import org.openstreetmap.josm.plugins.mapillary.oauth.MapillaryUser;
 import org.openstreetmap.josm.plugins.mapillary.oauth.OAuthUtils;
+import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryURL;
 import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.HttpClient;
@@ -119,14 +122,18 @@ public class PointObjectLayer extends AbstractOsmDataLayer
   implements DataSourceListener, MouseListener, Listener, HighlightUpdateListener, DataSelectionListener,
   MapillaryDataListener, LayerChangeListener {
   private final Collection<DataSource> dataSources = new HashSet<>();
-  private static final String NAME = marktr("Mapillary Point Objects");
+  private static final String[] NAMES = new String[] { marktr("Mapillary point features"),
+    marktr("Mapillary traffic signs") };
   private static final int HATCHED_SIZE = 15;
-  private final DataSet followDataSet;
+  public final DataSet followDataSet;
   private final FilterEventListener tableModelListener;
   private static final String PAINT_STYLE_SOURCE = "resource://mapcss/Mapillary.mapcss";
   private static MapCSSStyleSource mapcss;
   private final DataSet data;
   private final DataSetListenerAdapter dataSetListenerAdapter;
+  /** If true, display traffic signs. If false, display point objects. */
+  private final boolean trafficSigns;
+  private static final String DETECTIONS = "detections";
 
   /**
    * a texture for non-downloaded area Copied from OsmDataLayer
@@ -139,10 +146,10 @@ public class PointObjectLayer extends AbstractOsmDataLayer
 
   private static MapCSSStyleSource getMapCSSStyle() {
     List<MapCSSStyleSource> styles = MapPaintStyles.getStyles().getStyleSources().parallelStream()
-        .filter(MapCSSStyleSource.class::isInstance).map(MapCSSStyleSource.class::cast)
-        .filter(s -> PAINT_STYLE_SOURCE.equals(s.url)).collect(Collectors.toList());
+      .filter(MapCSSStyleSource.class::isInstance).map(MapCSSStyleSource.class::cast)
+      .filter(s -> PAINT_STYLE_SOURCE.equals(s.url)).collect(Collectors.toList());
     mapcss = styles.isEmpty() ? new MapCSSStyleSource(PAINT_STYLE_SOURCE, "Mapillary", "Mapillary Point Objects")
-        : styles.get(0);
+      : styles.get(0);
     return mapcss;
   }
 
@@ -162,15 +169,17 @@ public static void createHatchTexture() {
     hatched = bi;
   }
 
-  public PointObjectLayer() {
-    super(NAME);
+  public PointObjectLayer(boolean trafficSigns) {
+    super(tr(trafficSigns ? NAMES[1] : NAMES[0]));
+    String name = trafficSigns ? NAMES[1] : NAMES[0];
+    this.trafficSigns = trafficSigns;
     data = new DataSet();
     data.setUploadPolicy(UploadPolicy.BLOCKED);
     data.setDownloadPolicy(DownloadPolicy.BLOCKED);
     data.lock();
     followDataSet = MainApplication.getLayerManager().getActiveDataSet();
     followDataSet.addDataSourceListener(this);
-    this.setName(NAME + ": " + MainApplication.getLayerManager().getActiveDataLayer().getName());
+    this.setName(name + ": " + followDataSet.getName());
     MainApplication.worker.execute(() -> followDataSet.getDataSources().forEach(this::getData));
     getMapCSSStyle();
     if (!MapPaintStyles.getStyles().getStyleSources().contains(mapcss)) {
@@ -184,7 +193,7 @@ public PointObjectLayer() {
     MainApplication.getMap().filterDialog.getFilterModel().addTableModelListener(tableModelListener);
     tableModelListener.tableChanged(null);
 
-    this.data.setName(NAME);
+    this.data.setName(name);
     this.dataSetListenerAdapter = new DataSetListenerAdapter(this);
     data.addDataSetListener(dataSetListenerAdapter);
     data.addDataSetListener(MultipolygonCache.getInstance());
@@ -198,18 +207,26 @@ public PointObjectLayer() {
 
   @Override
   public void dataSourceChange(DataSourceChangeEvent event) {
-    if (SwingUtilities.isEventDispatchThread()) {
-      MainApplication.worker.execute(() -> event.getAdded().forEach(this::getData));
-    } else {
-      event.getAdded().forEach(this::getData);
+    if (MapillaryDownloader.DOWNLOAD_MODE.OSM_AREA
+      .equals(MapillaryDownloader.DOWNLOAD_MODE.fromPrefId(MapillaryProperties.DOWNLOAD_MODE.get()))) {
+      if (SwingUtilities.isEventDispatchThread()) {
+        MainApplication.worker.execute(() -> event.getAdded().forEach(this::getData));
+      } else {
+        event.getAdded().forEach(this::getData);
+      }
     }
+    String name = trafficSigns ? NAMES[1] : NAMES[0];
+    this.setName(name + ": " + followDataSet.getName());
+    this.data.setName(name);
   }
 
   public void getData(DataSource dataSource) {
     if (dataSources.add(dataSource)) {
       try {
         data.unlock();
-        realGetData(dataSource.bounds, data);
+        Bounds bound = dataSource.bounds;
+        realGetData(data,
+          trafficSigns ? MapillaryURL.APIv3.searchMapObjects(bound) : MapillaryURL.APIv3.searchMapPointObjects(bound));
         data.addDataSource(dataSource);
       } catch (IllegalDataException | IOException e) {
         Logging.error(e);
@@ -221,26 +238,26 @@ public void getData(DataSource dataSource) {
     }
   }
 
-  private static void realGetData(Bounds bound, DataSet data) throws IllegalDataException, IOException {
-    URL url = MapillaryURL.APIv3.searchMapPointObjects(bound);
+  private static void realGetData(DataSet data, URL url) throws IllegalDataException, IOException {
+    URL currentUrl = url;
     do {
-      HttpClient client = HttpClient.create(url);
+      HttpClient client = HttpClient.create(currentUrl);
       if (MapillaryUser.getUsername() != null)
         OAuthUtils.addAuthenticationHeader(client);
       client.connect();
       try (InputStream stream = client.getResponse().getContent()) {
         DataSet ds = GeoJSONReader.parseDataSet(stream, NullProgressMonitor.INSTANCE);
-        ds.allPrimitives().parallelStream().filter(p -> p.hasKey("detections"))
-            .forEach(p -> p.put("detections_num", Integer.toString(p.get("detections").split("detection_key").length)));
+        ds.allPrimitives().parallelStream().filter(p -> p.hasKey(DETECTIONS))
+          .forEach(p -> p.put("detections_num", Integer.toString(p.get(DETECTIONS).split("detection_key").length)));
         ds.allPrimitives().forEach(p -> p.setModified(false));
         synchronized (PointObjectLayer.class) {
           data.mergeFrom(ds);
         }
-        url = MapillaryURL.APIv3.parseNextFromLinkHeaderValue(client.getResponse().getHeaderField("Link"));
+        currentUrl = MapillaryURL.APIv3.parseNextFromLinkHeaderValue(client.getResponse().getHeaderField("Link"));
       } finally {
         client.disconnect();
       }
-    } while (url != null);
+    } while (currentUrl != null);
   }
 
   @Override
@@ -278,7 +295,7 @@ public void paint(final Graphics2D g, final MapView mv, Bounds box) {
       // paint remainder
       MapViewPoint anchor = mv.getState().getPointFor(new EastNorth(0, 0));
       Rectangle2D anchorRect = new Rectangle2D.Double(anchor.getInView().getX() % HATCHED_SIZE,
-          anchor.getInView().getY() % HATCHED_SIZE, HATCHED_SIZE, HATCHED_SIZE);
+        anchor.getInView().getY() % HATCHED_SIZE, HATCHED_SIZE, HATCHED_SIZE);
       if (hatched != null) {
         g.setPaint(new TexturePaint(hatched, anchorRect));
       }
@@ -292,22 +309,22 @@ public void paint(final Graphics2D g, final MapView mv, Bounds box) {
 
     AbstractMapRenderer painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, inactive);
     painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress()
-        || !OsmDataLayer.PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get());
+      || !OsmDataLayer.PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get());
     painter.render(data, virtual, box);
     MainApplication.getMap().conflictDialog.paintConflicts(g, mv);
   }
 
   @Override
   public void selectionChanged(SelectionChangeEvent event) {
-    OsmPrimitive prim = event.getSelection().parallelStream().filter(p -> p.hasKey("detections")).findFirst()
-        .orElse(null);
+    OsmPrimitive prim = event.getSelection().parallelStream().filter(p -> p.hasKey(DETECTIONS)).findFirst()
+      .orElse(null);
     if (prim != null && MapillaryLayer.hasInstance()) {
-      List<Map<String, String>> detections = parseDetections(prim.get("detections"));
+      List<Map<String, String>> detections = parseDetections(prim.get(DETECTIONS));
 
       MapillaryData mapillaryData = MapillaryLayer.getInstance().getData();
       MapillaryImage selectedImage = mapillaryData.getSelectedImage() instanceof MapillaryImage
-          ? (MapillaryImage) mapillaryData.getSelectedImage()
-          : null;
+        ? (MapillaryImage) mapillaryData.getSelectedImage()
+        : null;
       List<MapillaryImage> images = getImagesForDetections(mapillaryData, detections);
       MapillaryImage toSelect = images.isEmpty() ? null : images.get(0);
       boolean inDetections = selectedImage != null && images.contains(selectedImage);
@@ -334,7 +351,7 @@ public void hookUpMapView() {
   public static List<Map<String, String>> parseDetections(String detectionsValue) {
     List<Map<String, String>> detections = new ArrayList<>();
     try (JsonParser parser = Json
-        .createParser(new ByteArrayInputStream(detectionsValue.getBytes(StandardCharsets.UTF_8)))) {
+      .createParser(new ByteArrayInputStream(detectionsValue.getBytes(StandardCharsets.UTF_8)))) {
       while (parser.hasNext() && JsonParser.Event.START_ARRAY == parser.next()) {
         JsonArray array = parser.getArray();
         for (JsonObject obj : array.getValuesAs(JsonObject.class)) {
@@ -357,18 +374,19 @@ public static List<Map<String, String>> parseDetections(String detectionsValue)
 
   private static List<MapillaryImage> getImagesForDetections(MapillaryData data, List<Map<String, String>> detections) {
     return detections.stream().filter(m -> m.containsKey("image_key")).map(m -> m.get("image_key")).map(data::getImage)
-        .collect(Collectors.toList());
+      .collect(Collectors.toList());
   }
 
   @Override
   public Action[] getMenuEntries() {
     List<Action> actions = new ArrayList<>();
     actions.addAll(Arrays.asList(LayerListDialog.getInstance().createActivateLayerAction(this),
-        LayerListDialog.getInstance().createShowHideLayerAction(),
-        LayerListDialog.getInstance().createDeleteLayerAction(), SeparatorLayerAction.INSTANCE,
-        LayerListDialog.getInstance().createMergeLayerAction(this)));
+      LayerListDialog.getInstance().createShowHideLayerAction(),
+      LayerListDialog.getInstance().createDeleteLayerAction(), SeparatorLayerAction.INSTANCE,
+      LayerListDialog.getInstance().createMergeLayerAction(this)));
     actions.addAll(Arrays.asList(SeparatorLayerAction.INSTANCE, new RenameLayerAction(getAssociatedFile(), this),
-        SeparatorLayerAction.INSTANCE, new RequestDataAction(followDataSet)));
+      SeparatorLayerAction.INSTANCE, new RequestDataAction(followDataSet), SeparatorLayerAction.INSTANCE,
+      new LayerListPopup.InfoAction(this)));
     return actions.toArray(new Action[0]);
   }
 
@@ -387,7 +405,7 @@ public void actionPerformed(ActionEvent e) {
       String bbox = "?bbox="
         + String.join(";",
           data.getDataSourceBounds().stream()
-          .map(Bounds::toBBox).map(b -> b.toStringCSV(",")).collect(Collectors.toList()));
+            .map(Bounds::toBBox).map(b -> b.toStringCSV(",")).collect(Collectors.toList()));
       OpenBrowser.displayUrl("https://mapillary.github.io/mapillary_solutions/data-request" + bbox);
     }
   }
@@ -433,8 +451,8 @@ public String getToolTipText() {
     int rels = counter.relations - counter.deletedRelations;
 
     StringBuilder tooltip = new StringBuilder("<html>").append(trn("{0} node", "{0} nodes", nodes, nodes))
-        .append("<br>").append(trn("{0} way", "{0} ways", ways, ways)).append("<br>")
-        .append(trn("{0} relation", "{0} relations", rels, rels));
+      .append("<br>").append(trn("{0} way", "{0} ways", ways, ways)).append("<br>")
+      .append(trn("{0} relation", "{0} relations", rels, rels));
 
     File f = getAssociatedFile();
     if (f != null) {
@@ -450,16 +468,22 @@ public void mergeFrom(Layer from) {
       DataSet fromData = ((PointObjectLayer) from).getDataSet();
       final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Merging layers"));
       monitor.setCancelable(false);
-      fromData.unlock();
-      data.mergeFrom(fromData, monitor);
-      fromData.lock();
-      monitor.close();
+      try {
+        fromData.unlock();
+        data.unlock();
+        data.mergeFrom(fromData, monitor);
+      } finally {
+        fromData.lock();
+        data.lock();
+        monitor.close();
+        ((PointObjectLayer) from).destroy();
+      }
     }
   }
 
   @Override
   public boolean isMergable(Layer other) {
-    return other instanceof PointObjectLayer;
+    return other instanceof PointObjectLayer && this.trafficSigns == ((PointObjectLayer) other).trafficSigns;
   }
 
   @Override
@@ -476,11 +500,11 @@ public Object getInfoComponent() {
 
     p.add(new JLabel(tr("{0} consists of:", getName())), GBC.eol());
     p.add(new JLabel(nodeText, ImageProvider.get("data", "node"), SwingConstants.HORIZONTAL),
-        GBC.eop().insets(15, 0, 0, 0));
+      GBC.eop().insets(15, 0, 0, 0));
     p.add(new JLabel(wayText, ImageProvider.get("data", "way"), SwingConstants.HORIZONTAL),
-        GBC.eop().insets(15, 0, 0, 0));
+      GBC.eop().insets(15, 0, 0, 0));
     p.add(new JLabel(relationText, ImageProvider.get("data", "relation"), SwingConstants.HORIZONTAL),
-        GBC.eop().insets(15, 0, 0, 0));
+      GBC.eop().insets(15, 0, 0, 0));
 
     return p;
   }
@@ -582,10 +606,23 @@ public void selectedImageChanged(MapillaryAbstractImage oldImage, MapillaryAbstr
     if (newImage instanceof MapillaryImage) {
       MapillaryImage image = (MapillaryImage) newImage;
       Collection<INode> nodes = image.getDetections().parallelStream().map(ImageDetection::getKey).flatMap(
-        d -> data.getNodes().parallelStream().filter(n -> n.hasKey("detections") && n.get("detections").contains(d))
-      ).collect(Collectors.toList());
+        d -> data.getNodes().parallelStream().filter(n -> n.hasKey(DETECTIONS) && n.get(DETECTIONS).contains(d)))
+        .collect(Collectors.toList());
       data.setSelected(nodes);
     }
   }
 
+  /**
+   * @return true if this layer has traffic signs
+   */
+  public boolean hasTrafficSigns() {
+    return this.trafficSigns;
+  }
+
+  /**
+   * @return true if this layer has point features (does not include traffic signs)
+   */
+  public boolean hasPointFeatures() {
+    return !this.trafficSigns;
+  }
 }
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryURL.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryURL.java
index a37e1da94..cacca053e 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryURL.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryURL.java
@@ -106,7 +106,7 @@ public static URL searchMapPointObjects(final Bounds bounds) {
     }
 
     private static String getEnabledLayers() {
-      return String.join(",", Arrays.asList(TRAFFIC_SIGN_LAYER, POINT_FEATURES_LAYER, LINE_FEATURES_LAYER));
+      return String.join(",", Arrays.asList(POINT_FEATURES_LAYER, LINE_FEATURES_LAYER));
     }
 
     private static String getDetectionLayers() {
diff --git a/test/unit/org/openstreetmap/josm/plugins/mapillary/actions/MapObjectLayerActionTest.java b/test/unit/org/openstreetmap/josm/plugins/mapillary/actions/MapObjectLayerActionTest.java
index 7c47ffc0f..bb1a999e7 100644
--- a/test/unit/org/openstreetmap/josm/plugins/mapillary/actions/MapObjectLayerActionTest.java
+++ b/test/unit/org/openstreetmap/josm/plugins/mapillary/actions/MapObjectLayerActionTest.java
@@ -3,30 +3,34 @@
 
 import static org.junit.Assert.assertEquals;
 
-import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 
+import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.PointObjectLayer;
 import org.openstreetmap.josm.plugins.mapillary.utils.TestUtil.MapillaryTestRules;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 
 public class MapObjectLayerActionTest {
 
   @Rule
-  public JOSMTestRules rules = new MapillaryTestRules();
-
-  @Before
-  public void resetLayers() {
-    MainApplication.getLayerManager().getLayers().parallelStream().forEach(l -> MainApplication.getLayerManager().removeLayer(l));
-  }
+  public JOSMTestRules rules = new MapillaryTestRules().main().projection();
 
   @Test
   public void testAction() {
     assertEquals(0, MainApplication.getLayerManager().getLayers().size());
     new MapObjectLayerAction().actionPerformed(null);
-    assertEquals(1, MainApplication.getLayerManager().getLayers().size());
+    assertEquals(0, MainApplication.getLayerManager().getLayers().size());
+    MainApplication.getLayerManager().addLayer(new OsmDataLayer(new DataSet(), "Test", null));
+    new MapObjectLayerAction().actionPerformed(null);
+    assertEquals(2, MainApplication.getLayerManager().getLayers().size());
+    new MapObjectLayerAction().actionPerformed(null);
+    assertEquals(2, MainApplication.getLayerManager().getLayers().size());
+    MainApplication.getLayerManager()
+      .setActiveLayer(MainApplication.getLayerManager().getLayersOfType(PointObjectLayer.class).get(0));
     new MapObjectLayerAction().actionPerformed(null);
-    assertEquals(1, MainApplication.getLayerManager().getLayers().size());
+    assertEquals(2, MainApplication.getLayerManager().getLayers().size());
   }
 }

From e055443d23441b02be771327031cb33e1e205e58 Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Tue, 28 Apr 2020 10:20:12 -0600
Subject: [PATCH 03/20] Remove previous map object layer implementation.

This is due to PointObjectLayer being a bit more performant when images
are missing.

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 .../plugins/mapillary/MapillaryPlugin.java    |   4 +-
 .../mapillary/gui/layer/MapObjectLayer.java   | 241 ------------------
 .../download/MapObjectDownloadRunnable.java   |  94 -------
 ...yerTest.java => PointObjectLayerTest.java} |  53 ++--
 4 files changed, 27 insertions(+), 365 deletions(-)
 delete mode 100644 src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapObjectLayer.java
 delete mode 100644 src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/MapObjectDownloadRunnable.java
 rename test/unit/org/openstreetmap/josm/plugins/mapillary/gui/layer/{MapObjectLayerTest.java => PointObjectLayerTest.java} (65%)

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java
index bb2814f47..4e06499ef 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java
@@ -40,7 +40,6 @@
 import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryExpertFilterDialog;
 import org.openstreetmap.josm.plugins.mapillary.gui.imageinfo.ImageInfoHelpPopup;
 import org.openstreetmap.josm.plugins.mapillary.gui.imageinfo.ImageInfoPanel;
-import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapObjectLayer;
 import org.openstreetmap.josm.plugins.mapillary.gui.layer.PointObjectLayer;
 import org.openstreetmap.josm.plugins.mapillary.oauth.MapillaryUser;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
@@ -185,8 +184,7 @@ public void destroy() {
         menu.editMenu, menu.fileMenu, menu.windowMenu)) {
       clearMenues(jmenu);
     }
-    for (Class<? extends Layer> layerClazz : Arrays.asList(MapillaryLayer.class, MapObjectLayer.class,
-        PointObjectLayer.class)) {
+    for (Class<? extends Layer> layerClazz : Arrays.asList(MapillaryLayer.class, PointObjectLayer.class)) {
       MainApplication.getLayerManager().getLayersOfType(layerClazz)
           .forEach(layer -> MainApplication.getLayerManager().removeLayer(layer));
     }
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapObjectLayer.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapObjectLayer.java
deleted file mode 100644
index c27eeaf29..000000000
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapObjectLayer.java
+++ /dev/null
@@ -1,241 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.mapillary.gui.layer;
-
-import java.awt.Color;
-import java.awt.Font;
-import java.awt.FontMetrics;
-import java.awt.Graphics2D;
-import java.awt.Point;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-
-import javax.swing.Action;
-import javax.swing.Icon;
-import javax.swing.ImageIcon;
-
-import org.openstreetmap.josm.data.Bounds;
-import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
-import org.openstreetmap.josm.gui.MapView;
-import org.openstreetmap.josm.gui.NavigatableComponent;
-import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener;
-import org.openstreetmap.josm.gui.layer.Layer;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
-import org.openstreetmap.josm.plugins.mapillary.io.download.MapObjectDownloadRunnable;
-import org.openstreetmap.josm.plugins.mapillary.model.MapObject;
-import org.openstreetmap.josm.plugins.mapillary.utils.ImageUtil;
-import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
-import org.openstreetmap.josm.tools.I18n;
-import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
-import org.openstreetmap.josm.tools.Logging;
-
-public final class MapObjectLayer extends Layer implements ZoomChangeListener {
-
-  public enum STATUS {
-    DOWNLOADING(I18n.marktr("Downloading map objects…"), Color.YELLOW),
-    COMPLETE(I18n.marktr("All map objects loaded."), Color.GREEN),
-    INCOMPLETE(I18n.marktr("Too many map objects, zoom in to see all."), Color.ORANGE),
-    FAILED(I18n.marktr("Downloading map objects failed!"), Color.RED);
-
-    private final int colorValue;
-    public final String message;
-
-    STATUS(final String message, final Color color) {
-      this.colorValue = color.getRGB();
-      this.message = message;
-    }
-
-    /**
-     * Note: The color is stored as int and each time returned as new {@link Color} instance,
-     * because the class {@link Color} is not strictly immutable.
-     * @return the color associated with the current status
-     */
-    public Color getColor() {
-      return new Color(colorValue, true);
-    }
-  }
-
-  private static MapObjectLayer instance;
-
-  private STATUS status = STATUS.COMPLETE;
-  private MapObjectDownloadRunnable downloadRunnable;
-  private MapObjectDownloadRunnable nextDownloadRunnable;
-  private final Collection<MapObject> objects = new HashSet<>();
-
-  private final Map<String, ImageIcon> scaledIcons = new HashMap<>();
-
-  private MapObjectLayer() {
-    super(I18n.tr("Mapillary objects"));
-    NavigatableComponent.addZoomChangeListener(this);
-    MapillaryProperties.MAPOBJECT_ICON_SIZE.addListener(val -> {
-      scaledIcons.clear();
-      finishDownload(false);
-    });
-    zoomChanged();
-  }
-
-  private static void clearInstance() {
-    synchronized (MapObjectLayer.class) {
-      instance = null;
-    }
-  }
-
-  public static MapObjectLayer getInstance() {
-    synchronized (MapObjectLayer.class) {
-      if (instance == null) {
-        instance = new MapObjectLayer();
-      }
-      return instance;
-    }
-  }
-
-  public boolean isDownloadRunnableScheduled() {
-    synchronized (this) {
-      return nextDownloadRunnable != null;
-    }
-  }
-
-  public void finishDownload(boolean replaceMapObjects) {
-    synchronized (this) {
-      final MapObjectDownloadRunnable currentRunnable = downloadRunnable;
-      if (currentRunnable != null) {
-        synchronized (objects) {
-          if (replaceMapObjects) {
-            objects.clear();
-          }
-          objects.addAll(currentRunnable.getMapObjects());
-        }
-      }
-      downloadRunnable = null;
-      if (nextDownloadRunnable != null) {
-        downloadRunnable = nextDownloadRunnable;
-        nextDownloadRunnable = null;
-        new Thread(downloadRunnable, "downloadMapObjects").start();
-      }
-    }
-    new Thread(() -> {
-      synchronized (objects) {
-        for (MapObject object : objects) {
-          if (!scaledIcons.containsKey(object.getValue())) {
-            ImageIcon icon = MapObject.getIcon(object.getValue());
-            if (icon != null) {
-              scaledIcons.put(
-                object.getValue(),
-                ImageUtil.scaleImageIcon(icon, MapillaryProperties.MAPOBJECT_ICON_SIZE.get())
-              );
-            }
-          }
-        }
-      }
-      invalidate();
-    }, "downloadMapObjectIcons").start();
-  }
-
-  public int getObjectCount() {
-    return objects.size();
-  }
-
-  public void setStatus(STATUS status) {
-    this.status = status;
-    invalidate();
-  }
-
-  @Override
-  public void paint(Graphics2D g, MapView mv, Bounds bbox) {
-    final long startTime = System.currentTimeMillis();
-    final Collection<MapObject> displayedObjects = new HashSet<>();
-    synchronized (this) {
-      final MapObjectDownloadRunnable currentRunnable = downloadRunnable;
-      if (currentRunnable != null) {
-        displayedObjects.addAll(currentRunnable.getMapObjects());
-      }
-    }
-    displayedObjects.addAll(objects);
-
-    for (MapObject object : displayedObjects) {
-      final ImageIcon icon = scaledIcons.get(object.getValue());
-      if (icon != null) {
-        final Point p = mv.getPoint(object.getCoordinate());
-        g.drawImage(
-          icon.getImage(),
-          p.x - icon.getIconWidth() / 2,
-          p.y - icon.getIconHeight() / 2,
-          null
-        );
-      }
-    }
-
-    final STATUS currentStatus = status;
-    g.setFont(g.getFont().deriveFont(Font.PLAIN).deriveFont(12f));
-    g.setColor(currentStatus.getColor());
-    final FontMetrics fm = g.getFontMetrics();
-    g.fillRect(0, mv.getHeight() - fm.getAscent() - fm.getDescent(), fm.stringWidth(I18n.tr(currentStatus.message)), fm.getAscent() + fm.getDescent());
-    g.setColor(Color.BLACK);
-    g.drawString(I18n.tr(currentStatus.message), 0, mv.getHeight() - fm.getDescent());
-    Logging.debug("{0} painted in {1} milliseconds.", MapObjectLayer.class.getName(), System.currentTimeMillis() - startTime);
-  }
-
-  @Override
-  public Icon getIcon() {
-    return MapillaryPlugin.LOGO.setSize(ImageSizes.LAYER).get();
-  }
-
-  @Override
-  public String getToolTipText() {
-    return I18n.tr("Displays objects detected by Mapillary from their street view imagery");
-  }
-
-  @Override
-  public void mergeFrom(Layer from) {
-    // Not mergeable
-  }
-
-  @Override
-  public boolean isMergable(Layer other) {
-    return false;
-  }
-
-  @Override
-  public void visitBoundingBox(BoundingXYVisitor v) {
-    // Unused method enforced by the Layer class
-  }
-
-  @Override
-  public Object getInfoComponent() {
-    return null;
-  }
-
-  @Override
-  public Action[] getMenuEntries() {
-    return new Action[0];
-  }
-
-  public void scheduleDownload(final Bounds bounds) {
-    synchronized(this) {
-      if (downloadRunnable == null) {
-        downloadRunnable = new MapObjectDownloadRunnable(this, bounds);
-        new Thread(downloadRunnable).start();
-      } else {
-        nextDownloadRunnable = new MapObjectDownloadRunnable(this, bounds);
-      }
-    }
-  }
-
-  /* (non-Javadoc)
-   * @see org.openstreetmap.josm.gui.layer.Layer#destroy()
-   */
-  @Override
-  public synchronized void destroy() {
-    clearInstance();
-    super.destroy();
-  }
-
-  @Override
-  public void zoomChanged() {
-    MapView mv = MapillaryPlugin.getMapView();
-    if (mv != null) {
-      scheduleDownload(mv.getState().getViewArea().getLatLonBoundsBox());
-    }
-  }
-}
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/MapObjectDownloadRunnable.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/MapObjectDownloadRunnable.java
deleted file mode 100644
index fa6440ffc..000000000
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/MapObjectDownloadRunnable.java
+++ /dev/null
@@ -1,94 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.mapillary.io.download;
-
-import java.io.IOException;
-import java.net.URL;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.function.Function;
-
-import javax.json.Json;
-import javax.json.JsonException;
-import javax.json.JsonReader;
-
-import org.openstreetmap.josm.data.Bounds;
-import org.openstreetmap.josm.gui.Notification;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
-import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapObjectLayer;
-import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapObjectLayer.STATUS;
-import org.openstreetmap.josm.plugins.mapillary.model.MapObject;
-import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
-import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryURL.APIv3;
-import org.openstreetmap.josm.plugins.mapillary.utils.api.JsonDecoder;
-import org.openstreetmap.josm.plugins.mapillary.utils.api.JsonMapObjectDecoder;
-import org.openstreetmap.josm.tools.HttpClient;
-import org.openstreetmap.josm.tools.I18n;
-import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
-import org.openstreetmap.josm.tools.Logging;
-
-public class MapObjectDownloadRunnable implements Runnable {
-  private final Bounds bounds;
-  private final MapObjectLayer layer;
-  private static final Function<Bounds, URL> URL_GEN = APIv3::searchMapObjects;
-
-  private final Collection<MapObject> result = new HashSet<>();
-
-  public MapObjectDownloadRunnable(final MapObjectLayer layer, final Bounds bounds) {
-    this.bounds = bounds;
-    this.layer = layer;
-  }
-
-  public Collection<MapObject> getMapObjects() {
-    return result;
-  }
-
-  @Override
-  public void run() {
-    layer.setStatus(MapObjectLayer.STATUS.DOWNLOADING);
-    URL nextURL = URL_GEN.apply(bounds);
-    try {
-      while (nextURL != null && result.size() < MapillaryProperties.MAX_MAPOBJECTS.get() && !layer.isDownloadRunnableScheduled()) {
-        final int prevResultSize = result.size();
-        final long startTime = System.currentTimeMillis();
-        final HttpClient client = HttpClient.create(nextURL);
-        client.connect();
-        try (JsonReader reader = Json.createReader(client.getResponse().getContentReader())) {
-          result.addAll(JsonDecoder.decodeFeatureCollection(
-            reader.readObject(),
-            JsonMapObjectDecoder::decodeMapObject
-          ));
-        }
-        layer.invalidate();
-        BoundsDownloadRunnable.logConnectionInfo(client, String.format(
-          "%d map objects in %.2f s",
-          result.size() - prevResultSize,
-          (System.currentTimeMillis() - startTime) / 1000f
-        ));
-        nextURL = APIv3.parseNextFromLinkHeaderValue(client.getResponse().getHeaderField("Link"));
-      }
-    } catch (IOException | JsonException e) {
-      String message = I18n.tr("{0}\nCould not read map objects from URL\n{1}!", e.getLocalizedMessage(), nextURL.toString());
-      Logging.log(Logging.LEVEL_WARN, message, e);
-      new Notification(message)
-        .setIcon(MapillaryPlugin.LOGO.setSize(ImageSizes.LARGEICON).get())
-        .setDuration(Notification.TIME_LONG)
-        .show();
-      layer.setStatus(STATUS.FAILED);
-      layer.finishDownload(false);
-      return;
-    }
-    if (nextURL == null) {
-      layer.setStatus(STATUS.COMPLETE);
-    } else {
-      layer.setStatus(result.size() >= MapillaryProperties.MAX_MAPOBJECTS.get() ? STATUS.INCOMPLETE : STATUS.DOWNLOADING);
-    }
-    layer.finishDownload(nextURL == null || result.size() >= MapillaryProperties.MAX_MAPOBJECTS.get());
-    try {
-      Thread.sleep(1000); // Buffer between downloads to avoid too many downloads when e.g. panning around
-    } catch (InterruptedException e) {
-      Logging.debug(e);
-      Thread.currentThread().interrupt();
-    }
-  }
-
-}
diff --git a/test/unit/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapObjectLayerTest.java b/test/unit/org/openstreetmap/josm/plugins/mapillary/gui/layer/PointObjectLayerTest.java
similarity index 65%
rename from test/unit/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapObjectLayerTest.java
rename to test/unit/org/openstreetmap/josm/plugins/mapillary/gui/layer/PointObjectLayerTest.java
index df6b533f4..e7adb141f 100644
--- a/test/unit/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapObjectLayerTest.java
+++ b/test/unit/org/openstreetmap/josm/plugins/mapillary/gui/layer/PointObjectLayerTest.java
@@ -9,7 +9,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 
 import java.io.IOException;
 import java.net.URISyntaxException;
@@ -20,32 +19,43 @@
 
 import com.github.tomakehurst.wiremock.junit.WireMockRule;
 import com.github.tomakehurst.wiremock.matching.EqualToPattern;
+import org.awaitility.Awaitility;
+import org.awaitility.Durations;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 
 import org.openstreetmap.josm.data.Bounds;
-import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapObjectLayer.STATUS;
+import org.openstreetmap.josm.data.DataSource;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.plugins.mapillary.utils.TestUtil;
 import org.openstreetmap.josm.plugins.mapillary.utils.TestUtil.MapillaryTestRules;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
 
-public class MapObjectLayerTest {
+public class PointObjectLayerTest {
 
   @Rule
   public WireMockRule wmRule = new WireMockRule(wireMockConfig().dynamicPort());
 
   @Rule
-  public JOSMTestRules rules = new MapillaryTestRules().timeout(20000);
+  public JOSMTestRules rules = new MapillaryTestRules().timeout(20000).projection().main();
 
   private static String oldBaseUrl;
 
+  private PointObjectLayer instance;
+  private OsmDataLayer osm;
+
   @Before
   public void setUp() {
     oldBaseUrl = TestUtil.getApiV3BaseUrl();
     TestUtil.setAPIv3BaseUrl("http://localhost:" + wmRule.port() + "/");
+    osm = new OsmDataLayer(new DataSet(), "Test", null);
+    MainApplication.getLayerManager().addLayer(osm);
+    instance = new PointObjectLayer(false);
   }
 
   @After
@@ -54,13 +64,7 @@ public void cleanUp() throws IllegalArgumentException {
   }
 
   @Test
-  public void testStatusEnum() {
-    assertEquals(4, STATUS.values().length);
-    assertEquals(STATUS.COMPLETE, STATUS.valueOf("COMPLETE"));
-  }
-
-  @Test
-  public void testScheduleDownload() throws InterruptedException, URISyntaxException, IOException {
+  public void testScheduleDownload() throws URISyntaxException, IOException {
     stubFor(
       get(urlMatching("/map_features\\?.+"))
         .withQueryParam("client_id", new EqualToPattern("UTZhSnNFdGpxSEFFREUwb01GYzlXZzpjNGViMzQxMTIzMjY0MjZm"))
@@ -69,41 +73,36 @@ public void testScheduleDownload() throws InterruptedException, URISyntaxExcepti
           aResponse()
             .withStatus(200)
             .withBody(Files.readAllBytes(
-              Paths.get(MapObjectLayerTest.class.getResource("/api/v3/responses/searchMapObjects.json").toURI())
-            ))
-        )
-    );
+              Paths.get(PointObjectLayerTest.class.getResource("/api/v3/responses/searchMapObjects.json").toURI())))));
 
-    MapObjectLayer.getInstance().scheduleDownload(new Bounds(1,1,1,1));
+    osm.getDataSet().addDataSource(new DataSource(new Bounds(1, 1, 1, 1), "1/1/1/1"));
     // Wait for a maximum of 5 sec for a result
-    for (int i = 0; MapObjectLayer.getInstance().getObjectCount() <= 0 && i < 50; i++) {
-      Thread.sleep(100);
-    }
-    assertEquals(1, MapObjectLayer.getInstance().getObjectCount());
+    Awaitility.await().atMost(Durations.FIVE_SECONDS).until(() -> instance.getDataSet().allPrimitives().size() != 0);
+    assertEquals(1, instance.getDataSet().allPrimitives().size());
   }
 
   @Test
   public void testGetIcon() {
-    Icon i = MapObjectLayer.getInstance().getIcon();
+    Icon i = instance.getIcon();
     assertEquals(ImageSizes.LAYER.getAdjustedHeight(), i.getIconHeight());
     assertEquals(ImageSizes.LAYER.getAdjustedWidth(), i.getIconWidth());
   }
 
   @Test
   public void testMergable() {
-    assertFalse(MapObjectLayer.getInstance().isMergable(null));
-    MapObjectLayer.getInstance().mergeFrom(null);
+    assertFalse(instance.isMergable(null));
+    instance.mergeFrom(null);
   }
 
   @Test
   public void testInfoComponent() {
-    assertNull(MapObjectLayer.getInstance().getInfoComponent());
+    assertNotNull(instance.getInfoComponent());
   }
 
   @Test
   public void testTrivialMethods() {
-    assertNotNull(MapObjectLayer.getInstance().getToolTipText());
-    MapObjectLayer.getInstance().visitBoundingBox(null);
-    assertEquals(0, MapObjectLayer.getInstance().getMenuEntries().length);
+    assertNotNull(instance.getToolTipText());
+    instance.visitBoundingBox(null);
+    assertEquals(11, instance.getMenuEntries().length);
   }
 }

From 823573c411b36b5e25adf1fc31644341d106442b Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Tue, 28 Apr 2020 10:28:42 -0600
Subject: [PATCH 04/20] Move most dialogs into `mapillary/gui/dialog`

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 .../josm/plugins/mapillary/MapillaryLayer.java              | 4 ++--
 .../josm/plugins/mapillary/MapillaryPlugin.java             | 6 +++---
 .../plugins/mapillary/actions/MapillaryExportAction.java    | 2 +-
 .../actions/MapillarySubmitCurrentChangesetAction.java      | 2 +-
 .../plugins/mapillary/actions/MapillaryUploadAction.java    | 2 +-
 .../josm/plugins/mapillary/actions/MapillaryWalkAction.java | 2 +-
 .../gui/{ => dialog}/MapillaryChangesetDialog.java          | 3 ++-
 .../mapillary/gui/{ => dialog}/MapillaryExportDialog.java   | 2 +-
 .../mapillary/gui/{ => dialog}/MapillaryFilterDialog.java   | 6 ++++--
 .../mapillary/gui/{ => dialog}/MapillaryHistoryDialog.java  | 4 +++-
 .../mapillary/gui/{ => dialog}/MapillaryUploadDialog.java   | 2 +-
 .../mapillary/gui/{ => dialog}/MapillaryWalkDialog.java     | 2 +-
 .../io/download/MapillarySquareDownloadRunnable.java        | 2 +-
 13 files changed, 22 insertions(+), 17 deletions(-)
 rename src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/{ => dialog}/MapillaryChangesetDialog.java (97%)
 rename src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/{ => dialog}/MapillaryExportDialog.java (98%)
 rename src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/{ => dialog}/MapillaryFilterDialog.java (98%)
 rename src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/{ => dialog}/MapillaryHistoryDialog.java (97%)
 rename src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/{ => dialog}/MapillaryUploadDialog.java (97%)
 rename src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/{ => dialog}/MapillaryWalkDialog.java (96%)

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryLayer.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryLayer.java
index d5880a4dc..1ff947da0 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryLayer.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryLayer.java
@@ -59,9 +59,9 @@
 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.plugins.mapillary.cache.CacheUtils;
-import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryChangesetDialog;
-import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryFilterDialog;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryMainDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryChangesetDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryFilterDialog;
 import org.openstreetmap.josm.plugins.mapillary.history.MapillaryRecord;
 import org.openstreetmap.josm.plugins.mapillary.history.commands.CommandDelete;
 import org.openstreetmap.josm.plugins.mapillary.io.download.MapillaryDownloader;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java
index 4e06499ef..acdc11531 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java
@@ -32,12 +32,12 @@
 import org.openstreetmap.josm.plugins.mapillary.actions.MapillaryUploadAction;
 import org.openstreetmap.josm.plugins.mapillary.actions.MapillaryWalkAction;
 import org.openstreetmap.josm.plugins.mapillary.actions.MapillaryZoomAction;
-import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryChangesetDialog;
-import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryFilterDialog;
-import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryHistoryDialog;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryMainDialog;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryPreferenceSetting;
+import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryChangesetDialog;
 import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryExpertFilterDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryFilterDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryHistoryDialog;
 import org.openstreetmap.josm.plugins.mapillary.gui.imageinfo.ImageInfoHelpPopup;
 import org.openstreetmap.josm.plugins.mapillary.gui.imageinfo.ImageInfoPanel;
 import org.openstreetmap.josm.plugins.mapillary.gui.layer.PointObjectLayer;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryExportAction.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryExportAction.java
index edf990aaf..ed3ec93e0 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryExportAction.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryExportAction.java
@@ -22,7 +22,7 @@
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImportedImage;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
-import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryExportDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryExportDialog;
 import org.openstreetmap.josm.plugins.mapillary.io.export.MapillaryExportManager;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillarySubmitCurrentChangesetAction.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillarySubmitCurrentChangesetAction.java
index c020a7a30..233a3d016 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillarySubmitCurrentChangesetAction.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillarySubmitCurrentChangesetAction.java
@@ -20,7 +20,7 @@
 import org.openstreetmap.josm.gui.Notification;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryLocationChangeset;
-import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryChangesetDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryChangesetDialog;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryURL.APIv3;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryUtils;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryUploadAction.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryUploadAction.java
index fa8af0abc..a23986ec0 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryUploadAction.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryUploadAction.java
@@ -13,7 +13,7 @@
 import org.openstreetmap.josm.plugins.mapillary.MapillaryDataListener;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
-import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryUploadDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryUploadDialog;
 import org.openstreetmap.josm.plugins.mapillary.oauth.UploadUtils;
 import org.openstreetmap.josm.tools.I18n;
 import org.openstreetmap.josm.tools.ImageProvider;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryWalkAction.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryWalkAction.java
index 55de425ae..61123621c 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryWalkAction.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryWalkAction.java
@@ -18,7 +18,7 @@
 import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryMainDialog;
-import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryWalkDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryWalkDialog;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
 
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryChangesetDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryChangesetDialog.java
similarity index 97%
rename from src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryChangesetDialog.java
rename to src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryChangesetDialog.java
index e413ffb03..fd9d9af83 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryChangesetDialog.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryChangesetDialog.java
@@ -1,5 +1,5 @@
 // License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.mapillary.gui;
+package org.openstreetmap.josm.plugins.mapillary.gui.dialog;
 
 import static org.openstreetmap.josm.tools.I18n.tr;
 
@@ -30,6 +30,7 @@
 import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryLocationChangeset;
 import org.openstreetmap.josm.plugins.mapillary.actions.MapillarySubmitCurrentChangesetAction;
+import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryImageTreeCellRenderer;
 import org.openstreetmap.josm.plugins.mapillary.history.MapillaryRecord;
 import org.openstreetmap.josm.plugins.mapillary.history.commands.MapillaryCommand;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryChangesetListener;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryExportDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryExportDialog.java
similarity index 98%
rename from src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryExportDialog.java
rename to src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryExportDialog.java
index 10d43d986..0647af825 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryExportDialog.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryExportDialog.java
@@ -1,5 +1,5 @@
 // License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.mapillary.gui;
+package org.openstreetmap.josm.plugins.mapillary.gui.dialog;
 
 import static org.openstreetmap.josm.tools.I18n.tr;
 
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryFilterDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryFilterDialog.java
similarity index 98%
rename from src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryFilterDialog.java
rename to src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryFilterDialog.java
index b891ec7cd..d00b42bff 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryFilterDialog.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryFilterDialog.java
@@ -1,5 +1,5 @@
 // License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.mapillary.gui;
+package org.openstreetmap.josm.plugins.mapillary.gui.dialog;
 
 import static org.openstreetmap.josm.tools.I18n.tr;
 
@@ -45,7 +45,9 @@
 import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.data.mapillary.OrganizationRecord;
 import org.openstreetmap.josm.plugins.mapillary.data.mapillary.OrganizationRecord.OrganizationRecordListener;
-import org.openstreetmap.josm.plugins.mapillary.gui.dialog.TrafficSignFilter;
+import org.openstreetmap.josm.plugins.mapillary.gui.IDatePicker;
+import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryFilterChooseSigns;
+import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryPreferenceSetting;
 import org.openstreetmap.josm.plugins.mapillary.model.ImageDetection;
 import org.openstreetmap.josm.plugins.mapillary.model.UserProfile;
 import org.openstreetmap.josm.plugins.mapillary.oauth.MapillaryLoginListener;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryHistoryDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryHistoryDialog.java
similarity index 97%
rename from src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryHistoryDialog.java
rename to src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryHistoryDialog.java
index ec12a24e3..6caea3f7f 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryHistoryDialog.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryHistoryDialog.java
@@ -1,5 +1,5 @@
 // License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.mapillary.gui;
+package org.openstreetmap.josm.plugins.mapillary.gui.dialog;
 
 import static org.openstreetmap.josm.tools.I18n.tr;
 
@@ -31,6 +31,8 @@
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.SideButton;
 import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryImageTreeCellRenderer;
+import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryPreferenceSetting;
 import org.openstreetmap.josm.plugins.mapillary.history.MapillaryRecord;
 import org.openstreetmap.josm.plugins.mapillary.history.MapillaryRecordListener;
 import org.openstreetmap.josm.plugins.mapillary.history.commands.CommandDelete;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryUploadDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryUploadDialog.java
similarity index 97%
rename from src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryUploadDialog.java
rename to src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryUploadDialog.java
index ec2c8d68b..7b4dc9b30 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryUploadDialog.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryUploadDialog.java
@@ -1,5 +1,5 @@
 // License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.mapillary.gui;
+package org.openstreetmap.josm.plugins.mapillary.gui.dialog;
 
 import static org.openstreetmap.josm.tools.I18n.tr;
 
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryWalkDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryWalkDialog.java
similarity index 96%
rename from src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryWalkDialog.java
rename to src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryWalkDialog.java
index 64085993e..bb3bc4c03 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryWalkDialog.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryWalkDialog.java
@@ -1,5 +1,5 @@
 // License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.mapillary.gui;
+package org.openstreetmap.josm.plugins.mapillary.gui.dialog;
 
 import static org.openstreetmap.josm.tools.I18n.tr;
 
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/MapillarySquareDownloadRunnable.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/MapillarySquareDownloadRunnable.java
index 74b357734..89397cc58 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/MapillarySquareDownloadRunnable.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/MapillarySquareDownloadRunnable.java
@@ -7,8 +7,8 @@
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
-import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryFilterDialog;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryMainDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryFilterDialog;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryUtils;
 import org.openstreetmap.josm.plugins.mapillary.utils.PluginState;
 import org.openstreetmap.josm.tools.Logging;

From 1a78c8318a92e39ddc523c6f8e9aef6e264d3c16 Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Tue, 28 Apr 2020 10:41:04 -0600
Subject: [PATCH 05/20] Remove minimum detections UI component

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 .../gui/dialog/TrafficSignFilter.java         | 47 ++++---------------
 1 file changed, 8 insertions(+), 39 deletions(-)

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/TrafficSignFilter.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/TrafficSignFilter.java
index e8f17865e..82a1df142 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/TrafficSignFilter.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/TrafficSignFilter.java
@@ -74,8 +74,8 @@ public TrafficSignFilter() {
     layers.setLayout(new FlowLayout(FlowLayout.LEFT));
     layers.add(new JLabel(I18n.tr("Layer")));
     for (String[] layer : Arrays.asList(
-      new String[] { "trafficsigns", I18n.marktr("Traffic Signs") }, new String[] { "points", I18n.marktr("Point Objects") }
-    )) {
+      new String[] { "trafficsigns", I18n.marktr("Traffic Signs") },
+      new String[] { "points", I18n.marktr("Point Objects") })) {
       JCheckBox lbox = new JCheckBox(I18n.tr(layer[1]));
       layers.add(lbox);
       lbox.addItemListener(TrafficSignFilter::updateLayers);
@@ -84,23 +84,6 @@ public TrafficSignFilter() {
     }
     add(layers);
 
-    /* Filter minimum detections */
-    add(new JLabel(I18n.tr("Minimum object detections")));
-    JSpinner minDetections = new JSpinner();
-    minDetections.setModel(new SpinnerNumberModel(0, 0, 100, 1));
-    add(minDetections, GBC.eol());
-    Filter minDetectionFilter = MapillaryExpertFilterDialog.getInstance().getFilterModel().getFilters().parallelStream()
-      .filter(p -> p.text.contains("min_detections")).findFirst().orElseGet(() -> {
-        Filter filter = new Filter();
-        filter.enable = false;
-        filter.hiding = true;
-        filter.text = "min_detections < 0";
-        MapillaryExpertFilterDialog.getInstance().getFilterModel().addFilter(filter);
-        return filter;
-      });
-    minDetections.addChangeListener(l -> updateMinDetectionFilter(minDetections, minDetectionFilter));
-    /* End filter minimum detections */
-
     /* Filter signs */
     filterField = new FilterField().filter(this::filterButtons);
     filterField.setToolTipText(I18n.tr("Filter Mapillary Detections"));
@@ -169,9 +152,8 @@ private void updateDetectionPage(int difference) {
   private void updateShown(SpinnerNumberModel model) {
     buttons.parallelStream().forEach(i -> SwingUtilities.invokeLater(() -> i.setVisible(false)));
     buttons.stream().filter(i -> i.isFiltered(filterField.getText())).skip(
-      detectionPage * model.getNumber().longValue()
-    ).limit(model.getNumber().longValue()
-    ).forEach(i -> SwingUtilities.invokeLater(() -> i.setVisible(true)));
+      detectionPage * model.getNumber().longValue()).limit(model.getNumber().longValue())
+      .forEach(i -> SwingUtilities.invokeLater(() -> i.setVisible(true)));
     long notSelected = buttons.parallelStream().filter(Component::isVisible).filter(i -> !i.isSelected()).count();
     long selected = buttons.parallelStream().filter(Component::isVisible).filter(ImageCheckBoxButton::isSelected)
       .count();
@@ -201,8 +183,7 @@ private static void createFirstLastSeen(JPanel panel, String firstLast) {
   }
 
   private static void updateDates(
-    String position, IDatePicker<?> modified, IDatePicker<?> firstSeen, IDatePicker<?> lastSeen
-  ) {
+    String position, IDatePicker<?> modified, IDatePicker<?> firstSeen, IDatePicker<?> lastSeen) {
     LocalDate start = firstSeen.getDate();
     LocalDate end = lastSeen.getDate();
     if (start != null && end != null) {
@@ -237,17 +218,6 @@ private static void updateDates(
     }
   }
 
-  private static void updateMinDetectionFilter(JSpinner minDetections, Filter filter) {
-    if (minDetections.getModel() instanceof SpinnerNumberModel) {
-      SpinnerNumberModel model = (SpinnerNumberModel) minDetections.getModel();
-      filter.enable = !Integer.valueOf(0).equals(model.getNumber());
-      if (filter.enable && model.getNumber() != null) {
-        filter.text = "detections_num < " + model.getNumber();
-      }
-      doFilterAddRemoveWork(filter);
-    }
-  }
-
   private static void doFilterAddRemoveWork(Filter filter) {
     int index = MapillaryExpertFilterDialog.getInstance().getFilterModel().getFilters().indexOf(filter);
     if (index < 0 && filter.enable && !filter.text.isEmpty()) {
@@ -273,7 +243,7 @@ private void toggleVisible(boolean check) {
     }
     MapillaryExpertFilterDialog.getInstance().getFilterModel().pauseUpdates();
     List<Future<?>> futures = buttons.stream().filter(ImageCheckBoxButton::isVisible)
-        .map(b -> b.setSelected(check)).filter(Objects::nonNull).collect(Collectors.toList());
+      .map(b -> b.setSelected(check)).filter(Objects::nonNull).collect(Collectors.toList());
 
     for (Future<?> future : futures) {
       try {
@@ -297,8 +267,7 @@ private void addButtons() {
 
   private void filterButtons(String expr) {
     SwingUtilities.invokeLater(
-      () -> buttons.stream().forEach(b -> b.setVisible(b.isFiltered(expr) && (!showRelevant || b.isRelevant())))
-    );
+      () -> buttons.stream().forEach(b -> b.setVisible(b.isFiltered(expr) && (!showRelevant || b.isRelevant()))));
   }
 
   public void getIcons(JComponent panel, String type) {
@@ -309,7 +278,7 @@ public void getIcons(JComponent panel, String type) {
     String directory = "mapillary_sprite_source/package_" + type;
     try {
       List<String> files = IOUtils.readLines(ResourceProvider.getResourceAsStream("/images/" + directory),
-          Charsets.UTF_8.name());
+        Charsets.UTF_8.name());
       Collections.sort(files);
       for (String file : files) {
         try {

From 7c106c15e4cba6fa2a5d33fd5d03b016f3214507 Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Tue, 28 Apr 2020 13:13:39 -0600
Subject: [PATCH 06/20] Move MapillaryLayer to appropriate package

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 .../josm/plugins/mapillary/MapillaryData.java          |  3 ++-
 .../josm/plugins/mapillary/MapillaryImage.java         |  1 +
 .../josm/plugins/mapillary/MapillaryPlugin.java        |  1 +
 .../mapillary/actions/MapillaryDownloadAction.java     |  2 +-
 .../mapillary/actions/MapillaryDownloadViewAction.java |  2 +-
 .../mapillary/actions/MapillaryExportAction.java       |  2 +-
 .../plugins/mapillary/actions/MapillaryJoinAction.java |  2 +-
 .../actions/MapillarySubmitCurrentChangesetAction.java |  2 +-
 .../mapillary/actions/MapillaryUploadAction.java       |  2 +-
 .../plugins/mapillary/actions/MapillaryWalkAction.java |  2 +-
 .../plugins/mapillary/actions/MapillaryZoomAction.java |  2 +-
 .../mapillary/actions/SelectNextImageAction.java       |  2 +-
 .../josm/plugins/mapillary/actions/WalkThread.java     |  2 +-
 .../josm/plugins/mapillary/actions/package-info.java   |  2 +-
 .../plugins/mapillary/gui/MapillaryImageDisplay.java   |  2 +-
 .../plugins/mapillary/gui/MapillaryMainDialog.java     |  2 +-
 .../gui/dialog/ChooseGeoImageLayersDialog.java         |  2 +-
 .../mapillary/gui/dialog/ImportMethodDialog.java       |  2 +-
 .../mapillary/gui/dialog/MapillaryChangesetDialog.java |  2 +-
 .../mapillary/gui/dialog/MapillaryExportDialog.java    |  2 +-
 .../mapillary/gui/dialog/MapillaryFilterDialog.java    |  2 +-
 .../mapillary/gui/dialog/MapillaryUploadDialog.java    |  2 +-
 .../mapillary/{ => gui/layer}/MapillaryLayer.java      | 10 +++++++++-
 .../plugins/mapillary/gui/layer/PointObjectLayer.java  |  1 -
 .../mapillary/history/commands/CommandDelete.java      |  2 +-
 .../mapillary/history/commands/CommandImport.java      |  2 +-
 .../mapillary/history/commands/CommandMove.java        |  2 +-
 .../mapillary/history/commands/CommandTurn.java        |  2 +-
 .../io/download/DetectionsDownloadRunnable.java        |  2 +-
 .../mapillary/io/download/MapillaryDownloader.java     |  2 +-
 .../io/download/MapillarySquareDownloadRunnable.java   |  2 +-
 .../io/download/SequenceDownloadRunnable.java          |  2 +-
 .../josm/plugins/mapillary/mode/AbstractMode.java      |  2 +-
 .../josm/plugins/mapillary/mode/JoinMode.java          |  2 +-
 .../josm/plugins/mapillary/mode/SelectMode.java        |  2 +-
 .../josm/plugins/mapillary/mode/package-info.java      |  2 +-
 .../josm/plugins/mapillary/utils/MapillaryUtils.java   |  2 +-
 .../mapillary/{ => gui/layer}/MapillaryLayerTest.java  |  6 +++++-
 .../plugins/mapillary/history/MapillaryRecordTest.java |  2 +-
 .../io/download/SequenceDownloadRunnableTest.java      |  2 +-
 40 files changed, 52 insertions(+), 38 deletions(-)
 rename src/main/java/org/openstreetmap/josm/plugins/mapillary/{ => gui/layer}/MapillaryLayer.java (97%)
 rename test/unit/org/openstreetmap/josm/plugins/mapillary/{ => gui/layer}/MapillaryLayerTest.java (93%)

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryData.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryData.java
index ba1caecb9..89de5c96a 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryData.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryData.java
@@ -33,6 +33,7 @@
 import org.openstreetmap.josm.plugins.mapillary.cache.Caches;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryMainDialog;
 import org.openstreetmap.josm.plugins.mapillary.gui.imageinfo.ImageInfoPanel;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.gui.layer.PointObjectLayer;
 import org.openstreetmap.josm.plugins.mapillary.model.ImageDetection;
 import org.openstreetmap.josm.plugins.mapillary.oauth.MapillaryUser;
@@ -85,7 +86,7 @@ public class MapillaryData implements Data {
   /**
    * Creates a new object and adds the initial set of listeners.
    */
-  protected MapillaryData() {
+  public MapillaryData() {
     this.selectedImage = null;
     this.dataSources = new ArrayList<>();
 
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryImage.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryImage.java
index 23d1e4586..858065c82 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryImage.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryImage.java
@@ -9,6 +9,7 @@
 
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.gpx.GpxImageEntry;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.model.ImageDetection;
 import org.openstreetmap.josm.plugins.mapillary.model.UserProfile;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryColorScheme;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java
index acdc11531..f87b96682 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java
@@ -40,6 +40,7 @@
 import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryHistoryDialog;
 import org.openstreetmap.josm.plugins.mapillary.gui.imageinfo.ImageInfoHelpPopup;
 import org.openstreetmap.josm.plugins.mapillary.gui.imageinfo.ImageInfoPanel;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.gui.layer.PointObjectLayer;
 import org.openstreetmap.josm.plugins.mapillary.oauth.MapillaryUser;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryDownloadAction.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryDownloadAction.java
index 1a63e8796..0fbcbeea1 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryDownloadAction.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryDownloadAction.java
@@ -13,8 +13,8 @@
 import org.openstreetmap.josm.gui.dialogs.LayerListDialog.LayerListModel;
 import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
 import org.openstreetmap.josm.tools.Logging;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryDownloadViewAction.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryDownloadViewAction.java
index b79798be2..409337292 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryDownloadViewAction.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryDownloadViewAction.java
@@ -7,8 +7,8 @@
 import org.openstreetmap.josm.actions.JosmAction;
 import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeEvent;
 import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeListener;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.io.download.MapillaryDownloader;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
 import org.openstreetmap.josm.tools.I18n;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryExportAction.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryExportAction.java
index ed3ec93e0..0e6eabcac 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryExportAction.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryExportAction.java
@@ -20,9 +20,9 @@
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImage;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImportedImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
 import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryExportDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.io.export.MapillaryExportManager;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryJoinAction.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryJoinAction.java
index 37c01e6e7..cd245e694 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryJoinAction.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryJoinAction.java
@@ -7,7 +7,7 @@
 
 import org.openstreetmap.josm.actions.JosmAction;
 import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.mode.JoinMode;
 import org.openstreetmap.josm.plugins.mapillary.mode.SelectMode;
 import org.openstreetmap.josm.tools.ImageProvider;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillarySubmitCurrentChangesetAction.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillarySubmitCurrentChangesetAction.java
index 233a3d016..1bdadb49b 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillarySubmitCurrentChangesetAction.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillarySubmitCurrentChangesetAction.java
@@ -18,9 +18,9 @@
 
 import org.openstreetmap.josm.actions.JosmAction;
 import org.openstreetmap.josm.gui.Notification;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryLocationChangeset;
 import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryChangesetDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryURL.APIv3;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryUtils;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryUploadAction.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryUploadAction.java
index a23986ec0..8784a70b6 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryUploadAction.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryUploadAction.java
@@ -11,9 +11,9 @@
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryDataListener;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
 import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryUploadDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.oauth.UploadUtils;
 import org.openstreetmap.josm.tools.I18n;
 import org.openstreetmap.josm.tools.ImageProvider;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryWalkAction.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryWalkAction.java
index 61123621c..d82d1da6d 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryWalkAction.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryWalkAction.java
@@ -15,10 +15,10 @@
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryDataListener;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryMainDialog;
 import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryWalkDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
 
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryZoomAction.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryZoomAction.java
index 4181c063f..b07854b85 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryZoomAction.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryZoomAction.java
@@ -9,8 +9,8 @@
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryDataListener;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
 
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/SelectNextImageAction.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/SelectNextImageAction.java
index b5447eb9e..e7a67e428 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/SelectNextImageAction.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/SelectNextImageAction.java
@@ -9,7 +9,7 @@
 
 import org.openstreetmap.josm.actions.JosmAction;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
 import org.openstreetmap.josm.tools.Shortcut;
 
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/WalkThread.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/WalkThread.java
index ce0ba3273..94e376a98 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/WalkThread.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/WalkThread.java
@@ -8,11 +8,11 @@
 import org.openstreetmap.josm.plugins.mapillary.MapillaryData;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryDataListener;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
 import org.openstreetmap.josm.plugins.mapillary.cache.CacheUtils;
 import org.openstreetmap.josm.plugins.mapillary.cache.MapillaryCache;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryMainDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.tools.I18n;
 import org.openstreetmap.josm.tools.Logging;
 
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/package-info.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/package-info.java
index ff82ba55b..8dd706f60 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/package-info.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/package-info.java
@@ -1,7 +1,7 @@
 // License: GPL. For details, see LICENSE file.
 /**
  * Actions that are normally attached to a button or a menu item and are then executed
- * to manipulate the {@link org.openstreetmap.josm.plugins.mapillary.MapillaryLayer} when
+ * to manipulate the {@link org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer} when
  * the button is clicked.
  */
 package org.openstreetmap.josm.plugins.mapillary.actions;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryImageDisplay.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryImageDisplay.java
index 78043c06c..b528f97e3 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryImageDisplay.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryImageDisplay.java
@@ -36,8 +36,8 @@
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.layer.LayerManager;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.actions.MapillaryDownloadAction;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.gui.layer.PointObjectLayer;
 import org.openstreetmap.josm.plugins.mapillary.gui.panorama.CameraPlane;
 import org.openstreetmap.josm.plugins.mapillary.gui.panorama.UVMapping;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryMainDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryMainDialog.java
index d757e2312..33d6eeff7 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryMainDialog.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryMainDialog.java
@@ -38,13 +38,13 @@
 import org.openstreetmap.josm.plugins.mapillary.MapillaryDataListener;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImage;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImportedImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
 import org.openstreetmap.josm.plugins.mapillary.actions.SelectNextImageAction;
 import org.openstreetmap.josm.plugins.mapillary.actions.WalkListener;
 import org.openstreetmap.josm.plugins.mapillary.actions.WalkThread;
 import org.openstreetmap.josm.plugins.mapillary.cache.MapillaryCache;
 import org.openstreetmap.josm.plugins.mapillary.gui.imageinfo.ImageInfoHelpPopup;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.model.UserProfile;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
 import org.openstreetmap.josm.tools.ImageProvider;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/ChooseGeoImageLayersDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/ChooseGeoImageLayersDialog.java
index 0e2393b53..619dd8f02 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/ChooseGeoImageLayersDialog.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/ChooseGeoImageLayersDialog.java
@@ -28,10 +28,10 @@
 import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImportedImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
 import org.openstreetmap.josm.plugins.mapillary.MapillarySequence;
 import org.openstreetmap.josm.plugins.mapillary.actions.MapillaryImportAction;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.tools.I18n;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.Logging;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/ImportMethodDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/ImportMethodDialog.java
index bbec0c1be..26c77b4d9 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/ImportMethodDialog.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/ImportMethodDialog.java
@@ -25,10 +25,10 @@
 import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer;
 import org.openstreetmap.josm.gui.util.GuiHelper;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
 import org.openstreetmap.josm.plugins.mapillary.MapillarySequence;
 import org.openstreetmap.josm.plugins.mapillary.actions.MapillaryImportAction;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.utils.ImageImportUtil;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
 import org.openstreetmap.josm.tools.I18n;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryChangesetDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryChangesetDialog.java
index fd9d9af83..40fa042bb 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryChangesetDialog.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryChangesetDialog.java
@@ -27,10 +27,10 @@
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.SideButton;
 import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryLocationChangeset;
 import org.openstreetmap.josm.plugins.mapillary.actions.MapillarySubmitCurrentChangesetAction;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryImageTreeCellRenderer;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.history.MapillaryRecord;
 import org.openstreetmap.josm.plugins.mapillary.history.commands.MapillaryCommand;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryChangesetListener;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryExportDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryExportDialog.java
index 0647af825..7b6d0036c 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryExportDialog.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryExportDialog.java
@@ -18,7 +18,7 @@
 
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImage;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImportedImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 
 /**
  * GUI for exporting images.
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryFilterDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryFilterDialog.java
index d00b42bff..d181ba351 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryFilterDialog.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryFilterDialog.java
@@ -42,12 +42,12 @@
 import org.openstreetmap.josm.plugins.mapillary.MapillaryDataListener;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImage;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImportedImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.data.mapillary.OrganizationRecord;
 import org.openstreetmap.josm.plugins.mapillary.data.mapillary.OrganizationRecord.OrganizationRecordListener;
 import org.openstreetmap.josm.plugins.mapillary.gui.IDatePicker;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryFilterChooseSigns;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryPreferenceSetting;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.model.ImageDetection;
 import org.openstreetmap.josm.plugins.mapillary.model.UserProfile;
 import org.openstreetmap.josm.plugins.mapillary.oauth.MapillaryLoginListener;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryUploadDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryUploadDialog.java
index 7b4dc9b30..6d8046469 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryUploadDialog.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryUploadDialog.java
@@ -11,7 +11,7 @@
 import javax.swing.JRadioButton;
 
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImportedImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.oauth.MapillaryUser;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
 
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryLayer.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
similarity index 97%
rename from src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryLayer.java
rename to src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
index 1ff947da0..4940012ae 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryLayer.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
@@ -1,5 +1,5 @@
 // License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.mapillary;
+package org.openstreetmap.josm.plugins.mapillary.gui.layer;
 
 import java.awt.AlphaComposite;
 import java.awt.BasicStroke;
@@ -58,6 +58,14 @@
 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
+import org.openstreetmap.josm.plugins.mapillary.MapillaryData;
+import org.openstreetmap.josm.plugins.mapillary.MapillaryDataListener;
+import org.openstreetmap.josm.plugins.mapillary.MapillaryImage;
+import org.openstreetmap.josm.plugins.mapillary.MapillaryImportedImage;
+import org.openstreetmap.josm.plugins.mapillary.MapillaryLocationChangeset;
+import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
+import org.openstreetmap.josm.plugins.mapillary.MapillarySequence;
 import org.openstreetmap.josm.plugins.mapillary.cache.CacheUtils;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryMainDialog;
 import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryChangesetDialog;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/PointObjectLayer.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/PointObjectLayer.java
index 472ea8232..cd2a0df3b 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/PointObjectLayer.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/PointObjectLayer.java
@@ -97,7 +97,6 @@
 import org.openstreetmap.josm.plugins.mapillary.MapillaryData;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryDataListener;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
 import org.openstreetmap.josm.plugins.mapillary.data.osm.event.FilterEventListener;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryMainDialog;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/history/commands/CommandDelete.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/history/commands/CommandDelete.java
index d81848a18..bc9deb6b3 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/history/commands/CommandDelete.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/history/commands/CommandDelete.java
@@ -8,7 +8,7 @@
 import java.util.Set;
 
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 
 /**
  * Command used to delete a set of images.
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/history/commands/CommandImport.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/history/commands/CommandImport.java
index 3371c97f6..7e123076a 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/history/commands/CommandImport.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/history/commands/CommandImport.java
@@ -6,7 +6,7 @@
 import java.util.Set;
 
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 
 /**
  * Imports a set of images stored locally.
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/history/commands/CommandMove.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/history/commands/CommandMove.java
index 33d2cc63b..30456a39b 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/history/commands/CommandMove.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/history/commands/CommandMove.java
@@ -6,7 +6,7 @@
 import java.util.Set;
 
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 
 /**
  * Command created when an image's position is changed.
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/history/commands/CommandTurn.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/history/commands/CommandTurn.java
index 858a4d4dc..329a2f4f0 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/history/commands/CommandTurn.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/history/commands/CommandTurn.java
@@ -6,7 +6,7 @@
 import java.util.Set;
 
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 
 /**
  * Command created when an image's direction is changed.
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/DetectionsDownloadRunnable.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/DetectionsDownloadRunnable.java
index 4dfe56fd4..ec7098c15 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/DetectionsDownloadRunnable.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/DetectionsDownloadRunnable.java
@@ -17,8 +17,8 @@
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryData;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryMainDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.model.ImageDetection;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryURL.APIv3;
 import org.openstreetmap.josm.plugins.mapillary.utils.api.JsonDecoder;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/MapillaryDownloader.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/MapillaryDownloader.java
index 372c17ae8..4f9e0a3ec 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/MapillaryDownloader.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/MapillaryDownloader.java
@@ -10,8 +10,8 @@
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.gui.Notification;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
 import org.openstreetmap.josm.tools.I18n;
 import org.openstreetmap.josm.tools.Logging;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/MapillarySquareDownloadRunnable.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/MapillarySquareDownloadRunnable.java
index 89397cc58..8de7094ef 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/MapillarySquareDownloadRunnable.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/MapillarySquareDownloadRunnable.java
@@ -6,9 +6,9 @@
 
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryMainDialog;
 import org.openstreetmap.josm.plugins.mapillary.gui.dialog.MapillaryFilterDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryUtils;
 import org.openstreetmap.josm.plugins.mapillary.utils.PluginState;
 import org.openstreetmap.josm.tools.Logging;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/SequenceDownloadRunnable.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/SequenceDownloadRunnable.java
index 88376b6dc..f73935a23 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/SequenceDownloadRunnable.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/SequenceDownloadRunnable.java
@@ -14,8 +14,8 @@
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryData;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillarySequence;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryURL.APIv3;
 import org.openstreetmap.josm.plugins.mapillary.utils.api.JsonDecoder;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/AbstractMode.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/AbstractMode.java
index 852769ad0..e592d273e 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/AbstractMode.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/AbstractMode.java
@@ -12,7 +12,7 @@
 import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.io.download.MapillaryDownloader;
 
 /**
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/JoinMode.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/JoinMode.java
index ffab687bb..8fee86c1c 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/JoinMode.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/JoinMode.java
@@ -15,7 +15,7 @@
 import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImportedImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.history.MapillaryRecord;
 import org.openstreetmap.josm.plugins.mapillary.history.commands.CommandJoin;
 import org.openstreetmap.josm.plugins.mapillary.history.commands.CommandUnjoin;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/SelectMode.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/SelectMode.java
index 86760da0f..5ab8957be 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/SelectMode.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/SelectMode.java
@@ -21,8 +21,8 @@
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryData;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryMainDialog;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.history.MapillaryRecord;
 import org.openstreetmap.josm.plugins.mapillary.history.commands.CommandMove;
 import org.openstreetmap.josm.plugins.mapillary.history.commands.CommandTurn;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/package-info.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/package-info.java
index d30d28b92..1279f1d0b 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/package-info.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/package-info.java
@@ -1,6 +1,6 @@
 // License: GPL. For details, see LICENSE file.
 /**
- * The different modes that the {@link org.openstreetmap.josm.plugins.mapillary.MapillaryLayer} can be in.
+ * The different modes that the {@link org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer} can be in.
  * <br>
  * Currently there are two of them:
  * <ul>
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryUtils.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryUtils.java
index cc755c812..3930cb816 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryUtils.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryUtils.java
@@ -16,8 +16,8 @@
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillarySequence;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.tools.I18n;
 
 /**
diff --git a/test/unit/org/openstreetmap/josm/plugins/mapillary/MapillaryLayerTest.java b/test/unit/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayerTest.java
similarity index 93%
rename from test/unit/org/openstreetmap/josm/plugins/mapillary/MapillaryLayerTest.java
rename to test/unit/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayerTest.java
index e029ca344..3715696bd 100644
--- a/test/unit/org/openstreetmap/josm/plugins/mapillary/MapillaryLayerTest.java
+++ b/test/unit/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayerTest.java
@@ -1,5 +1,5 @@
 // License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.plugins.mapillary;
+package org.openstreetmap.josm.plugins.mapillary.gui.layer;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -20,6 +20,10 @@
 import org.openstreetmap.josm.gui.layer.ImageryLayer;
 import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
+import org.openstreetmap.josm.plugins.mapillary.MapillaryImage;
+import org.openstreetmap.josm.plugins.mapillary.MapillaryImportedImage;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.utils.TestUtil.MapillaryTestRules;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 
diff --git a/test/unit/org/openstreetmap/josm/plugins/mapillary/history/MapillaryRecordTest.java b/test/unit/org/openstreetmap/josm/plugins/mapillary/history/MapillaryRecordTest.java
index 5e89002b1..efdc13680 100644
--- a/test/unit/org/openstreetmap/josm/plugins/mapillary/history/MapillaryRecordTest.java
+++ b/test/unit/org/openstreetmap/josm/plugins/mapillary/history/MapillaryRecordTest.java
@@ -19,7 +19,7 @@
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryData;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.history.commands.CommandDelete;
 import org.openstreetmap.josm.plugins.mapillary.history.commands.CommandImport;
 import org.openstreetmap.josm.plugins.mapillary.history.commands.CommandJoin;
diff --git a/test/unit/org/openstreetmap/josm/plugins/mapillary/io/download/SequenceDownloadRunnableTest.java b/test/unit/org/openstreetmap/josm/plugins/mapillary/io/download/SequenceDownloadRunnableTest.java
index 448463ece..87c6a1e23 100644
--- a/test/unit/org/openstreetmap/josm/plugins/mapillary/io/download/SequenceDownloadRunnableTest.java
+++ b/test/unit/org/openstreetmap/josm/plugins/mapillary/io/download/SequenceDownloadRunnableTest.java
@@ -24,7 +24,7 @@
 import org.junit.Test;
 
 import org.openstreetmap.josm.data.Bounds;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
+import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.oauth.MapillaryUser;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
 import org.openstreetmap.josm.plugins.mapillary.utils.TestUtil;

From 21642ec0d47d499027ddf9a943aad9c323b37b1c Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Tue, 28 Apr 2020 15:10:41 -0600
Subject: [PATCH 07/20] Revert "Add preference to change number of clicks for
 deselection"

This reverts commit a286f3bb25739c97a3ded20214e37ce9b6a5aeea.
---
 .../openstreetmap/josm/plugins/mapillary/mode/SelectMode.java  | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/SelectMode.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/SelectMode.java
index 5ab8957be..b110ac084 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/SelectMode.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/SelectMode.java
@@ -27,7 +27,6 @@
 import org.openstreetmap.josm.plugins.mapillary.history.commands.CommandMove;
 import org.openstreetmap.josm.plugins.mapillary.history.commands.CommandTurn;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
-import org.openstreetmap.josm.spi.preferences.Config;
 
 /**
  * Handles the input event related with the layer. Mainly clicks.
@@ -62,7 +61,7 @@ public void mousePressed(MouseEvent e) {
     }
 
     if (MainApplication.getLayerManager().getActiveLayer() instanceof MapillaryLayer) {
-      if (e.getClickCount() == Config.getPref().getInt("mapillary.image.deselect.click.count", 3)) { // Triple click
+      if (e.getClickCount() == 2) { // Double click
         if (e.getButton() == MouseEvent.BUTTON1 && MapillaryLayer.getInstance().getData().getSelectedImage() != null) {
           MapillaryLayer.getInstance().getData().addMultiSelectedImage(closest.getSequence().getImages());
         }

From 43da6ee6a5c3a036e4dfa4cabea355c8ca00aa0a Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Tue, 28 Apr 2020 15:14:26 -0600
Subject: [PATCH 08/20] Modify Mapillary image layer to reduce lag

* Mapillary Image layer no longer draws circle + angle at low zoom
  levels (0-17), instead drawing a line
* Modify JoinMode/SelectMode to implement MapMode (through
  AbstractMode). This partially fixes #21.
* Actually fix #115 (I replaced the wrong double-click initially)

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 .../mapillary/gui/layer/MapillaryLayer.java   | 153 +++++++++++-------
 .../plugins/mapillary/mode/AbstractMode.java  |  37 ++++-
 .../josm/plugins/mapillary/mode/JoinMode.java |  20 +--
 .../plugins/mapillary/mode/SelectMode.java    |  40 +++--
 .../gui/layer/MapillaryLayerTest.java         |   1 -
 5 files changed, 155 insertions(+), 96 deletions(-)

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
index 4940012ae..01b568f7b 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
@@ -12,6 +12,7 @@
 import java.awt.RenderingHints;
 import java.awt.TexturePaint;
 import java.awt.event.ActionEvent;
+import java.awt.geom.Ellipse2D;
 import java.awt.geom.Line2D;
 import java.awt.geom.Path2D;
 import java.awt.image.BufferedImage;
@@ -30,6 +31,7 @@
 import javax.swing.AbstractAction;
 import javax.swing.Action;
 import javax.swing.Icon;
+import javax.swing.ImageIcon;
 import javax.swing.JComponent;
 import javax.swing.KeyStroke;
 import javax.swing.SwingUtilities;
@@ -58,6 +60,8 @@
 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.mappaint.Range;
+import org.openstreetmap.josm.gui.mappaint.mapcss.Selector;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryData;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryDataListener;
@@ -84,6 +88,7 @@
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.Geometry;
 import org.openstreetmap.josm.tools.I18n;
+import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
 import org.openstreetmap.josm.tools.Logging;
 
@@ -106,8 +111,22 @@ public final class MapillaryLayer extends AbstractModifiableLayer implements
   private static final int TRAFFIC_SIGN_SIZE = 6;
   /** A third of the height of the sign, for easier calculations */
   private static final double TRAFFIC_SIGN_HEIGHT_3RD = Math.sqrt(
-    Math.pow(TRAFFIC_SIGN_SIZE, 2) - Math.pow(TRAFFIC_SIGN_SIZE / 2d, 2)
-  ) / 3;
+    Math.pow(TRAFFIC_SIGN_SIZE, 2) - Math.pow(TRAFFIC_SIGN_SIZE / 2d, 2)) / 3;
+  /** The range to paint the full detection image at */
+  private static final Range IMAGE_CA_PAINT_RANGE = Selector.GeneralSelector.fromLevel(18, Integer.MAX_VALUE);
+  /** A shape to avoid many calculations */
+  private static final Ellipse2D.Double IMAGE_CIRCLE = new Ellipse2D.Double(0, 0, 2 * IMG_MARKER_RADIUS,
+    2 * IMG_MARKER_RADIUS);
+
+  /** The default sprite for a Mapillary image */
+  public static final ImageIcon DEFAULT_SPRITE = ImageProvider.get("mapillary_sprite_source/package_ui", "regular-0",
+    ImageProvider.ImageSizes.MAPMAX);
+  /** The sprite to use for the active Mapillary sequence */
+  public static final ImageIcon ACTIVE_SEQUENCE_SPRITE = ImageProvider.get("mapillary_sprite_source/package_ui",
+    "active-ca-0", ImageProvider.ImageSizes.MAPMAX);
+  /** The sprite to use for the currently selected image */
+  public static final ImageIcon SELECTED_IMAGE = ImageProvider.get("mapillary_sprite_source/package_ui", "cur-ca-0",
+    ImageProvider.ImageSizes.MAPMAX);
 
   private static class DataSetSourceListener implements DataSourceListener {
     @Override
@@ -195,11 +214,12 @@ public void setMode(AbstractMode mode) {
     if (this.mode != null && mv != null) {
       mv.removeMouseListener(this.mode);
       mv.removeMouseMotionListener(this.mode);
+      this.mode.exitMode();
       NavigatableComponent.removeZoomChangeListener(this.mode);
     }
     this.mode = mode;
     if (mode != null && mv != null) {
-      mv.setNewCursor(mode.cursor, this);
+      mode.enterMode();
       mv.addMouseListener(mode);
       mv.addMouseMotionListener(mode);
       NavigatableComponent.addZoomChangeListener(mode);
@@ -239,7 +259,7 @@ public static boolean hasInstance() {
    *
    * @return The {@link MapillaryData} object that stores the database.
    */
-  //@Override Depends upon #18801 for the override
+  // @Override Depends upon #18801 for the override
   public MapillaryData getData() {
     return this.data;
   }
@@ -258,6 +278,7 @@ public MapillaryLocationChangeset getLocationChangeset() {
    * 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, or null if no such image can be found
    */
@@ -304,7 +325,8 @@ public synchronized void destroy() {
         MainApplication.getLayerManager().getEditDataSet().removeDataSourceListener(DATASET_LISTENER);
       }
     } catch (IllegalArgumentException 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.
+      // 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.
     }
     UploadAction.unregisterUploadHook(this);
     super.destroy();
@@ -372,16 +394,13 @@ public synchronized void paint(final Graphics2D g, final MapView mv, final Bound
     for (MapillarySequence seq : getData().getSequences()) {
       if (seq.getImages().contains(selectedImage)) {
         g.setColor(
-          seq.getKey() == null ? MapillaryColorScheme.SEQ_IMPORTED_SELECTED : MapillaryColorScheme.SEQ_SELECTED
-          );
+          seq.getKey() == null ? MapillaryColorScheme.SEQ_IMPORTED_SELECTED : MapillaryColorScheme.SEQ_SELECTED);
       } else if (selectedImage == null) {
         g.setColor(
-          seq.getKey() == null ? MapillaryColorScheme.SEQ_IMPORTED_UNSELECTED : MapillaryColorScheme.SEQ_UNSELECTED
-          );
+          seq.getKey() == null ? MapillaryColorScheme.SEQ_IMPORTED_UNSELECTED : MapillaryColorScheme.SEQ_UNSELECTED);
       } else {
         g.setColor(
-          seq.getKey() == null ? MapillaryColorScheme.SEQ_IMPORTED_UNSELECTED : MapillaryColorScheme.SEQ_UNSELECTED
-          );
+          seq.getKey() == null ? MapillaryColorScheme.SEQ_IMPORTED_UNSELECTED : MapillaryColorScheme.SEQ_UNSELECTED);
         g.setComposite(fadeComposite);
       }
       g.draw(MapViewGeometryUtil.getSequencePath(mv, seq));
@@ -399,7 +418,8 @@ public synchronized void paint(final Graphics2D g, final MapView mv, final Bound
 
   /**
    * Draws an image marker onto the given Graphics context.
-   * @param g the 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 MapillaryAbstractImage img) {
@@ -419,7 +439,8 @@ private void drawImageMarker(final Graphics2D g, final MapillaryAbstractImage im
     if (selectedImg != null && getData().getMultiSelectedImages().contains(img)) {
       markerC = img.paintHighlightedColour();
       directionC = img.paintHighlightedAngleColour();
-    } else if (selectedImg != null && selectedImg.getSequence() != null && selectedImg.getSequence().equals(img.getSequence())) {
+    } else if (selectedImg != null && selectedImg.getSequence() != null
+      && selectedImg.getSequence().equals(img.getSequence())) {
       markerC = img.paintSelectedColour();
       directionC = img.paintSelectedAngleColour();
     } else {
@@ -428,35 +449,45 @@ private void drawImageMarker(final Graphics2D g, final MapillaryAbstractImage im
     }
 
     // Paint direction indicator
-    g.setColor(directionC);
-    if (img.isPanorama()) {
-      g.fillOval(p.x - CA_INDICATOR_RADIUS, p.y - CA_INDICATOR_RADIUS, 2 * CA_INDICATOR_RADIUS, 2 * CA_INDICATOR_RADIUS);
-    } else {
-      g.fillArc(p.x - CA_INDICATOR_RADIUS, p.y - CA_INDICATOR_RADIUS, 2 * CA_INDICATOR_RADIUS, 2 * CA_INDICATOR_RADIUS, (int) (90 - img.getMovingCa() - 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 (getData().getHighlightedImages().contains(img) || 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);
-    }
-
-    if (img instanceof MapillaryImage && !((MapillaryImage) img).getDetections().isEmpty()) {
-      Path2D trafficSign = new Path2D.Double();
-      trafficSign.moveTo(p.getX() - TRAFFIC_SIGN_SIZE / 2d, p.getY() - TRAFFIC_SIGN_HEIGHT_3RD);
-      trafficSign.lineTo(p.getX() + TRAFFIC_SIGN_SIZE / 2d, p.getY() - TRAFFIC_SIGN_HEIGHT_3RD);
-      trafficSign.lineTo(p.getX(), p.getY() + 2 * TRAFFIC_SIGN_HEIGHT_3RD);
-      trafficSign.closePath();
-      g.setColor(Color.WHITE);
-      g.fill(trafficSign);
-      g.setStroke(new BasicStroke(1));
-      g.setColor(Color.RED);
-      g.draw(trafficSign);
+    if (IMAGE_CA_PAINT_RANGE.contains(MainApplication.getMap().mapView.getDist100Pixel())) {
+      g.setColor(directionC);
+      if (img.isPanorama()) {
+        g.fillOval(p.x - CA_INDICATOR_RADIUS, p.y - CA_INDICATOR_RADIUS, 2 * CA_INDICATOR_RADIUS,
+          2 * CA_INDICATOR_RADIUS);
+      } else {
+        g.fillArc(p.x - CA_INDICATOR_RADIUS, p.y - CA_INDICATOR_RADIUS, 2 * CA_INDICATOR_RADIUS,
+          2 * CA_INDICATOR_RADIUS,
+          (int) (90 - img.getMovingCa() - CA_INDICATOR_ANGLE / 2d), CA_INDICATOR_ANGLE);
+      }
+      // Paint image marker
+      g.setColor(markerC);
+      g.setPaint(markerC);
+      synchronized (IMAGE_CIRCLE) {
+        IMAGE_CIRCLE.x = (double) p.x - IMG_MARKER_RADIUS;
+        IMAGE_CIRCLE.y = (double) p.y - IMG_MARKER_RADIUS;
+        g.fill(IMAGE_CIRCLE);
+      }
+
+      // Paint highlight for selected or highlighted images
+      if (getData().getHighlightedImages().contains(img) || 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);
+      }
+
+      if (img instanceof MapillaryImage && !((MapillaryImage) img).getDetections().isEmpty()) {
+        Path2D trafficSign = new Path2D.Double();
+        trafficSign.moveTo(p.getX() - TRAFFIC_SIGN_SIZE / 2d, p.getY() - TRAFFIC_SIGN_HEIGHT_3RD);
+        trafficSign.lineTo(p.getX() + TRAFFIC_SIGN_SIZE / 2d, p.getY() - TRAFFIC_SIGN_HEIGHT_3RD);
+        trafficSign.lineTo(p.getX(), p.getY() + 2 * TRAFFIC_SIGN_HEIGHT_3RD);
+        trafficSign.closePath();
+        g.setColor(Color.WHITE);
+        g.fill(trafficSign);
+        g.setStroke(new BasicStroke(1));
+        g.setColor(Color.RED);
+        g.draw(trafficSign);
+      }
     }
     g.setComposite(composite);
   }
@@ -485,8 +516,8 @@ private boolean isApplicable() {
                                                     // 0)
           List<OsmPrimitive> searchPrimitives = ds.searchPrimitives(bbox);
           if (primitives.parallelStream().filter(searchPrimitives::contains)
-              .mapToDouble(prim -> Geometry.getDistance(prim, new Node(image.getLatLon())))
-              .anyMatch(d -> d < maxDistance)) {
+            .mapToDouble(prim -> Geometry.getDistance(prim, new Node(image.getLatLon())))
+            .anyMatch(d -> d < maxDistance)) {
             isApplicable = true;
             break;
           }
@@ -514,7 +545,7 @@ public void mergeFrom(Layer from) {
 
   @Override
   public Action[] getMenuEntries() {
-    return new Action[]{
+    return new Action[] {
       LayerListDialog.getInstance().createShowHideLayerAction(),
       LayerListDialog.getInstance().createDeleteLayerAction(),
       new LayerListPopup.InfoAction(this)
@@ -523,7 +554,8 @@ public Action[] getMenuEntries() {
 
   @Override
   public Object getInfoComponent() {
-    IntSummaryStatistics seqSizeStats = getData().getSequences().stream().mapToInt(seq -> seq.getImages().size()).summaryStatistics();
+    IntSummaryStatistics seqSizeStats = getData().getSequences().stream().mapToInt(seq -> seq.getImages().size())
+      .summaryStatistics();
     final long numImported = getData().getImages().stream().filter(i -> i instanceof MapillaryImportedImage).count();
     final long numDownloaded = getData().getImages().stream().filter(i -> i instanceof MapillaryImage).count();
     final int numTotal = getData().getImages().size();
@@ -536,8 +568,7 @@ public Object getInfoComponent() {
         getData().getSequences().size(),
         seqSizeStats.getCount() <= 0 ? 0 : seqSizeStats.getMin(),
         seqSizeStats.getCount() <= 0 ? 0 : seqSizeStats.getMax(),
-        seqSizeStats.getAverage()
-      ))
+        seqSizeStats.getAverage()))
       .append("\n\n")
       .append(I18n.trn("{0} imported image", "{0} imported images", numImported, numImported))
       .append("\n+ ")
@@ -599,7 +630,8 @@ public void visitBoundingBox(BoundingXYVisitor v) {
     // Don't care about this
   }
 
-  /* (non-Javadoc)
+  /*
+   * (non-Javadoc)
    * @see org.openstreetmap.josm.plugins.mapillary.MapillaryDataListener#imagesAdded()
    */
   @Override
@@ -607,8 +639,11 @@ public void imagesAdded() {
     updateNearestImages();
   }
 
-  /* (non-Javadoc)
-   * @see org.openstreetmap.josm.plugins.mapillary.MapillaryDataListener#selectedImageChanged(org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage, org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage)
+  /*
+   * (non-Javadoc)
+   * @see
+   * org.openstreetmap.josm.plugins.mapillary.MapillaryDataListener#selectedImageChanged(org.openstreetmap.josm.plugins.
+   * mapillary.MapillaryAbstractImage, org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage)
    */
   @Override
   public void selectedImageChanged(MapillaryAbstractImage oldImage, MapillaryAbstractImage newImage) {
@@ -620,7 +655,7 @@ public void selectedImageChanged(MapillaryAbstractImage oldImage, MapillaryAbstr
    * 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
+   * @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 MapillaryImage[] getNearestImagesFromDifferentSequences(MapillaryAbstractImage target, int limit) {
@@ -633,10 +668,9 @@ private MapillaryImage[] getNearestImagesFromDifferentSequences(MapillaryAbstrac
         return resImg.orElse(null);
       })
       .filter(img -> // Filters out images too far away from target
-        img != null &&
-        img.getMovingLatLon().greatCircleDistance(target.getMovingLatLon())
-          < MapillaryProperties.SEQUENCE_MAX_JUMP_DISTANCE.get()
-       )
+      img != null &&
+        img.getMovingLatLon()
+          .greatCircleDistance(target.getMovingLatLon()) < MapillaryProperties.SEQUENCE_MAX_JUMP_DISTANCE.get())
       .sorted(new NearestImgToTargetComparator(target))
       .limit(limit)
       .toArray(MapillaryImage[]::new);
@@ -692,15 +726,16 @@ private static class NearestImgToTargetComparator implements Comparator<Mapillar
     public NearestImgToTargetComparator(MapillaryAbstractImage target) {
       this.target = target;
     }
-    /* (non-Javadoc)
+
+    /*
+     * (non-Javadoc)
      * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
      */
     @Override
     public int compare(MapillaryAbstractImage img1, MapillaryAbstractImage img2) {
       return (int) Math.signum(
         img1.getMovingLatLon().greatCircleDistance(target.getMovingLatLon()) -
-        img2.getMovingLatLon().greatCircleDistance(target.getMovingLatLon())
-      );
+          img2.getMovingLatLon().greatCircleDistance(target.getMovingLatLon()));
     }
   }
 
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/AbstractMode.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/AbstractMode.java
index e592d273e..0213da63a 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/AbstractMode.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/AbstractMode.java
@@ -4,9 +4,9 @@
 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.actions.mapmode.MapMode;
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.MapView;
@@ -14,6 +14,7 @@
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
 import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.io.download.MapillaryDownloader;
+import org.openstreetmap.josm.tools.Shortcut;
 
 /**
  * Superclass for all the mode of the {@link MapillaryLayer}.
@@ -21,16 +22,36 @@
  * @author nokutu
  * @see MapillaryLayer
  */
-public abstract class AbstractMode extends MouseAdapter implements
+public abstract class AbstractMode extends MapMode implements
   ZoomChangeListener {
 
-  private static final int DOWNLOAD_COOLDOWN = 2000;
-  private static SemiautomaticThread semiautomaticThread = new SemiautomaticThread();
+  /**
+   * Constructor for mapmodes with a menu (no shortcut will be registered)
+   *
+   * @param name     the action's text
+   * @param iconName icon filename in {@code mapmode} directory
+   * @param tooltip  a longer description of the action that will be displayed in the tooltip.
+   * @param cursor   cursor displayed when map mode is active
+   */
+  public AbstractMode(String name, String iconName, String tooltip, Cursor cursor) {
+    super(name, iconName, tooltip, cursor);
+  }
 
   /**
-   * Cursor that should become active when this mode is activated.
+   * Constructor for mapmodes without a menu
+   *
+   * @param name     the action's text
+   * @param iconName icon filename in {@code mapmode} directory
+   * @param tooltip  a longer description of the action that will be displayed in the tooltip.
+   * @param shortcut a ready-created shortcut object or null if you don't want a shortcut.
+   * @param cursor   cursor displayed when map mode is active
    */
-  public int cursor = Cursor.DEFAULT_CURSOR;
+  public AbstractMode(String name, String iconName, String tooltip, Shortcut shortcut, Cursor cursor) {
+    super(name, iconName, tooltip, shortcut, cursor);
+  }
+
+  private static final int DOWNLOAD_COOLDOWN = 2000;
+  private static SemiautomaticThread semiautomaticThread = new SemiautomaticThread();
 
   protected MapillaryAbstractImage getClosest(Point clickPoint) {
     double snapDistance = 10;
@@ -52,8 +73,8 @@ protected MapillaryAbstractImage getClosest(Point clickPoint) {
   /**
    * 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 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);
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/JoinMode.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/JoinMode.java
index 8fee86c1c..fe5a73687 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/JoinMode.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/JoinMode.java
@@ -4,7 +4,6 @@
 import static org.openstreetmap.josm.tools.I18n.tr;
 
 import java.awt.Color;
-import java.awt.Cursor;
 import java.awt.Graphics2D;
 import java.awt.Point;
 import java.awt.event.MouseEvent;
@@ -19,6 +18,7 @@
 import org.openstreetmap.josm.plugins.mapillary.history.MapillaryRecord;
 import org.openstreetmap.josm.plugins.mapillary.history.commands.CommandJoin;
 import org.openstreetmap.josm.plugins.mapillary.history.commands.CommandUnjoin;
+import org.openstreetmap.josm.tools.ImageProvider;
 
 /**
  * In this mode the user can join pictures to make sequences or unjoin them.
@@ -35,7 +35,8 @@ public class JoinMode extends AbstractMode {
    * Main constructor.
    */
   public JoinMode() {
-    this.cursor = Cursor.CROSSHAIR_CURSOR;
+    super(tr("Mapillary Join Mode"), "mapillary-join", tr("Join images into sequences in the Mapillary Layer"),
+      ImageProvider.getCursor("crosshair", null));
   }
 
   @Override
@@ -47,19 +48,14 @@ public void mousePressed(MouseEvent e) {
     if (this.lastClick == null && highlighted instanceof MapillaryImportedImage) {
       this.lastClick = (MapillaryImportedImage) highlighted;
     } else if (this.lastClick != null
-        && highlighted instanceof MapillaryImportedImage) {
-      if (
-        (
-          (highlighted.previous() == null && this.lastClick.next() == null) ||
-          (highlighted.next() == null && this.lastClick.previous() == null)
-        )
-        && !highlighted.getSequence().equals(this.lastClick.getSequence())
-      ) {
+      && highlighted instanceof MapillaryImportedImage) {
+      if (((highlighted.previous() == null && this.lastClick.next() == null) ||
+        (highlighted.next() == null && this.lastClick.previous() == null))
+        && !highlighted.getSequence().equals(this.lastClick.getSequence())) {
         MapillaryRecord.getInstance().addCommand(new CommandJoin(this.lastClick, highlighted));
       } else if (highlighted.equals(this.lastClick.next()) || highlighted.equals(this.lastClick.previous())) {
         MapillaryRecord.getInstance().addCommand(
-          new CommandUnjoin(Arrays.asList(this.lastClick, highlighted))
-        );
+          new CommandUnjoin(Arrays.asList(this.lastClick, highlighted)));
       }
       this.lastClick = null;
     }
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/SelectMode.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/SelectMode.java
index b110ac084..42b4ba597 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/SelectMode.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/mode/SelectMode.java
@@ -27,6 +27,8 @@
 import org.openstreetmap.josm.plugins.mapillary.history.commands.CommandMove;
 import org.openstreetmap.josm.plugins.mapillary.history.commands.CommandTurn;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.tools.ImageProvider;
 
 /**
  * Handles the input event related with the layer. Mainly clicks.
@@ -44,6 +46,8 @@ public class SelectMode extends AbstractMode {
    * Main constructor.
    */
   public SelectMode() {
+    super(tr("Mapillary Select Mode"), "mapillary-select", tr("Select images in the Mapillary Layer"),
+      ImageProvider.getCursor("normal", null));
     this.record = MapillaryRecord.getInstance();
   }
 
@@ -54,7 +58,9 @@ public void mousePressed(MouseEvent e) {
     }
     final MapillaryAbstractImage closest = getClosest(e.getPoint());
     if (closest == null) {
-      if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2 && MapillaryLayer.hasInstance()) {
+      if (e.getButton() == MouseEvent.BUTTON1
+        && e.getClickCount() == Config.getPref().getInt("mapillary.image.deselect.click.count", 3)
+        && MapillaryLayer.hasInstance()) { // Triple click
         MapillaryLayer.getInstance().getData().setSelectedImage(null);
       }
       return;
@@ -74,9 +80,7 @@ public void mousePressed(MouseEvent e) {
           MapillaryLayer.getInstance().getData().addMultiSelectedImage(
             new ConcurrentSkipListSet<>(closest.getSequence().getImages().subList(
               Math.min(i, j),
-              Math.max(i, j) + 1
-            ))
-          );
+              Math.max(i, j) + 1)));
         }
       } else { // click
         MapillaryLayer.getInstance().getData().setSelectedImage(closest);
@@ -93,20 +97,23 @@ public void mousePressed(MouseEvent e) {
   @Override
   public void mouseDragged(MouseEvent e) {
     MapillaryAbstractImage highlightImg = MapillaryLayer.getInstance().getData().getHighlightedImage();
-    if (
-            MainApplication.getLayerManager().getActiveLayer() == MapillaryLayer.getInstance()
-                && SwingUtilities.isLeftMouseButton(e)
-                && highlightImg != null && highlightImg.getLatLon() != null
-            ) {
+    if (MainApplication.getLayerManager().getActiveLayer() == MapillaryLayer.getInstance()
+      && SwingUtilities.isLeftMouseButton(e)
+      && highlightImg != null && highlightImg.getLatLon() != null) {
       Point highlightImgPoint = MainApplication.getMap().mapView.getPoint(highlightImg.getTempLatLon());
       if (e.isShiftDown()) { // turn
-        MapillaryLayer.getInstance().getData().getMultiSelectedImages().parallelStream().filter(img -> !(img instanceof MapillaryImage) || MapillaryProperties.DEVELOPER.get())
-                .forEach(img -> img.turn(Math.toDegrees(Math.atan2(e.getX() - highlightImgPoint.getX(), -e.getY() + highlightImgPoint.getY())) - highlightImg.getTempCa()));
+        MapillaryLayer.getInstance().getData().getMultiSelectedImages().parallelStream()
+          .filter(img -> !(img instanceof MapillaryImage) || MapillaryProperties.DEVELOPER.get())
+          .forEach(img -> img
+            .turn(Math.toDegrees(Math.atan2(e.getX() - highlightImgPoint.getX(), -e.getY() + highlightImgPoint.getY()))
+              - highlightImg.getTempCa()));
       } else { // move
         LatLon eventLatLon = MainApplication.getMap().mapView.getLatLon(e.getX(), e.getY());
-        LatLon imgLatLon = MainApplication.getMap().mapView.getLatLon(highlightImgPoint.getX(), highlightImgPoint.getY());
-        MapillaryLayer.getInstance().getData().getMultiSelectedImages().parallelStream().filter(img -> !(img instanceof MapillaryImage) || MapillaryProperties.DEVELOPER.get())
-                .forEach(img -> img.move(eventLatLon.getX() - imgLatLon.getX(), eventLatLon.getY() - imgLatLon.getY()));
+        LatLon imgLatLon = MainApplication.getMap().mapView.getLatLon(highlightImgPoint.getX(),
+          highlightImgPoint.getY());
+        MapillaryLayer.getInstance().getData().getMultiSelectedImages().parallelStream()
+          .filter(img -> !(img instanceof MapillaryImage) || MapillaryProperties.DEVELOPER.get())
+          .forEach(img -> img.move(eventLatLon.getX() - imgLatLon.getX(), eventLatLon.getY() - imgLatLon.getY()));
       }
       MapillaryLayer.invalidateInstance();
     }
@@ -125,7 +132,8 @@ public void mouseReleased(MouseEvent e) {
     } 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()));
+      record
+        .addCommand(new CommandMove(data.getMultiSelectedImages(), to.getX() - from.getX(), to.getY() - from.getY()));
     }
     data.getMultiSelectedImages().parallelStream().filter(Objects::nonNull).forEach(MapillaryAbstractImage::stopMoving);
     MapillaryLayer.invalidateInstance();
@@ -137,7 +145,7 @@ public void mouseReleased(MouseEvent e) {
   @Override
   public void mouseMoved(MouseEvent e) {
     if (MainApplication.getLayerManager().getActiveLayer() instanceof OsmDataLayer
-            && MainApplication.getMap().mapMode != MainApplication.getMap().mapModeSelect) {
+      && MainApplication.getMap().mapMode != MainApplication.getMap().mapModeSelect) {
       return;
     }
     if (!MapillaryProperties.HOVER_ENABLED.get()) {
diff --git a/test/unit/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayerTest.java b/test/unit/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayerTest.java
index 3715696bd..4cd31eea7 100644
--- a/test/unit/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayerTest.java
+++ b/test/unit/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayerTest.java
@@ -23,7 +23,6 @@
 import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImage;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImportedImage;
-import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.utils.TestUtil.MapillaryTestRules;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 

From 2958ee5d4a13496bd41d7953ebd7ea23b5d6f2a3 Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Tue, 28 Apr 2020 15:52:50 -0600
Subject: [PATCH 09/20] Use a sprite for the traffic sign detections hint

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 .../mapillary/gui/layer/MapillaryLayer.java   | 19 ++++++++-----------
 1 file changed, 8 insertions(+), 11 deletions(-)

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
index 01b568f7b..ce0d84a74 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
@@ -7,6 +7,7 @@
 import java.awt.Composite;
 import java.awt.Graphics2D;
 import java.awt.GraphicsEnvironment;
+import java.awt.Image;
 import java.awt.Point;
 import java.awt.Rectangle;
 import java.awt.RenderingHints;
@@ -14,7 +15,6 @@
 import java.awt.event.ActionEvent;
 import java.awt.geom.Ellipse2D;
 import java.awt.geom.Line2D;
-import java.awt.geom.Path2D;
 import java.awt.image.BufferedImage;
 import java.util.Collection;
 import java.util.Collections;
@@ -127,6 +127,11 @@ public final class MapillaryLayer extends AbstractModifiableLayer implements
   /** The sprite to use for the currently selected image */
   public static final ImageIcon SELECTED_IMAGE = ImageProvider.get("mapillary_sprite_source/package_ui", "cur-ca-0",
     ImageProvider.ImageSizes.MAPMAX);
+  /** The sprite to use to indicate that there are sign detections in the image */
+  private static final Image YIELD_SIGN = new ImageProvider("mapillary_sprite_source/package_signs",
+    "regulatory--yield--g1")
+      .setMaxSize(TRAFFIC_SIGN_SIZE).get()
+      .getImage();
 
   private static class DataSetSourceListener implements DataSourceListener {
     @Override
@@ -477,16 +482,8 @@ private void drawImageMarker(final Graphics2D g, final MapillaryAbstractImage im
       }
 
       if (img instanceof MapillaryImage && !((MapillaryImage) img).getDetections().isEmpty()) {
-        Path2D trafficSign = new Path2D.Double();
-        trafficSign.moveTo(p.getX() - TRAFFIC_SIGN_SIZE / 2d, p.getY() - TRAFFIC_SIGN_HEIGHT_3RD);
-        trafficSign.lineTo(p.getX() + TRAFFIC_SIGN_SIZE / 2d, p.getY() - TRAFFIC_SIGN_HEIGHT_3RD);
-        trafficSign.lineTo(p.getX(), p.getY() + 2 * TRAFFIC_SIGN_HEIGHT_3RD);
-        trafficSign.closePath();
-        g.setColor(Color.WHITE);
-        g.fill(trafficSign);
-        g.setStroke(new BasicStroke(1));
-        g.setColor(Color.RED);
-        g.draw(trafficSign);
+        g.drawImage(YIELD_SIGN, (int) (p.getX() - TRAFFIC_SIGN_SIZE / 2d), (int) (p.getY() - TRAFFIC_SIGN_SIZE / 2d),
+          null);
       }
     }
     g.setComposite(composite);

From d103dd61a01a1c025115d4e85dde528c23015d22 Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Tue, 28 Apr 2020 16:02:51 -0600
Subject: [PATCH 10/20] FIXUP: Keep painting selected sequence

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 .../mapillary/gui/layer/MapillaryLayer.java   | 66 ++++++++++---------
 1 file changed, 35 insertions(+), 31 deletions(-)

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
index ce0d84a74..c8152cd12 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
@@ -433,6 +433,12 @@ private void drawImageMarker(final Graphics2D g, final MapillaryAbstractImage im
       return;
     }
     final MapillaryAbstractImage selectedImg = getData().getSelectedImage();
+    if (!IMAGE_CA_PAINT_RANGE.contains(MainApplication.getMap().mapView.getDist100Pixel()) && !img.equals(selectedImg)
+      && !getData().getMultiSelectedImages().contains(img)
+      && (selectedImg == null || !img.getSequence().equals(selectedImg.getSequence()))) {
+      Logging.trace("An image was not painted due to a high zoom level, and not being the selected image/sequence");
+      return;
+    }
     final Point p = MainApplication.getMap().mapView.getPoint(img.getMovingLatLon());
     Composite composite = g.getComposite();
     if (selectedImg != null && !selectedImg.getSequence().equals(img.getSequence())) {
@@ -454,37 +460,35 @@ private void drawImageMarker(final Graphics2D g, final MapillaryAbstractImage im
     }
 
     // Paint direction indicator
-    if (IMAGE_CA_PAINT_RANGE.contains(MainApplication.getMap().mapView.getDist100Pixel())) {
-      g.setColor(directionC);
-      if (img.isPanorama()) {
-        g.fillOval(p.x - CA_INDICATOR_RADIUS, p.y - CA_INDICATOR_RADIUS, 2 * CA_INDICATOR_RADIUS,
-          2 * CA_INDICATOR_RADIUS);
-      } else {
-        g.fillArc(p.x - CA_INDICATOR_RADIUS, p.y - CA_INDICATOR_RADIUS, 2 * CA_INDICATOR_RADIUS,
-          2 * CA_INDICATOR_RADIUS,
-          (int) (90 - img.getMovingCa() - CA_INDICATOR_ANGLE / 2d), CA_INDICATOR_ANGLE);
-      }
-      // Paint image marker
-      g.setColor(markerC);
-      g.setPaint(markerC);
-      synchronized (IMAGE_CIRCLE) {
-        IMAGE_CIRCLE.x = (double) p.x - IMG_MARKER_RADIUS;
-        IMAGE_CIRCLE.y = (double) p.y - IMG_MARKER_RADIUS;
-        g.fill(IMAGE_CIRCLE);
-      }
-
-      // Paint highlight for selected or highlighted images
-      if (getData().getHighlightedImages().contains(img) || 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);
-      }
-
-      if (img instanceof MapillaryImage && !((MapillaryImage) img).getDetections().isEmpty()) {
-        g.drawImage(YIELD_SIGN, (int) (p.getX() - TRAFFIC_SIGN_SIZE / 2d), (int) (p.getY() - TRAFFIC_SIGN_SIZE / 2d),
-          null);
-      }
+    g.setColor(directionC);
+    if (img.isPanorama()) {
+      g.fillOval(p.x - CA_INDICATOR_RADIUS, p.y - CA_INDICATOR_RADIUS, 2 * CA_INDICATOR_RADIUS,
+        2 * CA_INDICATOR_RADIUS);
+    } else {
+      g.fillArc(p.x - CA_INDICATOR_RADIUS, p.y - CA_INDICATOR_RADIUS, 2 * CA_INDICATOR_RADIUS,
+        2 * CA_INDICATOR_RADIUS,
+        (int) (90 - img.getMovingCa() - CA_INDICATOR_ANGLE / 2d), CA_INDICATOR_ANGLE);
+    }
+    // Paint image marker
+    g.setColor(markerC);
+    g.setPaint(markerC);
+    synchronized (IMAGE_CIRCLE) {
+      IMAGE_CIRCLE.x = (double) p.x - IMG_MARKER_RADIUS;
+      IMAGE_CIRCLE.y = (double) p.y - IMG_MARKER_RADIUS;
+      g.fill(IMAGE_CIRCLE);
+    }
+
+    // Paint highlight for selected or highlighted images
+    if (getData().getHighlightedImages().contains(img) || 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);
+    }
+
+    if (img instanceof MapillaryImage && !((MapillaryImage) img).getDetections().isEmpty()) {
+      g.drawImage(YIELD_SIGN, (int) (p.getX() - TRAFFIC_SIGN_SIZE / 2d), (int) (p.getY() - TRAFFIC_SIGN_SIZE / 2d),
+        null);
     }
     g.setComposite(composite);
   }

From d25e6ff2bcac02072270ce4ef67d1f8d0b01afff Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Thu, 30 Apr 2020 16:46:40 -0600
Subject: [PATCH 11/20] Fix JOSM-19175

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 .../plugins/mapillary/MapillaryPlugin.java     |  7 ++++++-
 .../actions/MapillaryDownloadAction.java       | 18 +++++++++---------
 2 files changed, 15 insertions(+), 10 deletions(-)

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java
index f87b96682..3cdeecc13 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java
@@ -65,6 +65,8 @@ public class MapillaryPlugin extends Plugin implements Destroyable {
 
   private final List<Destroyable> destroyables = new ArrayList<>();
 
+  private MapillaryDownloadAction mapillaryDownloadAction;
+
   /**
    * Main constructor.
    *
@@ -84,7 +86,8 @@ public MapillaryPlugin(PluginInformation info) {
     MainMenu.add(menu.fileMenu, mapillaryExportAction, false, 14);
     destroyables.add(mapillaryExportAction);
 
-    MapillaryDownloadAction mapillaryDownloadAction = new MapillaryDownloadAction();
+    mapillaryDownloadAction = new MapillaryDownloadAction();
+    mapillaryDownloadAction.setEnabled(false);
     MainMenu.add(menu.imagerySubMenu, mapillaryDownloadAction, false);
     destroyables.add(mapillaryDownloadAction);
 
@@ -152,6 +155,7 @@ public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
       newFrame.addToggleDialog(MapillaryFilterDialog.getInstance(), false);
       newFrame.addToggleDialog(MapillaryExpertFilterDialog.getInstance(), true);
       toggleDialog.add(MapillaryExpertFilterDialog.getInstance());
+      mapillaryDownloadAction.setEnabled(true);
       // This fixes a UI issue -- for whatever reason, the tab pane is occasionally unusable when the expert filter
       // dialog is added.
       newFrame.conflictDialog.getToggleAction().actionPerformed(null);
@@ -159,6 +163,7 @@ public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
     } else if (oldFrame != null && newFrame == null) { // map frame removed
       toggleDialog.forEach(ToggleDialog::destroy);
       toggleDialog.clear();
+      mapillaryDownloadAction.setEnabled(false);
     }
   }
 
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryDownloadAction.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryDownloadAction.java
index 0fbcbeea1..faead9518 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryDownloadAction.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryDownloadAction.java
@@ -37,19 +37,19 @@ public class MapillaryDownloadAction extends JosmAction {
    */
   public MapillaryDownloadAction() {
     super(
-        tr("Mapillary"),
-        new ImageProvider(MapillaryPlugin.LOGO).setSize(ImageSizes.DEFAULT),
-        tr("Open Mapillary layer"),
-        SHORTCUT,
-        false,
-        "mapillaryDownload",
-        false
-    );
+      tr("Mapillary"),
+      new ImageProvider(MapillaryPlugin.LOGO).setSize(ImageSizes.DEFAULT),
+      tr("Open Mapillary layer"),
+      SHORTCUT,
+      false,
+      "mapillaryDownload",
+      false);
   }
 
   @Override
   public void actionPerformed(ActionEvent ae) {
-    if (!MapillaryLayer.hasInstance() || !MainApplication.getLayerManager().containsLayer(MapillaryLayer.getInstance())) {
+    if ((!MapillaryLayer.hasInstance()
+      || !MainApplication.getLayerManager().containsLayer(MapillaryLayer.getInstance()))) {
       LayerListModel model = LayerListDialog.getInstance().getModel();
       model.getLayerManager().addLayer(MapillaryLayer.getInstance());
       List<Layer> selected = model.getSelectedLayers();

From 9a5d3cb67208538e1d044dca0be92bb34e746ab2 Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Thu, 7 May 2020 08:06:10 -0600
Subject: [PATCH 12/20] FIXUP: EDT violation

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 .../mapillary/io/download/BoundsDownloadRunnable.java | 11 +++--------
 1 file changed, 3 insertions(+), 8 deletions(-)

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/BoundsDownloadRunnable.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/BoundsDownloadRunnable.java
index 1c1c5d16e..86cac40f4 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/BoundsDownloadRunnable.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/io/download/BoundsDownloadRunnable.java
@@ -13,10 +13,9 @@
 import java.util.concurrent.RecursiveAction;
 import java.util.function.Function;
 
-import javax.swing.SwingUtilities;
-
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.gui.Notification;
+import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
 import org.openstreetmap.josm.plugins.mapillary.oauth.MapillaryUser;
 import org.openstreetmap.josm.plugins.mapillary.oauth.OAuthUtils;
@@ -70,18 +69,14 @@ private void realRun(URL currentUrl) {
       String message = I18n.tr("Could not read from URL {0}!", currentUrl.toString());
       Logging.log(Logging.LEVEL_WARN, message, e);
       if (!GraphicsEnvironment.isHeadless()) {
-        if (SwingUtilities.isEventDispatchThread()) {
-          showNotification(message);
-        } else {
-          SwingUtilities.invokeLater(() -> showNotification(message));
-        }
+        GuiHelper.runInEDT(() -> showNotification(message));
       }
     }
   }
 
   private static void showNotification(String message) {
     new Notification(message).setIcon(MapillaryPlugin.LOGO.setSize(ImageSizes.LARGEICON).get())
-        .setDuration(Notification.TIME_LONG).show();
+      .setDuration(Notification.TIME_LONG).show();
   }
 
   /**

From 53bdf5831f4895c53bb0ac8a8e3eff0b6faf3f1c Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Wed, 6 May 2020 07:38:13 -0600
Subject: [PATCH 13/20] Attempt to avoid caching too many images into memory

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 .../josm/plugins/mapillary/MapillaryData.java | 27 +++++++++++++----
 .../josm/plugins/mapillary/cache/Caches.java  | 23 ++++++++++-----
 .../mapillary/cache/MapillaryCache.java       | 29 +++++++++++++++----
 3 files changed, 61 insertions(+), 18 deletions(-)

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryData.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryData.java
index 89de5c96a..ee32057f2 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryData.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryData.java
@@ -15,6 +15,7 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import javax.json.Json;
 import javax.json.JsonException;
@@ -31,6 +32,7 @@
 import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.plugins.mapillary.cache.CacheUtils;
 import org.openstreetmap.josm.plugins.mapillary.cache.Caches;
+import org.openstreetmap.josm.plugins.mapillary.cache.MapillaryCache;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryMainDialog;
 import org.openstreetmap.josm.plugins.mapillary.gui.imageinfo.ImageInfoPanel;
 import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
@@ -353,25 +355,35 @@ public void setSelectedImage(MapillaryAbstractImage image, boolean zoom) {
    */
   public static boolean inCurrentlySelectedDetection(MapillaryImage image) {
     return MainApplication.getLayerManager().getLayersOfType(PointObjectLayer.class).parallelStream()
-        .map(PointObjectLayer::getDataSet).flatMap(d -> d.getSelected().parallelStream())
-        .filter(p -> p.hasTag("detections"))
-        .flatMap(p -> PointObjectLayer.parseDetections(p.get("detections")).parallelStream())
-        .anyMatch(p -> image.getKey().equals(p.getOrDefault("image_key", null)));
+      .map(PointObjectLayer::getDataSet).flatMap(d -> d.getSelected().parallelStream())
+      .filter(p -> p.hasTag("detections"))
+      .flatMap(p -> PointObjectLayer.parseDetections(p.get("detections")).parallelStream())
+      .anyMatch(p -> image.getKey().equals(p.getOrDefault("image_key", null)));
   }
 
   /**
    * Downloads surrounding images of this mapillary image in background threads
+   *
    * @param mapillaryImage the image for which the surrounding images should be downloaded
    */
   private static void downloadSurroundingImages(MapillaryImage mapillaryImage) {
     MainApplication.worker.execute(() -> {
       final int prefetchCount = MapillaryProperties.PRE_FETCH_IMAGE_COUNT.get();
-      CacheAccess<String, BufferedImageCacheEntry> imageCache = Caches.ImageCache.getInstance().getCache();
+      CacheAccess<String, BufferedImageCacheEntry> imageCache = Caches.ImageCache.getInstance()
+        .getCache(MapillaryCache.Type.FULL_IMAGE);
 
       MapillaryAbstractImage nextImage = mapillaryImage.next();
       MapillaryAbstractImage prevImage = mapillaryImage.previous();
 
+      long freeMemory = Runtime.getRuntime().freeMemory();
+      // 3 bytes for RGB (jpg doesn't support the Alpha channel). I'm using 4 bytes instead of 3 for a buffer.
+      long estimatedImageSize = Stream.of(MapillaryCache.Type.values()).mapToLong(v -> v.getHeight() * v.getWidth() * 4)
+        .sum();
+
       for (int i = 0; i < prefetchCount; i++) {
+        if (freeMemory - estimatedImageSize < 0) {
+          break; // It doesn't make sense to try to cache images that won't be kept.
+        }
         if (nextImage != null) {
           if ((nextImage instanceof MapillaryImage) &&
             (imageCache.get(((MapillaryImage) nextImage).getKey()) == null)) {
@@ -386,6 +398,11 @@ private static void downloadSurroundingImages(MapillaryImage mapillaryImage) {
           }
           prevImage = prevImage.previous();
         }
+        imageCache.getCacheControl();
+        imageCache.get(mapillaryImage.getKey());
+        if (mapillaryImage.next() != null) {
+          imageCache.get(mapillaryImage.getKey());
+        }
       }
     });
   }
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/Caches.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/Caches.java
index f1ddb0910..70a33adf1 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/Caches.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/Caches.java
@@ -12,7 +12,9 @@
 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.mapillary.cache.MapillaryCache.Type;
 import org.openstreetmap.josm.plugins.mapillary.model.UserProfile;
+import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
 
 public final class Caches {
 
@@ -50,11 +52,18 @@ public void put(final K key, final V value) {
 
   public static class ImageCache {
     private static ImageCache instance;
-    private final CacheAccess<String, BufferedImageCacheEntry> cache =
-        JCSCacheManager.getCache("mapillary", 10, 10000, getCacheDirectory().getPath());
-
-    public CacheAccess<String, BufferedImageCacheEntry> getCache() {
-      return cache;
+    private final CacheAccess<String, BufferedImageCacheEntry> thumbnailCache = JCSCacheManager.getCache(
+      "mapillary:thumbnailImage",
+      Math.max(3 * MapillaryProperties.PRE_FETCH_IMAGE_COUNT.get(), 10), 10000, getCacheDirectory().getPath());
+    private final CacheAccess<String, BufferedImageCacheEntry> imageCache = JCSCacheManager
+      .getCache("mapillary:fullImage", Math.max(2 * MapillaryProperties.PRE_FETCH_IMAGE_COUNT.get() + 4, 10), 10_000,
+        getCacheDirectory().getPath());
+
+    public CacheAccess<String, BufferedImageCacheEntry> getCache(Type type) {
+      if (Type.THUMBNAIL.equals(type)) {
+        return thumbnailCache;
+      }
+      return imageCache;
     }
 
     public static ImageCache getInstance() {
@@ -99,8 +108,8 @@ public static CacheProxy<String, UserProfile> getInstance() {
 
     @Override
     protected CacheAccess<String, UserProfile> createNewCache() {
-      CacheAccess<String, UserProfile> cache =
-        JCSCacheManager.getCache("userProfile", 100, 1000, getCacheDirectory().getPath());
+      CacheAccess<String, UserProfile> cache = JCSCacheManager.getCache("userProfile", 100, 1000,
+        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);
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/MapillaryCache.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/MapillaryCache.java
index 79ae7193b..6208f721d 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/MapillaryCache.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/MapillaryCache.java
@@ -28,22 +28,39 @@ public class MapillaryCache extends JCSCachedTileLoaderJob<String, BufferedImage
    */
   public enum Type {
     /** Full quality image */
-    FULL_IMAGE,
+    FULL_IMAGE(2048),
     /** Low quality image */
-    THUMBNAIL
+    THUMBNAIL(320);
+
+    private int width;
+
+    private Type(int dimension) {
+      this.width = dimension;
+    }
+
+    /** Get the anticipated width for the image */
+    public int getWidth() {
+      return width;
+    }
+
+    /** Get the anticipated height for the image */
+    public int getHeight() {
+      return width;
+    }
   }
 
   /**
    * Main constructor.
    *
    * @param key
-   *          The key of the image.
+   *             The key of the image.
    * @param type
-   *          The type of image that must be downloaded (THUMBNAIL or
-   *          FULL_IMAGE).
+   *             The type of image that must be downloaded (THUMBNAIL or
+   *             FULL_IMAGE).
    */
   public MapillaryCache(final String key, final Type type) {
-    super(Caches.ImageCache.getInstance().getCache(), new TileJobOptions(50_000, 50_000, new HashMap<>(), TimeUnit.HOURS.toSeconds(4)));
+    super(Caches.ImageCache.getInstance().getCache(type),
+      new TileJobOptions(50_000, 50_000, new HashMap<>(), TimeUnit.HOURS.toSeconds(4)));
     if (key == null || type == null) {
       this.key = null;
       this.url = null;

From b43b6d7f66c51ee6a180938748fc997924cdf657 Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Wed, 6 May 2020 07:43:09 -0600
Subject: [PATCH 14/20] Modify datepicker to catch a (potential) exception from
 JavaFX (depends upon JOSM-19169)

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 .../josm/plugins/mapillary/gui/DatePickerFx.java     |  3 ++-
 .../josm/plugins/mapillary/gui/IDatePicker.java      | 12 ++++--------
 2 files changed, 6 insertions(+), 9 deletions(-)

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/DatePickerFx.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/DatePickerFx.java
index 0dbe635d1..f2c6e0ebf 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/DatePickerFx.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/DatePickerFx.java
@@ -3,6 +3,7 @@
 
 import java.awt.Dimension;
 import java.time.LocalDate;
+import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
 
 import javafx.event.ActionEvent;
@@ -34,7 +35,7 @@ public LocalDate fromString(String date) {
 
   }
 
-  public DatePickerFx() {
+  public DatePickerFx() throws ExecutionException {
     super(DatePicker.class);
     this.getNode().setConverter(new LocalDateConverterJavaFX());
     GuiHelper.runInEDT(() -> {
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/IDatePicker.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/IDatePicker.java
index 45df18318..45c0159d8 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/IDatePicker.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/IDatePicker.java
@@ -2,6 +2,7 @@
 package org.openstreetmap.josm.plugins.mapillary.gui;
 
 import java.time.LocalDate;
+import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
 
 import javax.swing.JComponent;
@@ -23,20 +24,15 @@ public interface IDatePicker<T extends JComponent> {
 
   void addEventHandler(Consumer<IDatePicker<?>> function);
 
-  public static IDatePicker<? extends JComponent> getNewDatePicker() {
-    boolean fx = false;
+  static IDatePicker<? extends JComponent> getNewDatePicker() {
     boolean useFx = MapillaryProperties.JAVA_FX.get();
     if (useFx) {
       try {
-        new DatePickerFx();
-        fx = true;
-      } catch (UnsupportedClassVersionError e) {
+        return new DatePickerFx();
+      } catch (NoClassDefFoundError | UnsupportedClassVersionError | ExecutionException e) {
         Logging.error(e);
       }
     }
-    if (fx) {
-      return new DatePickerFx();
-    }
     return new DatePickerSwing();
   }
 }

From 0cebfa4e27b401955dc9704c7c8caad119a9bb5c Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Thu, 7 May 2020 08:02:54 -0600
Subject: [PATCH 15/20] Use Mapillary-provided images for sprites

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 .../mapillary/MapillaryAbstractImage.java     | 41 ++++++++--
 .../plugins/mapillary/MapillaryImage.java     | 26 +++---
 .../mapillary/MapillaryImportedImage.java     | 28 +++----
 .../mapillary/gui/layer/MapillaryLayer.java   | 80 +++++++++----------
 .../resources/images/josm-ca/current-ca.svg   | 73 +++++++++++++++++
 .../images/josm-ca/default-ca-hover.svg       | 73 +++++++++++++++++
 .../resources/images/josm-ca/default-ca.svg   | 73 +++++++++++++++++
 .../images/josm-ca/private-ca-hover.svg       | 73 +++++++++++++++++
 .../resources/images/josm-ca/private-ca.svg   | 72 +++++++++++++++++
 .../images/josm-ca/sequence-ca-hover.svg      | 73 +++++++++++++++++
 .../resources/images/josm-ca/sequence-ca.svg  | 72 +++++++++++++++++
 .../images/josm-ca/sign-detection.svg         | 63 +++++++++++++++
 .../plugins/mapillary/oauth/UploadTest.java   |  4 +
 13 files changed, 672 insertions(+), 79 deletions(-)
 create mode 100644 src/main/resources/images/josm-ca/current-ca.svg
 create mode 100644 src/main/resources/images/josm-ca/default-ca-hover.svg
 create mode 100644 src/main/resources/images/josm-ca/default-ca.svg
 create mode 100644 src/main/resources/images/josm-ca/private-ca-hover.svg
 create mode 100644 src/main/resources/images/josm-ca/private-ca.svg
 create mode 100644 src/main/resources/images/josm-ca/sequence-ca-hover.svg
 create mode 100644 src/main/resources/images/josm-ca/sequence-ca.svg
 create mode 100644 src/main/resources/images/josm-ca/sign-detection.svg

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryAbstractImage.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryAbstractImage.java
index 8e2c0e071..919233134 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryAbstractImage.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryAbstractImage.java
@@ -2,15 +2,19 @@
 package org.openstreetmap.josm.plugins.mapillary;
 
 import java.awt.Color;
+import java.awt.Image;
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.Locale;
 
+import javax.swing.ImageIcon;
+
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.gpx.GpxImageEntry;
 import org.openstreetmap.josm.plugins.mapillary.utils.LocalDateConverter;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
+import org.openstreetmap.josm.tools.ImageProvider;
 
 /**
  * Abstract superclass for all image objects. At the moment there are just 2,
@@ -20,6 +24,18 @@
  *
  */
 public abstract class MapillaryAbstractImage extends GpxImageEntry {
+  /** The common directory for the Mapillary image sprites (for Mapillary Images) */
+  protected static final String IMAGE_SPRITE_DIR = "josm-ca";
+  /** The default sprite for a Mapillary image */
+  protected static final ImageIcon DEFAULT_SPRITE = new ImageProvider(IMAGE_SPRITE_DIR, "default-ca")
+    .setMaxWidth(ImageProvider.ImageSizes.MAP.getAdjustedHeight()).get();
+  /** The sprite to use for the active Mapillary sequence */
+  public static final ImageIcon ACTIVE_SEQUENCE_SPRITE = new ImageProvider(IMAGE_SPRITE_DIR, "sequence-ca")
+    .setMaxWidth(ImageProvider.ImageSizes.MAP.getAdjustedHeight()).get();
+  /** The sprite to use for the currently selected image */
+  public static final ImageIcon SELECTED_IMAGE = new ImageProvider(IMAGE_SPRITE_DIR, "current-ca")
+    .setMaxWidth(ImageProvider.ImageSizes.MAP.getAdjustedHeight()).get();
+
   /**
    * If two values for field ca differ by less than EPSILON both values are considered equal.
    */
@@ -314,15 +330,28 @@ public void turn(final double ca) {
     this.movingCa = this.tempCa + ca;
   }
 
-  public abstract Color paintHighlightedColour();
-
   public abstract Color paintHighlightedAngleColour();
 
-  public abstract Color paintSelectedColour();
-
   public abstract Color paintSelectedAngleColour();
 
-  public abstract Color paintUnselectedColour();
-
   public abstract Color paintUnselectedAngleColour();
+
+  /**
+   * @return The default image to represent this particular type of image
+   */
+  public abstract Image getDefaultImage();
+
+  /**
+   * @return The default image to indicate that this particular image is selected
+   */
+  public Image getSelectedImage() {
+    return SELECTED_IMAGE.getImage();
+  }
+
+  /**
+   * @return The image to indicate that the current sequence is active
+   */
+  public Image getActiveSequenceImage() {
+    return ACTIVE_SEQUENCE_SPRITE.getImage();
+  }
 }
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryImage.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryImage.java
index 858065c82..e5ad1e808 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryImage.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryImage.java
@@ -2,17 +2,21 @@
 package org.openstreetmap.josm.plugins.mapillary;
 
 import java.awt.Color;
+import java.awt.Image;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 
+import javax.swing.ImageIcon;
+
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.gpx.GpxImageEntry;
 import org.openstreetmap.josm.plugins.mapillary.gui.layer.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.model.ImageDetection;
 import org.openstreetmap.josm.plugins.mapillary.model.UserProfile;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryColorScheme;
+import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.Logging;
 
 /**
@@ -36,6 +40,10 @@ public class MapillaryImage extends MapillaryAbstractImage {
    */
   private final boolean privateImage;
 
+  /** The default sprite for a private Mapillary image */
+  public static final ImageIcon PRIVATE_SPRITE = new ImageProvider(IMAGE_SPRITE_DIR, "private-ca")
+    .setMaxWidth(ImageProvider.ImageSizes.MAP.getAdjustedHeight()).get();
+
   /**
    * Main constructor of the class MapillaryImage
    *
@@ -125,33 +133,23 @@ public void turn(double ca) {
     checkModified();
   }
 
-  @Override
-  public Color paintHighlightedColour() {
-    return privateImage ? MapillaryColorScheme.SEQ_PRIVATE_HIGHLIGHTED : MapillaryColorScheme.SEQ_HIGHLIGHTED;
-  }
-
   @Override
   public Color paintHighlightedAngleColour() {
     return privateImage ? MapillaryColorScheme.SEQ_PRIVATE_HIGHLIGHTED_CA : MapillaryColorScheme.SEQ_HIGHLIGHTED_CA;
   }
 
-  @Override
-  public Color paintSelectedColour() {
-    return privateImage ? MapillaryColorScheme.SEQ_PRIVATE_SELECTED : MapillaryColorScheme.SEQ_SELECTED;
-  }
-
   @Override
   public Color paintSelectedAngleColour() {
     return privateImage ? MapillaryColorScheme.SEQ_PRIVATE_SELECTED_CA : MapillaryColorScheme.SEQ_SELECTED_CA;
   }
 
   @Override
-  public Color paintUnselectedColour() {
-    return privateImage ? MapillaryColorScheme.SEQ_PRIVATE_UNSELECTED : MapillaryColorScheme.SEQ_UNSELECTED;
+  public Color paintUnselectedAngleColour() {
+    return privateImage ? MapillaryColorScheme.SEQ_PRIVATE_UNSELECTED_CA : MapillaryColorScheme.SEQ_UNSELECTED_CA;
   }
 
   @Override
-  public Color paintUnselectedAngleColour() {
-    return privateImage ? MapillaryColorScheme.SEQ_PRIVATE_UNSELECTED_CA : MapillaryColorScheme.SEQ_UNSELECTED_CA;
+  public Image getDefaultImage() {
+    return privateImage ? PRIVATE_SPRITE.getImage() : DEFAULT_SPRITE.getImage();
   }
 }
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryImportedImage.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryImportedImage.java
index cc165eb70..7b0753df2 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryImportedImage.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryImportedImage.java
@@ -2,6 +2,7 @@
 package org.openstreetmap.josm.plugins.mapillary;
 
 import java.awt.Color;
+import java.awt.Image;
 import java.awt.image.BufferedImage;
 import java.io.File;
 import java.io.FileInputStream;
@@ -52,12 +53,14 @@ public MapillaryImportedImage(final LatLon latLon, final double ca, final File f
    * @param datetimeOriginal The date the picture was taken.
    * @param pano             The property to indicate whether image is panorama or not.
    */
-  public MapillaryImportedImage(final LatLon latLon, final double ca, final File file, final boolean pano, final String datetimeOriginal) {
+  public MapillaryImportedImage(final LatLon latLon, final double ca, final File file, final boolean pano,
+    final String datetimeOriginal) {
     this(latLon, ca, file, pano, parseTimestampElseCurrentTime(datetimeOriginal));
   }
 
   /**
    * Constructs a new image from an image entry of a {@link GeoImageLayer}.
+   *
    * @param geoImage the {@link ImageEntry}, from which the corresponding fields are taken
    * @return new image
    */
@@ -81,7 +84,7 @@ public static MapillaryImportedImage createInstance(final ImageEntry geoImage) {
     boolean pano = false;
     try (FileInputStream fis = new FileInputStream(geoImage.getFile())) {
       pano = ImageMetaDataUtil.isPanorama(fis);
-    } catch(IOException ex) {
+    } catch (IOException ex) {
       Logging.trace(ex);
     }
     return new MapillaryImportedImage(coord, ca, geoImage.getFile(), pano, time);
@@ -99,7 +102,8 @@ private static long parseTimestampElseCurrentTime(final String timestamp) {
     }
   }
 
-  public MapillaryImportedImage(final LatLon latLon, final double ca, final File file, final boolean pano, final long capturedAt) {
+  public MapillaryImportedImage(final LatLon latLon, final double ca, final File file, final boolean pano,
+    final long capturedAt) {
     super(latLon, ca, pano);
     super.setFile(file);
     this.capturedAt = capturedAt;
@@ -156,34 +160,24 @@ public boolean equals(Object obj) {
     return true;
   }
 
-  @Override
-  public Color paintHighlightedColour() {
-    return MapillaryColorScheme.SEQ_IMPORTED_HIGHLIGHTED;
-  }
-
   @Override
   public Color paintHighlightedAngleColour() {
     return MapillaryColorScheme.SEQ_IMPORTED_HIGHLIGHTED_CA;
   }
 
-  @Override
-  public Color paintSelectedColour() {
-    return MapillaryColorScheme.SEQ_IMPORTED_SELECTED;
-  }
-
   @Override
   public Color paintSelectedAngleColour() {
     return MapillaryColorScheme.SEQ_IMPORTED_SELECTED_CA;
   }
 
   @Override
-  public Color paintUnselectedColour() {
-    return MapillaryColorScheme.SEQ_IMPORTED_UNSELECTED;
+  public Color paintUnselectedAngleColour() {
+    return MapillaryColorScheme.SEQ_IMPORTED_UNSELECTED_CA;
   }
 
   @Override
-  public Color paintUnselectedAngleColour() {
-    return MapillaryColorScheme.SEQ_IMPORTED_UNSELECTED_CA;
+  public Image getDefaultImage() {
+    return DEFAULT_SPRITE.getImage();
   }
 
 }
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
index c8152cd12..5dda447a0 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
@@ -13,8 +13,9 @@
 import java.awt.RenderingHints;
 import java.awt.TexturePaint;
 import java.awt.event.ActionEvent;
-import java.awt.geom.Ellipse2D;
+import java.awt.geom.AffineTransform;
 import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
 import java.awt.image.BufferedImage;
 import java.util.Collection;
 import java.util.Collections;
@@ -31,7 +32,6 @@
 import javax.swing.AbstractAction;
 import javax.swing.Action;
 import javax.swing.Icon;
-import javax.swing.ImageIcon;
 import javax.swing.JComponent;
 import javax.swing.KeyStroke;
 import javax.swing.SwingUtilities;
@@ -105,33 +105,15 @@ public final class MapillaryLayer extends AbstractModifiableLayer implements
   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;
   /** Length of the edge of the small sign, which indicates that traffic signs have been found in an image. */
-  private static final int TRAFFIC_SIGN_SIZE = 6;
-  /** A third of the height of the sign, for easier calculations */
-  private static final double TRAFFIC_SIGN_HEIGHT_3RD = Math.sqrt(
-    Math.pow(TRAFFIC_SIGN_SIZE, 2) - Math.pow(TRAFFIC_SIGN_SIZE / 2d, 2)) / 3;
+  private static final int TRAFFIC_SIGN_SIZE = ImageProvider.ImageSizes.MAP.getAdjustedWidth();
   /** The range to paint the full detection image at */
   private static final Range IMAGE_CA_PAINT_RANGE = Selector.GeneralSelector.fromLevel(18, Integer.MAX_VALUE);
-  /** A shape to avoid many calculations */
-  private static final Ellipse2D.Double IMAGE_CIRCLE = new Ellipse2D.Double(0, 0, 2 * IMG_MARKER_RADIUS,
-    2 * IMG_MARKER_RADIUS);
-
-  /** The default sprite for a Mapillary image */
-  public static final ImageIcon DEFAULT_SPRITE = ImageProvider.get("mapillary_sprite_source/package_ui", "regular-0",
-    ImageProvider.ImageSizes.MAPMAX);
-  /** The sprite to use for the active Mapillary sequence */
-  public static final ImageIcon ACTIVE_SEQUENCE_SPRITE = ImageProvider.get("mapillary_sprite_source/package_ui",
-    "active-ca-0", ImageProvider.ImageSizes.MAPMAX);
-  /** The sprite to use for the currently selected image */
-  public static final ImageIcon SELECTED_IMAGE = ImageProvider.get("mapillary_sprite_source/package_ui", "cur-ca-0",
-    ImageProvider.ImageSizes.MAPMAX);
+
   /** The sprite to use to indicate that there are sign detections in the image */
-  private static final Image YIELD_SIGN = new ImageProvider("mapillary_sprite_source/package_signs",
-    "regulatory--yield--g1")
-      .setMaxSize(TRAFFIC_SIGN_SIZE).get()
-      .getImage();
+  private static final Image YIELD_SIGN = new ImageProvider("josm-ca", "sign-detection")
+    .setMaxSize(TRAFFIC_SIGN_SIZE).get()
+    .getImage();
 
   private static class DataSetSourceListener implements DataSourceListener {
     @Override
@@ -159,6 +141,7 @@ public void dataSourceChange(DataSourceChangeEvent event) {
   private final MapillaryLocationChangeset locationChangeset = new MapillaryLocationChangeset();
   private static AlphaComposite fadeComposite = AlphaComposite
     .getInstance(AlphaComposite.SRC_OVER, MapillaryProperties.UNSELECTED_OPACITY.get().floatValue());
+  private static Point2D standardImageCentroid = null;
 
   private MapillaryLayer() {
     super(I18n.tr("Mapillary Images"));
@@ -445,39 +428,41 @@ private void drawImageMarker(final Graphics2D g, final MapillaryAbstractImage im
       g.setComposite(fadeComposite);
     }
     // Determine colors
-    final Color markerC;
     final Color directionC;
+    final Image i;
     if (selectedImg != null && getData().getMultiSelectedImages().contains(img)) {
-      markerC = img.paintHighlightedColour();
+      i = img.getSelectedImage();
       directionC = img.paintHighlightedAngleColour();
     } else if (selectedImg != null && selectedImg.getSequence() != null
       && selectedImg.getSequence().equals(img.getSequence())) {
-      markerC = img.paintSelectedColour();
       directionC = img.paintSelectedAngleColour();
+      i = img.getActiveSequenceImage();
     } else {
-      markerC = img.paintUnselectedColour();
+      i = img.getDefaultImage();
       directionC = img.paintUnselectedAngleColour();
     }
-
     // Paint direction indicator
     g.setColor(directionC);
     if (img.isPanorama()) {
+      Composite currentComposit = g.getComposite();
+      g.setComposite(fadeComposite);
       g.fillOval(p.x - CA_INDICATOR_RADIUS, p.y - CA_INDICATOR_RADIUS, 2 * CA_INDICATOR_RADIUS,
         2 * CA_INDICATOR_RADIUS);
-    } else {
-      g.fillArc(p.x - CA_INDICATOR_RADIUS, p.y - CA_INDICATOR_RADIUS, 2 * CA_INDICATOR_RADIUS,
-        2 * CA_INDICATOR_RADIUS,
-        (int) (90 - img.getMovingCa() - CA_INDICATOR_ANGLE / 2d), CA_INDICATOR_ANGLE);
-    }
-    // Paint image marker
-    g.setColor(markerC);
-    g.setPaint(markerC);
-    synchronized (IMAGE_CIRCLE) {
-      IMAGE_CIRCLE.x = (double) p.x - IMG_MARKER_RADIUS;
-      IMAGE_CIRCLE.y = (double) p.y - IMG_MARKER_RADIUS;
-      g.fill(IMAGE_CIRCLE);
+      g.setComposite(currentComposit);
     }
 
+    double angle = Math.toRadians(img.getMovingCa());
+    AffineTransform backup = g.getTransform();
+    Point2D originalCentroid = getOriginalCentroid(i);
+    AffineTransform move = AffineTransform.getRotateInstance(angle, p.getX(), p.getY());
+    move.translate(-originalCentroid.getX(), -originalCentroid.getY());
+    Point2D.Double d2 = new Point2D.Double(p.x + originalCentroid.getX(), p.y + originalCentroid.getY());
+    move.transform(d2, d2);
+    g.setTransform(move);
+
+    g.drawImage(i, p.x, p.y, null);
+    g.setTransform(backup);
+
     // Paint highlight for selected or highlighted images
     if (getData().getHighlightedImages().contains(img) || img.equals(getData().getHighlightedImage())
       || getData().getMultiSelectedImages().contains(img)) {
@@ -493,6 +478,17 @@ private void drawImageMarker(final Graphics2D g, final MapillaryAbstractImage im
     g.setComposite(composite);
   }
 
+  private static Point2D getOriginalCentroid(Image i) {
+    if (standardImageCentroid == null) {
+      int width = i.getWidth(null);
+      int height = i.getHeight(null);
+      double originalCentroidX = width / 2d;
+      double originalCentroidY = 2 * height / 3d;
+      standardImageCentroid = new Point2D.Double(originalCentroidX, originalCentroidY);
+    }
+    return standardImageCentroid;
+  }
+
   @Override
   public Icon getIcon() {
     return MapillaryPlugin.LOGO.setSize(ImageSizes.LAYER).get();
diff --git a/src/main/resources/images/josm-ca/current-ca.svg b/src/main/resources/images/josm-ca/current-ca.svg
new file mode 100644
index 000000000..75923fbb6
--- /dev/null
+++ b/src/main/resources/images/josm-ca/current-ca.svg
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="4.2333288mm"
+   height="6.0854163mm"
+   viewBox="0 0 4.2333288 6.0854163"
+   version="1.1"
+   id="svg2725"
+   inkscape:version="1.0beta1 (32d4812, 2019-09-19)"
+   sodipodi:docname="current-ca.svg">
+  <defs
+     id="defs2719" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="9.0390037"
+     inkscape:cx="1.8530716"
+     inkscape:cy="12.440369"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     inkscape:document-rotation="0"
+     showgrid="false"
+     inkscape:window-width="1600"
+     inkscape:window-height="907"
+     inkscape:window-x="204"
+     inkscape:window-y="123"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata2722">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-93.124201,-86.88786)">
+    <path
+       style="stroke-width:0.264583"
+       inkscape:connector-curvature="0"
+       fill-rule="evenodd"
+       clip-rule="evenodd"
+       d="m 93.766609,87.170726 1.474259,3.685884 1.474258,-3.685884 c -0.455612,-0.182483 -0.953294,-0.282866 -1.474258,-0.282866 -0.520965,0 -1.018646,0.100383 -1.474259,0.282866 z"
+       fill="#F5B81A"
+       id="path859" />
+    <path
+       inkscape:connector-curvature="0"
+       fill-rule="evenodd"
+       clip-rule="evenodd"
+       d="m 95.240868,92.708693 c 1.022879,0 1.852078,-0.829204 1.852078,-1.852083 0,-1.022879 -0.829199,-1.852083 -1.852078,-1.852083 -1.022879,0 -1.852084,0.829204 -1.852084,1.852083 0,1.022879 0.829205,1.852083 1.852084,1.852083 z"
+       fill="#F5811A"
+       stroke="white"
+       stroke-width="1"
+       id="path861" />
+  </g>
+</svg>
diff --git a/src/main/resources/images/josm-ca/default-ca-hover.svg b/src/main/resources/images/josm-ca/default-ca-hover.svg
new file mode 100644
index 000000000..624ec32b4
--- /dev/null
+++ b/src/main/resources/images/josm-ca/default-ca-hover.svg
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   sodipodi:docname="default-ca-hover.svg"
+   inkscape:version="1.0beta1 (32d4812, 2019-09-19)"
+   id="svg2081"
+   version="1.1"
+   viewBox="0 0 4.2333336 6.0854235"
+   height="6.0854235mm"
+   width="4.2333336mm">
+  <defs
+     id="defs2075" />
+  <sodipodi:namedview
+     inkscape:window-maximized="0"
+     inkscape:window-y="61"
+     inkscape:window-x="2100"
+     inkscape:window-height="907"
+     inkscape:window-width="1600"
+     showgrid="false"
+     inkscape:document-rotation="0"
+     inkscape:current-layer="layer1"
+     inkscape:document-units="mm"
+     inkscape:cy="11.500013"
+     inkscape:cx="8.0000005"
+     inkscape:zoom="30.39127"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     borderopacity="1.0"
+     bordercolor="#666666"
+     pagecolor="#ffffff"
+     id="base" />
+  <metadata
+     id="metadata2078">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     transform="translate(-84.817857,-165.53467)"
+     id="layer1"
+     inkscape:groupmode="layer"
+     inkscape:label="Layer 1">
+    <path
+       style="stroke-width:0.264583"
+       inkscape:connector-curvature="0"
+       fill-rule="evenodd"
+       clip-rule="evenodd"
+       d="m 85.460186,165.81754 1.474364,3.68588 1.474338,-3.68588 c -0.455798,-0.18248 -0.953347,-0.28287 -1.474338,-0.28287 -0.520991,0 -1.01854,0.10039 -1.474364,0.28287 z"
+       fill="#187A45"
+       id="path875" />
+    <path
+       inkscape:connector-curvature="0"
+       fill-rule="evenodd"
+       clip-rule="evenodd"
+       d="m 86.934524,171.35551 c 1.022879,0 1.852083,-0.82921 1.852083,-1.85209 0,-1.02288 -0.829204,-1.85208 -1.852083,-1.85208 -1.022879,0 -1.852084,0.8292 -1.852084,1.85208 0,1.02288 0.829205,1.85209 1.852084,1.85209 z"
+       fill="#05CB63"
+       stroke="white"
+       stroke-width="2"
+       id="path877" />
+  </g>
+</svg>
diff --git a/src/main/resources/images/josm-ca/default-ca.svg b/src/main/resources/images/josm-ca/default-ca.svg
new file mode 100644
index 000000000..c04a86db3
--- /dev/null
+++ b/src/main/resources/images/josm-ca/default-ca.svg
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="3.7041659mm"
+   height="5.8208399mm"
+   viewBox="0 0 3.7041659 5.8208399"
+   version="1.1"
+   id="svg1476"
+   inkscape:version="1.0beta1 (32d4812, 2019-09-19)"
+   sodipodi:docname="default-ca.svg">
+  <defs
+     id="defs1470" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="31.772691"
+     inkscape:cx="6.8408291"
+     inkscape:cy="11.818325"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     inkscape:document-rotation="0"
+     showgrid="false"
+     inkscape:window-width="1600"
+     inkscape:window-height="907"
+     inkscape:window-x="0"
+     inkscape:window-y="23"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata1473">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-93.397917,-101.41101)">
+    <path
+       id="path871"
+       fill="#187A45"
+       d="m 93.775662,101.69389 1.474364,3.68587 1.474338,-3.68587 c -0.455798,-0.18249 -0.953347,-0.28288 -1.474338,-0.28288 -0.520991,0 -1.01854,0.10039 -1.474364,0.28288 z"
+       clip-rule="evenodd"
+       fill-rule="evenodd"
+       inkscape:connector-curvature="0"
+       style="stroke-width:0.264583" />
+    <circle
+       clip-rule="evenodd"
+       fill-rule="evenodd"
+       id="circle873"
+       fill="#05CB63"
+       style="stroke-width:0.264583"
+       r="1.8669461"
+       cy="105.37702"
+       cx="95.253685" />
+  </g>
+</svg>
diff --git a/src/main/resources/images/josm-ca/private-ca-hover.svg b/src/main/resources/images/josm-ca/private-ca-hover.svg
new file mode 100644
index 000000000..418484843
--- /dev/null
+++ b/src/main/resources/images/josm-ca/private-ca-hover.svg
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   sodipodi:docname="private-ca-hover.svg"
+   inkscape:version="1.0beta1 (32d4812, 2019-09-19)"
+   id="svg1492"
+   version="1.1"
+   viewBox="0 0 4.2333336 6.0854135"
+   height="6.0854135mm"
+   width="4.2333336mm">
+  <defs
+     id="defs1486" />
+  <sodipodi:namedview
+     inkscape:window-maximized="0"
+     inkscape:window-y="100"
+     inkscape:window-x="297"
+     inkscape:window-height="907"
+     inkscape:window-width="1600"
+     showgrid="false"
+     inkscape:document-rotation="0"
+     inkscape:current-layer="layer1"
+     inkscape:document-units="mm"
+     inkscape:cy="11.499994"
+     inkscape:cx="8.0000006"
+     inkscape:zoom="30.391321"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     borderopacity="1.0"
+     bordercolor="#666666"
+     pagecolor="#ffffff"
+     id="base" />
+  <metadata
+     id="metadata1489">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     transform="translate(-87.841667,-120.17753)"
+     id="layer1"
+     inkscape:groupmode="layer"
+     inkscape:label="Layer 1">
+    <path
+       style="stroke-width:0.264583"
+       inkscape:connector-curvature="0"
+       fill-rule="evenodd"
+       clip-rule="evenodd"
+       d="m 88.483996,120.4604 1.474364,3.68588 1.474337,-3.68588 c -0.455797,-0.18249 -0.953346,-0.28287 -1.474337,-0.28287 -0.520991,0 -1.01854,0.10038 -1.474364,0.28287 z"
+       fill="#6236FF"
+       id="path855" />
+    <path
+       inkscape:connector-curvature="0"
+       fill-rule="evenodd"
+       clip-rule="evenodd"
+       d="m 89.958333,125.99836 c 1.022879,0 1.852084,-0.8292 1.852084,-1.85208 0,-1.02288 -0.829205,-1.85208 -1.852084,-1.85208 -1.022879,0 -1.852083,0.8292 -1.852083,1.85208 0,1.02288 0.829204,1.85208 1.852083,1.85208 z"
+       fill="#8F89F5"
+       stroke="white"
+       stroke-width="2"
+       id="path857" />
+  </g>
+</svg>
diff --git a/src/main/resources/images/josm-ca/private-ca.svg b/src/main/resources/images/josm-ca/private-ca.svg
new file mode 100644
index 000000000..56f87410d
--- /dev/null
+++ b/src/main/resources/images/josm-ca/private-ca.svg
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   sodipodi:docname="private-ca.svg"
+   inkscape:version="1.0beta1 (32d4812, 2019-09-19)"
+   id="svg887"
+   version="1.1"
+   viewBox="0 0 3.70417 5.8208332"
+   height="5.8208332mm"
+   width="3.70417mm">
+  <defs
+     id="defs881" />
+  <sodipodi:namedview
+     inkscape:window-maximized="0"
+     inkscape:window-y="23"
+     inkscape:window-x="0"
+     inkscape:window-height="907"
+     inkscape:window-width="1600"
+     showgrid="false"
+     inkscape:document-rotation="0"
+     inkscape:current-layer="layer1"
+     inkscape:document-units="mm"
+     inkscape:cy="10.999999"
+     inkscape:cx="7.0000063"
+     inkscape:zoom="31.772729"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     borderopacity="1.0"
+     bordercolor="#666666"
+     pagecolor="#ffffff"
+     id="base" />
+  <metadata
+     id="metadata884">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     transform="translate(-119.85625,-65.88125)"
+     id="layer1"
+     inkscape:groupmode="layer"
+     inkscape:label="Layer 1">
+    <path
+       style="stroke-width:0.264583"
+       inkscape:connector-curvature="0"
+       fill-rule="evenodd"
+       clip-rule="evenodd"
+       d="m 120.234,66.164129 1.47436,3.685871 1.47434,-3.685871 c -0.4558,-0.182491 -0.95335,-0.282879 -1.47434,-0.282879 -0.52099,0 -1.01854,0.100388 -1.47436,0.282879 z"
+       fill="#6236FF"
+       id="path851" />
+    <path
+       style="stroke-width:0.264583"
+       inkscape:connector-curvature="0"
+       fill-rule="evenodd"
+       clip-rule="evenodd"
+       d="m 121.70833,71.702083 c 1.02288,0 1.85209,-0.829204 1.85209,-1.852083 0,-1.022879 -0.82921,-1.852083 -1.85209,-1.852083 -1.02288,0 -1.85208,0.829204 -1.85208,1.852083 0,1.022879 0.8292,1.852083 1.85208,1.852083 z"
+       fill="#8F89F5"
+       id="path853" />
+  </g>
+</svg>
diff --git a/src/main/resources/images/josm-ca/sequence-ca-hover.svg b/src/main/resources/images/josm-ca/sequence-ca-hover.svg
new file mode 100644
index 000000000..4fc032b4f
--- /dev/null
+++ b/src/main/resources/images/josm-ca/sequence-ca-hover.svg
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   sodipodi:docname="sequence-ca-hover.svg"
+   inkscape:version="1.0beta1 (32d4812, 2019-09-19)"
+   id="svg3316"
+   version="1.1"
+   viewBox="0 0 4.2333326 6.0854235"
+   height="6.0854235mm"
+   width="4.2333326mm">
+  <defs
+     id="defs3310" />
+  <sodipodi:namedview
+     inkscape:window-maximized="0"
+     inkscape:window-y="103"
+     inkscape:window-x="2057"
+     inkscape:window-height="907"
+     inkscape:window-width="1690"
+     showgrid="false"
+     inkscape:document-rotation="0"
+     inkscape:current-layer="layer1"
+     inkscape:document-units="mm"
+     inkscape:cy="11.500013"
+     inkscape:cx="7.9999987"
+     inkscape:zoom="30.39127"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     borderopacity="1.0"
+     bordercolor="#666666"
+     pagecolor="#ffffff"
+     id="base" />
+  <metadata
+     id="metadata3313">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     transform="translate(-95.401191,-149.65967)"
+     id="layer1"
+     inkscape:groupmode="layer"
+     inkscape:label="Layer 1">
+    <path
+       style="stroke-width:0.264583"
+       inkscape:connector-curvature="0"
+       fill-rule="evenodd"
+       clip-rule="evenodd"
+       d="m 96.043519,149.94254 1.474365,3.68588 1.474337,-3.68588 c -0.455797,-0.18248 -0.953346,-0.28287 -1.474337,-0.28287 -0.520991,0 -1.01854,0.10039 -1.474365,0.28287 z"
+       fill="#00769D"
+       id="path885" />
+    <path
+       inkscape:connector-curvature="0"
+       fill-rule="evenodd"
+       clip-rule="evenodd"
+       d="m 97.517857,155.48051 c 1.022879,0 1.852083,-0.82921 1.852083,-1.85209 0,-1.02288 -0.829204,-1.85208 -1.852083,-1.85208 -1.022879,0 -1.852083,0.8292 -1.852083,1.85208 0,1.02288 0.829204,1.85209 1.852083,1.85209 z"
+       fill="#00B5F5"
+       stroke="white"
+       stroke-width="2"
+       id="path887" />
+  </g>
+</svg>
diff --git a/src/main/resources/images/josm-ca/sequence-ca.svg b/src/main/resources/images/josm-ca/sequence-ca.svg
new file mode 100644
index 000000000..cbd6b31bf
--- /dev/null
+++ b/src/main/resources/images/josm-ca/sequence-ca.svg
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   sodipodi:docname="sequence-ca.svg"
+   inkscape:version="1.0beta1 (32d4812, 2019-09-19)"
+   id="svg2711"
+   version="1.1"
+   viewBox="0 0 3.7041659 5.8208342"
+   height="5.8208342mm"
+   width="3.7041659mm">
+  <defs
+     id="defs2705" />
+  <sodipodi:namedview
+     inkscape:window-maximized="0"
+     inkscape:window-y="80"
+     inkscape:window-x="2095"
+     inkscape:window-height="907"
+     inkscape:window-width="1600"
+     showgrid="false"
+     inkscape:document-rotation="0"
+     inkscape:current-layer="layer1"
+     inkscape:document-units="mm"
+     inkscape:cy="11.000002"
+     inkscape:cx="6.9999986"
+     inkscape:zoom="31.772723"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     borderopacity="1.0"
+     bordercolor="#666666"
+     pagecolor="#ffffff"
+     id="base" />
+  <metadata
+     id="metadata2708">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     transform="translate(-77.522917,-90.071726)"
+     id="layer1"
+     inkscape:groupmode="layer"
+     inkscape:label="Layer 1">
+    <path
+       style="stroke-width:0.264583"
+       inkscape:connector-curvature="0"
+       fill-rule="evenodd"
+       clip-rule="evenodd"
+       d="m 77.900662,90.354605 1.474364,3.685871 1.474338,-3.685871 c -0.455798,-0.182491 -0.953347,-0.282879 -1.474338,-0.282879 -0.520991,0 -1.01854,0.100388 -1.474364,0.282879 z"
+       fill="#00769D"
+       id="path881" />
+    <path
+       style="stroke-width:0.264583"
+       inkscape:connector-curvature="0"
+       fill-rule="evenodd"
+       clip-rule="evenodd"
+       d="m 79.375,95.89256 c 1.022879,0 1.852083,-0.829205 1.852083,-1.852084 0,-1.022879 -0.829204,-1.852083 -1.852083,-1.852083 -1.022879,0 -1.852083,0.829204 -1.852083,1.852083 0,1.022879 0.829204,1.852084 1.852083,1.852084 z"
+       fill="#00B5F5"
+       id="path883" />
+  </g>
+</svg>
diff --git a/src/main/resources/images/josm-ca/sign-detection.svg b/src/main/resources/images/josm-ca/sign-detection.svg
new file mode 100644
index 000000000..8676a565b
--- /dev/null
+++ b/src/main/resources/images/josm-ca/sign-detection.svg
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   sodipodi:docname="sign-detection.svg"
+   inkscape:version="1.0beta1 (32d4812, 2019-09-19)"
+   id="svg2121"
+   version="1.1"
+   viewBox="0 0 2.116426 1.8521025"
+   height="1.8521025mm"
+   width="2.116426mm">
+  <defs
+     id="defs2115" />
+  <sodipodi:namedview
+     inkscape:window-maximized="0"
+     inkscape:window-y="23"
+     inkscape:window-x="0"
+     inkscape:window-height="907"
+     inkscape:window-width="1600"
+     showgrid="false"
+     inkscape:document-rotation="0"
+     inkscape:current-layer="layer1"
+     inkscape:document-units="mm"
+     inkscape:cy="3.5000343"
+     inkscape:cx="3.9995502"
+     inkscape:zoom="99.856111"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     borderopacity="1.0"
+     bordercolor="#666666"
+     pagecolor="#ffffff"
+     id="base" />
+  <metadata
+     id="metadata2118">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     transform="translate(-140.30488,-92.812044)"
+     id="layer1"
+     inkscape:groupmode="layer"
+     inkscape:label="Layer 1">
+    <path
+       style="stroke-width:0.264583"
+       inkscape:connector-curvature="0"
+       d="m 141.3631,94.397479 -0.83027,-1.453144 h 1.66053 z"
+       fill="white"
+       stroke="#D40000"
+       id="path841" />
+  </g>
+</svg>
diff --git a/test/unit/org/openstreetmap/josm/plugins/mapillary/oauth/UploadTest.java b/test/unit/org/openstreetmap/josm/plugins/mapillary/oauth/UploadTest.java
index 2bb1284f6..6a9ad3599 100644
--- a/test/unit/org/openstreetmap/josm/plugins/mapillary/oauth/UploadTest.java
+++ b/test/unit/org/openstreetmap/josm/plugins/mapillary/oauth/UploadTest.java
@@ -16,12 +16,14 @@
 import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata;
 import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
 import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants;
+import org.junit.Rule;
 import org.junit.Test;
 
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryImportedImage;
 import org.openstreetmap.josm.plugins.mapillary.utils.ImageImportUtil;
 import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryUtils;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
 
 /**
  * Tests the {@link UploadUtils} class.
@@ -30,6 +32,8 @@
  * @see UploadUtils
  */
 public class UploadTest {
+  @Rule
+  public JOSMTestRules rule = new JOSMTestRules().projection();
 
   /**
    * Tests the {@link UploadUtils#updateFile(MapillaryImportedImage)} method.

From 4f99847a49d5372f3276a3c1a5f0ed5481e1d7b5 Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Thu, 7 May 2020 08:05:40 -0600
Subject: [PATCH 16/20] Fix an issue where Mapillary servers being down would
 crash the plugin

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 .../data/mapillary/OrganizationRecord.java         | 14 +++++---------
 1 file changed, 5 insertions(+), 9 deletions(-)

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/data/mapillary/OrganizationRecord.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/data/mapillary/OrganizationRecord.java
index f16c1eb39..c161d29ac 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/data/mapillary/OrganizationRecord.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/data/mapillary/OrganizationRecord.java
@@ -41,8 +41,7 @@ public final class OrganizationRecord {
 
   private OrganizationRecord(
     String avatar, String description, String key, String name, String niceName, boolean privateRepository,
-    boolean publicRepository
-  ) {
+    boolean publicRepository) {
     this.avatar = createAvatarIcon(avatar, key);
     this.description = description;
     this.key = key;
@@ -57,10 +56,9 @@ private static ImageIcon createAvatarIcon(String avatar, String organizationKey)
       return ImageProvider.get(avatar, ImageProvider.ImageSizes.DEFAULT);
     } else if (organizationKey != null && !organizationKey.isEmpty()) {
       try (CachedFile possibleAvatar = new CachedFile(
-        MapillaryURL.APIv3.retrieveOrganizationAvatar(organizationKey).toExternalForm()
-      )) {
+        MapillaryURL.APIv3.retrieveOrganizationAvatar(organizationKey).toExternalForm())) {
         OAuthUtils.addAuthenticationHeader(possibleAvatar);
-        return ImageProvider.get(possibleAvatar.getFile().getAbsolutePath(), ImageProvider.ImageSizes.DEFAULT);
+        return ImageProvider.getIfAvailable(possibleAvatar.getFile().getAbsolutePath());
       } catch (IOException e) {
         Logging.error(e);
       }
@@ -70,12 +68,10 @@ private static ImageIcon createAvatarIcon(String avatar, String organizationKey)
 
   public static OrganizationRecord getOrganization(
     String avatar, String description, String key, String name, String niceName, boolean privateRepository,
-    boolean publicRepository
-  ) {
+    boolean publicRepository) {
     boolean newRecord = !CACHE.containsKey(key);
     OrganizationRecord record = CACHE.computeIfAbsent(
-      key, k -> new OrganizationRecord(avatar, description, key, name, niceName, privateRepository, publicRepository)
-    );
+      key, k -> new OrganizationRecord(avatar, description, key, name, niceName, privateRepository, publicRepository));
     // TODO remove when getNewOrganization is done, and make vars final again
     record.avatar = createAvatarIcon(avatar, key);
     record.description = description;

From 109c374038a36afe440b924940e46b8dfe0b2124 Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Fri, 8 May 2020 09:39:00 -0600
Subject: [PATCH 17/20] Disable point features (again).

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 .../plugins/mapillary/MapillaryPlugin.java    | 15 +++--
 .../mapillary/gui/MapillaryMainDialog.java    | 58 ++++++++++---------
 .../gui/dialog/MapillaryFilterDialog.java     | 34 +++++++----
 .../mapillary/utils/MapillaryProperties.java  |  2 +
 4 files changed, 66 insertions(+), 43 deletions(-)

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java
index 3cdeecc13..580fe2e5b 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryPlugin.java
@@ -12,6 +12,7 @@
 import javax.swing.JMenu;
 import javax.swing.JMenuItem;
 
+import org.openstreetmap.josm.actions.ExpertToggleAction;
 import org.openstreetmap.josm.actions.JosmAction;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.MainMenu;
@@ -116,9 +117,11 @@ public MapillaryPlugin(PluginInformation info) {
     MainMenu.add(menu.imagerySubMenu, mapObjectLayerAction, false);
     destroyables.add(mapObjectLayerAction);
 
-    MapPointObjectLayerAction mapPointObjectLayerAction = new MapPointObjectLayerAction();
-    MainMenu.add(menu.imagerySubMenu, mapPointObjectLayerAction, false);
-    destroyables.add(mapPointObjectLayerAction);
+    if (ExpertToggleAction.isExpert() && Boolean.TRUE.equals(MapillaryProperties.DEVELOPER_BROKEN.get())) {
+      MapPointObjectLayerAction mapPointObjectLayerAction = new MapPointObjectLayerAction();
+      MainMenu.add(menu.imagerySubMenu, mapPointObjectLayerAction, false);
+      destroyables.add(mapPointObjectLayerAction);
+    }
 
     mapFrameInitialized(null, MainApplication.getMap());
   }
@@ -153,8 +156,10 @@ public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
       newFrame.addToggleDialog(MapillaryChangesetDialog.getInstance(), false);
       toggleDialog.add(MapillaryFilterDialog.getInstance());
       newFrame.addToggleDialog(MapillaryFilterDialog.getInstance(), false);
-      newFrame.addToggleDialog(MapillaryExpertFilterDialog.getInstance(), true);
-      toggleDialog.add(MapillaryExpertFilterDialog.getInstance());
+      if (ExpertToggleAction.isExpert() && Boolean.TRUE.equals(MapillaryProperties.DEVELOPER_BROKEN.get())) {
+        newFrame.addToggleDialog(MapillaryExpertFilterDialog.getInstance(), true);
+        toggleDialog.add(MapillaryExpertFilterDialog.getInstance());
+      }
       mapillaryDownloadAction.setEnabled(true);
       // This fixes a UI issue -- for whatever reason, the tab pane is occasionally unusable when the expert filter
       // dialog is added.
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryMainDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryMainDialog.java
index 33d6eeff7..5fa1d9e15 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryMainDialog.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/MapillaryMainDialog.java
@@ -14,8 +14,10 @@
 import java.awt.image.BufferedImage;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
-import java.util.Arrays;
 import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import javax.imageio.ImageIO;
 import javax.swing.AbstractAction;
@@ -24,6 +26,7 @@
 import javax.swing.JPanel;
 import javax.swing.SwingUtilities;
 
+import org.openstreetmap.josm.actions.ExpertToggleAction;
 import org.openstreetmap.josm.actions.JosmAction;
 import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
 import org.openstreetmap.josm.data.cache.CacheEntry;
@@ -125,22 +128,6 @@ private MapillaryMainDialog() {
 
     panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
     panel.add(mapillaryImageDisplay);
-    JPanel buttons = new JPanel();
-    buttons.setLayout(new BoxLayout(buttons, BoxLayout.LINE_AXIS));
-    Dimension buttonDim = new Dimension(52, 34);
-    JButton toggleSigns = new JButton(showSignDetectionsAction);
-    JButton toggleDetections = new JButton(showDetectionOutlinesAction);
-    showDetectionOutlinesAction.setButton(toggleDetections);
-    showSignDetectionsAction.setButton(toggleSigns);
-    toggleDetections.setPreferredSize(buttonDim);
-    toggleSigns.setPreferredSize(buttonDim);
-    // Mac OS X won't show background colors if buttons aren't opaque.
-    toggleSigns.setOpaque(true);
-    toggleDetections.setOpaque(true);
-    buttons.add(toggleSigns);
-    buttons.add(toggleDetections);
-    panel.add(buttons);
-
     setMode(MODE.NORMAL);
   }
 
@@ -150,8 +137,7 @@ private static abstract class JosmButtonAction extends JosmAction {
 
     public JosmButtonAction(
       String name, ImageProvider icon, String tooltip, Shortcut shortcut, boolean registerInToolbar, String toolbarId,
-      boolean installAdapters
-    ) {
+      boolean installAdapters) {
       super(name, icon, tooltip, shortcut, registerInToolbar, toolbarId, installAdapters);
     }
 
@@ -181,8 +167,10 @@ private static class ShowDetectionOutlinesAction extends JosmButtonAction {
 
     ShowDetectionOutlinesAction() {
       super(
-        null, new ImageProvider("mapillary_sprite_source/package_objects", "object--traffic-light--other"), tr("Toggle detection outlines"), Shortcut.registerShortcut("mapillary:showdetections", tr("Mapillary: toggle detections"), KeyEvent.VK_UNDEFINED, Shortcut.NONE), false, null, false
-      );
+        null, new ImageProvider("mapillary_sprite_source/package_objects", "object--traffic-light--other"),
+        tr("Toggle detection outlines"), Shortcut.registerShortcut("mapillary:showdetections",
+          tr("Mapillary: toggle detections"), KeyEvent.VK_UNDEFINED, Shortcut.NONE),
+        false, null, false);
     }
 
     @Override
@@ -202,8 +190,10 @@ private static class ShowSignDetectionsAction extends JosmButtonAction {
 
     ShowSignDetectionsAction() {
       super(
-        null, new ImageProvider("mapillary_sprite_source/package_signs", "regulatory--go-straight-or-turn-left--g2"), tr("Toggle sign detection outlines"), Shortcut.registerShortcut("mapillary:showsigndetections", tr("Mapillary: toggle sign detections"), KeyEvent.VK_UNDEFINED, Shortcut.NONE), false, null, false
-      );
+        null, new ImageProvider("mapillary_sprite_source/package_signs", "regulatory--go-straight-or-turn-left--g2"),
+        tr("Toggle sign detection outlines"), Shortcut.registerShortcut("mapillary:showsigndetections",
+          tr("Mapillary: toggle sign detections"), KeyEvent.VK_UNDEFINED, Shortcut.NONE),
+        false, null, false);
     }
 
     @Override
@@ -244,16 +234,32 @@ public synchronized void setImageInfoHelp(ImageInfoHelpPopup popup) {
    * Sets a new mode for the dialog.
    *
    * @param mode
-   *          The mode to be set. Must not be {@code null}.
+   *             The mode to be set. Must not be {@code null}.
    */
   public void setMode(MODE mode) {
+    Dimension buttonDim = new Dimension(52, 34);
+    SideButton toggleSigns = new SideButton(showSignDetectionsAction);
+    showSignDetectionsAction.setButton(toggleSigns);
+    toggleSigns.setPreferredSize(buttonDim);
+    // Mac OS X won't show background colors if buttons aren't opaque.
+    toggleSigns.setOpaque(true);
+    SideButton toggleDetections = null;
+    if (ExpertToggleAction.isExpert() && Boolean.TRUE.equals(MapillaryProperties.DEVELOPER_BROKEN.get())) {
+      toggleDetections = new SideButton(showDetectionOutlinesAction);
+      showDetectionOutlinesAction.setButton(toggleDetections);
+      toggleDetections.setPreferredSize(buttonDim);
+      toggleDetections.setOpaque(true);
+    }
     switch (mode) {
     case WALK:
-      createLayout(this.panel, Arrays.asList(playButton, pauseButton, stopButton));
+      createLayout(this.panel, Stream.of(toggleSigns, toggleDetections, playButton, pauseButton, stopButton)
+        .filter(Objects::nonNull).collect(Collectors.toList()));
       break;
     case NORMAL:
     default:
-      createLayout(this.panel, Arrays.asList(blueButton, previousButton, nextButton, redButton));
+      createLayout(this.panel,
+        Stream.of(blueButton, previousButton, toggleSigns, toggleDetections, nextButton, redButton)
+          .filter(Objects::nonNull).collect(Collectors.toList()));
       break;
     }
     disableAllButtons();
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryFilterDialog.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryFilterDialog.java
index d181ba351..b585767da 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryFilterDialog.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/dialog/MapillaryFilterDialog.java
@@ -33,6 +33,7 @@
 import javax.swing.JTextField;
 import javax.swing.SpinnerNumberModel;
 
+import org.openstreetmap.josm.actions.ExpertToggleAction;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.SideButton;
 import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
@@ -52,6 +53,7 @@
 import org.openstreetmap.josm.plugins.mapillary.model.UserProfile;
 import org.openstreetmap.josm.plugins.mapillary.oauth.MapillaryLoginListener;
 import org.openstreetmap.josm.plugins.mapillary.oauth.MapillaryUser;
+import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryProperties;
 import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.ImageProvider;
 
@@ -61,15 +63,16 @@
  * @author nokutu
  * @see MapillaryFilterChooseSigns
  */
-public final class MapillaryFilterDialog extends ToggleDialog implements MapillaryDataListener, MapillaryLoginListener, OrganizationRecordListener {
+public final class MapillaryFilterDialog extends ToggleDialog
+  implements MapillaryDataListener, MapillaryLoginListener, OrganizationRecordListener {
 
   private static final long serialVersionUID = -4192029663670922103L;
 
   private static MapillaryFilterDialog instance;
 
-  private static final String[] TIME_LIST = {tr("Years"), tr("Months"), tr("Days")};
+  private static final String[] TIME_LIST = { tr("Years"), tr("Months"), tr("Days") };
 
-  private static final long[] TIME_FACTOR = new long[]{
+  private static final long[] TIME_FACTOR = new long[] {
     31_536_000_000L, // = 365 * 24 * 60 * 60 * 1000 = number of ms in a year
     2_592_000_000L, // = 30 * 24 * 60 * 60 * 1000 = number of ms in a month
     86_400_000 // = 24 * 60 * 60 * 1000 = number of ms in a day
@@ -101,8 +104,8 @@ public final class MapillaryFilterDialog extends ToggleDialog implements Mapilla
 
   private MapillaryFilterDialog() {
     super(
-      tr("Mapillary filter"), "mapillary-filter", tr("Open Mapillary filter dialog"), null, 200, false, MapillaryPreferenceSetting.class
-    );
+      tr("Mapillary filter"), "mapillary-filter", tr("Open Mapillary filter dialog"), null, 200, false,
+      MapillaryPreferenceSetting.class);
     MapillaryUser.addListener(this);
 
     this.signChooser.setEnabled(false);
@@ -166,7 +169,8 @@ private MapillaryFilterDialog() {
       private static final long serialVersionUID = -1650696801628131389L;
 
       @Override
-      public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
+      public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,
+        boolean cellHasFocus) {
         JLabel comp = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
         if (value instanceof OrganizationRecord) {
           OrganizationRecord organization = (OrganizationRecord) value;
@@ -206,10 +210,13 @@ public Component getListCellRendererComponent(JList<?> list, Object value, int i
     signs.add(signChooserPanel, GBC.eol().anchor(GridBagConstraints.LINE_START));
     panel.add(signs, GBC.eol().anchor(GridBagConstraints.LINE_START));
 
-    panel.add(new JSeparator(), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
-    objectFilter = new TrafficSignFilter();
-    panel.add(objectFilter, GBC.eol().fill().anchor(GridBagConstraints.WEST));
-
+    if (ExpertToggleAction.isExpert() && Boolean.TRUE.equals(MapillaryProperties.DEVELOPER_BROKEN.get())) {
+      panel.add(new JSeparator(), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
+      objectFilter = new TrafficSignFilter();
+      panel.add(objectFilter, GBC.eol().fill().anchor(GridBagConstraints.WEST));
+    } else {
+      objectFilter = null;
+    }
     createLayout(panel, true, Arrays.asList(new SideButton(new UpdateAction()), new SideButton(new ResetAction())));
   }
 
@@ -280,7 +287,8 @@ public void reset() {
     this.time.setSelectedItem(TIME_LIST[0]);
     this.signChooser.setEnabled(false);
     this.spinnerModel.setValue(1);
-    this.objectFilter.reset();
+    if (this.objectFilter != null)
+      this.objectFilter.reset();
     if (this.endDate != null && this.startDate != null) {
       this.endDate.reset();
       this.startDate.reset();
@@ -509,7 +517,9 @@ public void actionPerformed(ActionEvent arg0) {
   public void destroy() {
     if (!destroyed) {
       super.destroy();
-      objectFilter.destroy();
+      if (objectFilter != null) {
+        objectFilter.destroy();
+      }
       MainApplication.getMap().removeToggleDialog(this);
       // OrganizationRecord.removeOrganizationListener(this); // TODO uncomment when API for orgs is available
       MapillaryUser.removeListener(this);
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryProperties.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryProperties.java
index bf5801f76..5e3d27c22 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryProperties.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryProperties.java
@@ -16,6 +16,8 @@
 public final class MapillaryProperties {
   public static final BooleanProperty DELETE_AFTER_UPLOAD = new BooleanProperty("mapillary.delete-after-upload", true);
   public static final BooleanProperty DEVELOPER = new BooleanProperty("mapillary.developer", false);
+  /** This is for WIP items that may be very broken */
+  public static final BooleanProperty DEVELOPER_BROKEN = new BooleanProperty("mapillary.developer.broken", false);
   public static final BooleanProperty DISPLAY_HOUR = new BooleanProperty("mapillary.display-hour", true);
   public static final BooleanProperty HOVER_ENABLED = new BooleanProperty("mapillary.hover-enabled", true);
   public static final BooleanProperty DARK_MODE = new BooleanProperty("mapillary.dark-mode", true);

From 9628284699d3864994a3daf2d2ed2ea07a599f05 Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Fri, 8 May 2020 09:39:48 -0600
Subject: [PATCH 18/20] Finish up switch to sprites (360 halo is still drawn)

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 .../josm/plugins/mapillary/gui/layer/MapillaryLayer.java      | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
index 5dda447a0..76d4a5f3b 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/gui/layer/MapillaryLayer.java
@@ -106,7 +106,7 @@ public final class MapillaryLayer extends AbstractModifiableLayer implements
   /** The radius of the circular sector that indicates the camera angle */
   private static final int CA_INDICATOR_RADIUS = 15;
   /** Length of the edge of the small sign, which indicates that traffic signs have been found in an image. */
-  private static final int TRAFFIC_SIGN_SIZE = ImageProvider.ImageSizes.MAP.getAdjustedWidth();
+  private static final int TRAFFIC_SIGN_SIZE = (int) (ImageProvider.ImageSizes.MAP.getAdjustedWidth() / 1.5);
   /** The range to paint the full detection image at */
   private static final Range IMAGE_CA_PAINT_RANGE = Selector.GeneralSelector.fromLevel(18, Integer.MAX_VALUE);
 
@@ -472,7 +472,7 @@ private void drawImageMarker(final Graphics2D g, final MapillaryAbstractImage im
     }
 
     if (img instanceof MapillaryImage && !((MapillaryImage) img).getDetections().isEmpty()) {
-      g.drawImage(YIELD_SIGN, (int) (p.getX() - TRAFFIC_SIGN_SIZE / 2d), (int) (p.getY() - TRAFFIC_SIGN_SIZE / 2d),
+      g.drawImage(YIELD_SIGN, (int) (p.getX() - TRAFFIC_SIGN_SIZE / 3d), (int) (p.getY() - TRAFFIC_SIGN_SIZE / 3d),
         null);
     }
     g.setComposite(composite);

From e6a9fb8a06a1b744d6b26e1e4156c20338a92268 Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Fri, 8 May 2020 10:24:48 -0600
Subject: [PATCH 19/20] Drop stroke to indicate selection for the current image

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 src/main/resources/images/josm-ca/current-ca.svg | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/main/resources/images/josm-ca/current-ca.svg b/src/main/resources/images/josm-ca/current-ca.svg
index 75923fbb6..9d61ef91b 100644
--- a/src/main/resources/images/josm-ca/current-ca.svg
+++ b/src/main/resources/images/josm-ca/current-ca.svg
@@ -66,8 +66,6 @@
        clip-rule="evenodd"
        d="m 95.240868,92.708693 c 1.022879,0 1.852078,-0.829204 1.852078,-1.852083 0,-1.022879 -0.829199,-1.852083 -1.852078,-1.852083 -1.022879,0 -1.852084,0.829204 -1.852084,1.852083 0,1.022879 0.829205,1.852083 1.852084,1.852083 z"
        fill="#F5811A"
-       stroke="white"
-       stroke-width="1"
        id="path861" />
   </g>
 </svg>

From 6f1fe9c5dca62cda82a01180bb2019b5cddbf438 Mon Sep 17 00:00:00 2001
From: Taylor Smock <taylor.smock@kaart.com>
Date: Mon, 11 May 2020 07:54:40 -0600
Subject: [PATCH 20/20] Update minimum JOSM version to deal with JOSM-19208

This is due to updating JCS (the caching library we use).
JCS changed the package name from "org.apache.commons.jcs" to
"org.apache.commons.jcs3".

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
---
 build.gradle.kts                                              | 1 +
 gradle.properties                                             | 4 ++--
 .../openstreetmap/josm/plugins/mapillary/MapillaryData.java   | 2 +-
 .../openstreetmap/josm/plugins/mapillary/cache/Caches.java    | 4 ++--
 4 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/build.gradle.kts b/build.gradle.kts
index 9459ce66e..79a410c6b 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -133,6 +133,7 @@ josm {
   debugPort = 7051
   manifest {
     // See https://floscher.gitlab.io/gradle-josm-plugin/kdoc/latest/gradle-josm-plugin/org.openstreetmap.josm.gradle.plugin.config/-josm-manifest/old-version-download-link.html
+    oldVersionDownloadLink(16114, "v1.5.22", URL("https://github.com/JOSM/Mapillary/releases/download/v1.5.22/Mapillary.jar"))
     oldVersionDownloadLink(15909, "v1.5.20", URL("https://github.com/JOSM/Mapillary/releases/download/v1.5.20/Mapillary.jar"))
     oldVersionDownloadLink(14149, "v1.5.16", URL("https://github.com/JOSM/Mapillary/releases/download/v1.5.16/Mapillary.jar"))
     oldVersionDownloadLink(13733, "v1.5.15", URL("https://github.com/JOSM/Mapillary/releases/download/v1.5.15/Mapillary.jar"))
diff --git a/gradle.properties b/gradle.properties
index 5a2faaa3f..024b240e4 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -6,11 +6,11 @@ plugin.icon=images/mapillary-logo.svg
 plugin.link=https://wiki.openstreetmap.org/wiki/JOSM/Plugins/Mapillary
 # 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=16114
+plugin.main.version=16402
 # 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=16114
+plugin.compile.version=16402
 plugin.requires=apache-commons;apache-http;javafx
 
 # Character encoding of Gradle files
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryData.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryData.java
index ee32057f2..af0ddcd2f 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryData.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/MapillaryData.java
@@ -22,7 +22,7 @@
 import javax.json.JsonReader;
 import javax.swing.SwingUtilities;
 
-import org.apache.commons.jcs.access.CacheAccess;
+import org.apache.commons.jcs3.access.CacheAccess;
 
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.Data;
diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/Caches.java b/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/Caches.java
index 70a33adf1..60ecb3f2a 100644
--- a/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/Caches.java
+++ b/src/main/java/org/openstreetmap/josm/plugins/mapillary/cache/Caches.java
@@ -6,8 +6,8 @@
 
 import javax.swing.ImageIcon;
 
-import org.apache.commons.jcs.access.CacheAccess;
-import org.apache.commons.jcs.engine.behavior.IElementAttributes;
+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;