    001// License: GPL. For details, see LICENSE file.
    002package org.openstreetmap.josm.plugins.streetside;
    003
    004import java.awt.AlphaComposite;
    005import java.awt.BasicStroke;
    006import java.awt.Color;
    007import java.awt.Composite;
    008import java.awt.Graphics2D;
    009import java.awt.GraphicsEnvironment;
    010import java.awt.Point;
    011import java.awt.Rectangle;
    012import java.awt.RenderingHints;
    013import java.awt.TexturePaint;
    014import java.awt.geom.Line2D;
    015import java.awt.image.BufferedImage;
    016import java.util.Comparator;
    017import java.util.IntSummaryStatistics;
    018import java.util.Optional;
    019
    020import javax.swing.Action;
    021import javax.swing.Icon;
    022
    023import org.openstreetmap.josm.Main;
    024import org.openstreetmap.josm.data.Bounds;
    025import org.openstreetmap.josm.data.osm.DataSet;
    026import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
    027import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
    028import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
    029import org.openstreetmap.josm.gui.MainApplication;
    030import org.openstreetmap.josm.gui.MapView;
    031import org.openstreetmap.josm.gui.NavigatableComponent;
    032import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
    033import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
    034import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
    035import org.openstreetmap.josm.gui.layer.Layer;
    036import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
    037import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
    038import org.openstreetmap.josm.plugins.streetside.cache.CacheUtils;
    039import org.openstreetmap.josm.plugins.streetside.gui.StreetsideMainDialog;
    040import org.openstreetmap.josm.plugins.streetside.history.StreetsideRecord;
    041import org.openstreetmap.josm.plugins.streetside.io.download.StreetsideDownloader;
    042import org.openstreetmap.josm.plugins.streetside.io.download.StreetsideDownloader.DOWNLOAD_MODE;
    043import org.openstreetmap.josm.plugins.streetside.mode.AbstractMode;
    044import org.openstreetmap.josm.plugins.streetside.mode.JoinMode;
    045import org.openstreetmap.josm.plugins.streetside.mode.SelectMode;
    046import org.openstreetmap.josm.plugins.streetside.utils.MapViewGeometryUtil;
    047import org.openstreetmap.josm.plugins.streetside.utils.StreetsideColorScheme;
    048import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
    049import org.openstreetmap.josm.plugins.streetside.utils.StreetsideUtils;
    050import org.openstreetmap.josm.tools.I18n;
    051import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
    052import org.openstreetmap.josm.tools.Logging;
    053
    054/**
    055 * This class represents the layer shown in JOSM. There can only exist one
    056 * instance of this object.
    057 *
    058 * @author nokutu
    059 */
    060public final class StreetsideLayer extends AbstractModifiableLayer implements
    061ActiveLayerChangeListener, StreetsideDataListener {
    062
    063  /** The radius of the image marker */
    064  private static final int IMG_MARKER_RADIUS = 7;
    065  /** The radius of the circular sector that indicates the camera angle */
    066  private static final int CA_INDICATOR_RADIUS = 15;
    067  /** The angle of the circular sector that indicates the camera angle */
    068  private static final int CA_INDICATOR_ANGLE = 40;
    069  /** Length of the edge of the small sign, which indicates that traffic signs have been found in an image. */
    070  private static final int TRAFFIC_SIGN_SIZE = 6;
    071  /** A third of the height of the sign, for easier calculations */
    072  private static final double TRAFFIC_SIGN_HEIGHT_3RD = Math.sqrt(
    073    Math.pow(TRAFFIC_SIGN_SIZE, 2) - Math.pow(TRAFFIC_SIGN_SIZE / 2d, 2)
    074  ) / 3;
    075
    076        private static final DataSetListenerAdapter DATASET_LISTENER =
    077                        new DataSetListenerAdapter(e -> {
    078                                if (e instanceof DataChangedEvent && StreetsideDownloader.getMode() == DOWNLOAD_MODE.OSM_AREA) {
    079                                        // When more data is downloaded, a delayed update is thrown, in order to
    080                                        // wait for the data bounds to be set.
    081                                        MainApplication.worker.execute(StreetsideDownloader::downloadOSMArea);
    082                                }
    083                        });
    084
    085        /** Unique instance of the class. */
    086        private static StreetsideLayer instance;
    087        /** The nearest images to the selected image from different sequences sorted by distance from selection. */
    088        private StreetsideImage[] nearestImages = {};
    089        /** {@link StreetsideData} object that stores the database. */
    090        private final StreetsideData data;
    091
    092        /** Mode of the layer. */
    093        public AbstractMode mode;
    094
    095        private volatile TexturePaint hatched;
    096        private final StreetsideLocationChangeset locationChangeset = new StreetsideLocationChangeset();
    097
    098        private StreetsideLayer() {
    099                super(I18n.tr("Microsoft Streetside Images"));
    100                data = new StreetsideData();
    101                data.addListener(this);
    102        }
    103
    104  /**
    105   * Initializes the Layer.
    106   */
    107  private void init() {
    108    final DataSet ds = MainApplication.getLayerManager().getEditDataSet();
    109    if (ds != null) {
    110      ds.addDataSetListener(DATASET_LISTENER);
    111    }
    112    MainApplication.getLayerManager().addActiveLayerChangeListener(this);
    113    if (!GraphicsEnvironment.isHeadless()) {
    114      setMode(new SelectMode());
    115      if (StreetsideDownloader.getMode() == DOWNLOAD_MODE.OSM_AREA) {
    116        MainApplication.worker.execute(StreetsideDownloader::downloadOSMArea);
    117      }
    118      if (StreetsideDownloader.getMode() == DOWNLOAD_MODE.VISIBLE_AREA) {
    119        mode.zoomChanged();
    120      }
    121    }
    122    // Does not execute when in headless mode
    123    if (Main.main != null && !StreetsideMainDialog.getInstance().isShowing()) {
    124      StreetsideMainDialog.getInstance().showDialog();
    125    }
    126    if (StreetsidePlugin.getMapView() != null) {
    127      StreetsideMainDialog.getInstance().streetsideImageDisplay.repaint();
    128      /*StreetsideMainDialog.getInstance()
    129        .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
    130        .put(KeyStroke.getKeyStroke("DELETE"), "StreetsideDel");
    131      StreetsideMainDialog.getInstance().getActionMap()
    132        .put("StreetsideDel", new DeleteImageAction());*/
    133
    134                        // There is no delete image action for Streetside (Streetside functionality here removed).
    135                        //getLocationChangeset().addChangesetListener(StreetsideChangesetDialog.getInstance());
    136                }
    137                createHatchTexture();
    138                invalidate();
    139        }
    140
    141  public static void invalidateInstance() {
    142    if (hasInstance()) {
    143      getInstance().invalidate();
    144    }
    145  }
    146
    147  /**
    148   * Changes the mode the the given one.
    149   *
    150   * @param mode The mode that is going to be activated.
    151   */
    152  public void setMode(AbstractMode mode) {
    153    final MapView mv = StreetsidePlugin.getMapView();
    154    if (this.mode != null && mv != null) {
    155      mv.removeMouseListener(this.mode);
    156      mv.removeMouseMotionListener(this.mode);
    157      NavigatableComponent.removeZoomChangeListener(this.mode);
    158    }
    159    this.mode = mode;
    160    if (mode != null && mv != null) {
    161      mv.setNewCursor(mode.cursor, this);
    162      mv.addMouseListener(mode);
    163      mv.addMouseMotionListener(mode);
    164      NavigatableComponent.addZoomChangeListener(mode);
    165      StreetsideUtils.updateHelpText();
    166    }
    167  }
    168
    169  private static synchronized void clearInstance() {
    170    instance = null;
    171  }
    172
    173  /**
    174   * Returns the unique instance of this class.
    175   *
    176   * @return The unique instance of this class.
    177   */
    178  public static synchronized StreetsideLayer getInstance() {
    179    if (instance != null) {
    180      return instance;
    181    }
    182    final StreetsideLayer layer = new StreetsideLayer();
    183    layer.init();
    184    instance = layer; // Only set instance field after initialization is complete
    185    return instance;
    186  }
    187
    188  /**
    189   * @return if the unique instance of this layer is currently instantiated
    190   */
    191  public static boolean hasInstance() {
    192    return instance != null;
    193  }
    194
    195        /**
    196         * Returns the {@link StreetsideData} object, which acts as the database of the
    197         * Layer.
    198         *
    199         * @return The {@link StreetsideData} object that stores the database.
    200         */
    201        public StreetsideData getData() {
    202                return data;
    203        }
    204
    205  /**
    206   * Returns the {@link StreetsideLocationChangeset} object, which acts as the database of the
    207   * Layer.
    208   *
    209   * @return The {@link StreetsideData} object that stores the database.
    210   */
    211  public StreetsideLocationChangeset getLocationChangeset() {
    212    return locationChangeset;
    213  }
    214
    215  /**
    216   * Returns the n-nearest image, for n=1 the nearest one is returned, for n=2 the second nearest one and so on.
    217   * The "n-nearest image" is picked from the list of one image from every sequence that is nearest to the currently
    218   * selected image, excluding the sequence to which the selected image belongs.
    219   * @param n the index for picking from the list of "nearest images", beginning from 1
    220   * @return the n-nearest image to the currently selected image
    221   */
    222  public synchronized StreetsideImage getNNearestImage(final int n) {
    223    return n >= 1 && n <= nearestImages.length ? nearestImages[n - 1] : null;
    224  }
    225
    226  @Override
    227  public synchronized void destroy() {
    228    clearInstance();
    229    setMode(null);
    230    StreetsideRecord.getInstance().reset();
    231    AbstractMode.resetThread();
    232    StreetsideDownloader.stopAll();
    233    if (StreetsideMainDialog.hasInstance()) {
    234      StreetsideMainDialog.getInstance().setImage(null);
    235      StreetsideMainDialog.getInstance().updateImage();
    236    }
    237    final MapView mv = StreetsidePlugin.getMapView();
    238    if (mv != null) {
    239      mv.removeMouseListener(mode);
    240      mv.removeMouseMotionListener(mode);
    241    }
    242    try {
    243      MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
    244      if (MainApplication.getLayerManager().getEditDataSet() != null) {
    245        MainApplication.getLayerManager().getEditDataSet().removeDataSetListener(DATASET_LISTENER);
    246      }
    247    } catch (IllegalArgumentException e) {
    248      // 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.
    249    }
    250    super.destroy();
    251  }
    252
    253
    254        @Override
    255  public boolean isModified() {
    256    return data.getImages().parallelStream().anyMatch(StreetsideAbstractImage::isModified);
    257  }
    258
    259  @Override
    260  public void setVisible(boolean visible) {
    261    super.setVisible(visible);
    262    getData().getImages().parallelStream().forEach(img -> img.setVisible(visible));
    263    if (MainApplication.getMap() != null) {
    264      //StreetsideFilterDialog.getInstance().refresh();
    265    }
    266  }
    267
    268  /**
    269   * Initialize the hatch pattern used to paint the non-downloaded area.
    270   */
    271  private void createHatchTexture() {
    272    BufferedImage bi = new BufferedImage(15, 15, BufferedImage.TYPE_INT_ARGB);
    273    Graphics2D big = bi.createGraphics();
    274    big.setColor(StreetsideProperties.BACKGROUND.get());
    275    Composite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f);
    276    big.setComposite(comp);
    277    big.fillRect(0, 0, 15, 15);
    278    big.setColor(StreetsideProperties.OUTSIDE_DOWNLOADED_AREA.get());
    279    big.drawLine(0, 15, 15, 0);
    280    Rectangle r = new Rectangle(0, 0, 15, 15);
    281    hatched = new TexturePaint(bi, r);
    282  }
    283
    284  @Override
    285  public synchronized void paint(final Graphics2D g, final MapView mv, final Bounds box) {
    286    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    287    if (MainApplication.getLayerManager().getActiveLayer() == this) {
    288      // paint remainder
    289      g.setPaint(hatched);
    290      g.fill(MapViewGeometryUtil.getNonDownloadedArea(mv, data.getBounds()));
    291    }
    292
    293    // Draw the blue and red line
    294    synchronized (StreetsideLayer.class) {
    295      final StreetsideAbstractImage selectedImg = data.getSelectedImage();
    296      for (int i = 0; i < nearestImages.length && selectedImg != null; i++) {
    297        if (i == 0) {
    298          g.setColor(Color.RED);
    299        } else {
    300          g.setColor(Color.BLUE);
    301        }
    302        final Point selected = mv.getPoint(selectedImg.getMovingLatLon());
    303        final Point p = mv.getPoint(nearestImages[i].getMovingLatLon());
    304        g.draw(new Line2D.Double(p.getX(), p.getY(), selected.getX(), selected.getY()));
    305      }
    306    }
    307
    308    // Draw sequence line
    309    /*g.setStroke(new BasicStroke(2));
    310    final StreetsideAbstractImage selectedImage = getData().getSelectedImage();
    311    for (StreetsideSequence seq : getData().getSequences()) {
    312      if (seq.getImages().contains(selectedImage)) {
    313        g.setColor(
    314          seq.getId() == null ? StreetsideColorScheme.SEQ_IMPORTED_SELECTED : StreetsideColorScheme.SEQ_SELECTED
    315        );
    316      } else {
    317        g.setColor(
    318          seq.getId() == null ? StreetsideColorScheme.SEQ_IMPORTED_UNSELECTED : StreetsideColorScheme.SEQ_UNSELECTED
    319        );
    320      }
    321      g.draw(MapViewGeometryUtil.getSequencePath(mv, seq));
    322    }*/
    323    for (StreetsideAbstractImage imageAbs : data.getImages()) {
    324      if (imageAbs.isVisible() && mv != null && mv.contains(mv.getPoint(imageAbs.getMovingLatLon()))) {
    325        drawImageMarker(g, imageAbs);
    326      }
    327    }
    328    if (mode instanceof JoinMode) {
    329      mode.paint(g, mv, box);
    330    }
    331  }
    332
    333  /**
    334   * Draws an image marker onto the given Graphics context.
    335   * @param g the Graphics context
    336   * @param img the image to be drawn onto the Graphics context
    337   */
    338  private void drawImageMarker(final Graphics2D g, final StreetsideAbstractImage img) {
    339    if (img == null || img.getLatLon() == null) {
    340      Logging.warn("An image is not painted, because it is null or has no LatLon!");
    341      return;
    342    }
    343    final StreetsideAbstractImage selectedImg = getData().getSelectedImage();
    344    final Point p = MainApplication.getMap().mapView.getPoint(img.getMovingLatLon());
    345
    346    // Determine colors
    347    final Color markerC;
    348    final Color directionC;
    349    if (selectedImg != null && getData().getMultiSelectedImages().contains(img)) {
    350      markerC = img instanceof StreetsideImportedImage
    351        ? StreetsideColorScheme.SEQ_IMPORTED_HIGHLIGHTED
    352        : StreetsideColorScheme.SEQ_HIGHLIGHTED;
    353      directionC = img instanceof StreetsideImportedImage
    354        ? StreetsideColorScheme.SEQ_IMPORTED_HIGHLIGHTED_CA
    355        : StreetsideColorScheme.SEQ_HIGHLIGHTED_CA;
    356    } else if (selectedImg != null && selectedImg.getSequence() != null && selectedImg.getSequence().equals(img.getSequence())) {
    357      markerC = img instanceof StreetsideImportedImage
    358        ? StreetsideColorScheme.SEQ_IMPORTED_SELECTED
    359        : StreetsideColorScheme.SEQ_SELECTED;
    360      directionC = img instanceof StreetsideImportedImage
    361        ? StreetsideColorScheme.SEQ_IMPORTED_SELECTED_CA
    362        : StreetsideColorScheme.SEQ_SELECTED_CA;
    363    } else {
    364      markerC = img instanceof StreetsideImportedImage
    365        ? StreetsideColorScheme.SEQ_IMPORTED_UNSELECTED
    366        : StreetsideColorScheme.SEQ_UNSELECTED;
    367      directionC = img instanceof StreetsideImportedImage
    368        ? StreetsideColorScheme.SEQ_IMPORTED_UNSELECTED_CA
    369        : StreetsideColorScheme.SEQ_UNSELECTED_CA;
    370    }
    371
    372    // Paint direction indicator
    373    g.setColor(directionC);
    374    g.fillArc(p.x - CA_INDICATOR_RADIUS, p.y - CA_INDICATOR_RADIUS, 2 * CA_INDICATOR_RADIUS, 2 * CA_INDICATOR_RADIUS, (int) (90 - img.getMovingHe() - CA_INDICATOR_ANGLE / 2d), CA_INDICATOR_ANGLE);
    375    // Paint image marker
    376    g.setColor(markerC);
    377    g.fillOval(p.x - IMG_MARKER_RADIUS, p.y - IMG_MARKER_RADIUS, 2 * IMG_MARKER_RADIUS, 2 * IMG_MARKER_RADIUS);
    378
    379    // Paint highlight for selected or highlighted images
    380    if (img.equals(getData().getHighlightedImage()) || getData().getMultiSelectedImages().contains(img)) {
    381      g.setColor(Color.WHITE);
    382      g.setStroke(new BasicStroke(2));
    383      g.drawOval(p.x - IMG_MARKER_RADIUS, p.y - IMG_MARKER_RADIUS, 2 * IMG_MARKER_RADIUS, 2 * IMG_MARKER_RADIUS);
    384    }
    385
    386
    387                /*if (img instanceof StreetsideImage && !((StreetsideImage) img).getDetections().isEmpty()) {
    388                        final Path2D trafficSign = new Path2D.Double();
    389                        trafficSign.moveTo(p.getX() - StreetsideLayer.TRAFFIC_SIGN_SIZE / 2d, p.getY() - StreetsideLayer.TRAFFIC_SIGN_HEIGHT_3RD);
    390                        trafficSign.lineTo(p.getX() + StreetsideLayer.TRAFFIC_SIGN_SIZE / 2d, p.getY() - StreetsideLayer.TRAFFIC_SIGN_HEIGHT_3RD);
    391                        trafficSign.lineTo(p.getX(), p.getY() + 2 * StreetsideLayer.TRAFFIC_SIGN_HEIGHT_3RD);
    392                        trafficSign.closePath();
    393                        g.setColor(Color.WHITE);
    394                        g.fill(trafficSign);
    395                        g.setStroke(new BasicStroke(1));
    396                        g.setColor(Color.RED);
    397                        g.draw(trafficSign);
    398                }*/
    399        }
    400
    401  @Override
    402  public Icon getIcon() {
    403    return StreetsidePlugin.LOGO.setSize(ImageSizes.LAYER).get();
    404  }
    405
    406  @Override
    407  public boolean isMergable(Layer other) {
    408    return false;
    409  }
    410
    411  @Override
    412  public void mergeFrom(Layer from) {
    413    throw new UnsupportedOperationException(
    414      "This layer does not support merging yet");
    415  }
    416
    417  @Override
    418  public Action[] getMenuEntries() {
    419    return new Action[]{
    420      LayerListDialog.getInstance().createShowHideLayerAction(),
    421      LayerListDialog.getInstance().createDeleteLayerAction(),
    422      new LayerListPopup.InfoAction(this)
    423    };
    424  }
    425
    426  @Override
    427  public Object getInfoComponent() {
    428    IntSummaryStatistics seqSizeStats = getData().getSequences().stream().mapToInt(seq -> seq.getImages().size()).summaryStatistics();
    429    return new StringBuilder(I18n.tr("Streetside layer"))
    430      .append('\n')
    431      .append(I18n.tr(
    432        "{0} sequences, each containing between {1} and {2} images (ø {3})",
    433        getData().getSequences().size(),
    434        seqSizeStats.getCount() <= 0 ? 0 : seqSizeStats.getMin(),
    435        seqSizeStats.getCount() <= 0 ? 0 : seqSizeStats.getMax(),
    436        seqSizeStats.getAverage()
    437      ))
    438      .append("\n\n")
    439      .append(I18n.tr(
    440        "{0} imported images",
    441        getData().getImages().stream().filter(i -> i instanceof StreetsideImportedImage).count()
    442      ))
    443      .append("\n+ ")
    444      .append(I18n.tr(
    445        "{0} downloaded images",
    446        getData().getImages().stream().filter(i -> i instanceof StreetsideImage).count()
    447      ))
    448      .append("\n= ")
    449      .append(I18n.tr(
    450        "{0} images in total",
    451        getData().getImages().size()
    452      )).toString();
    453  }
    454
    455  @Override
    456  public String getToolTipText() {
    457    return I18n.tr("{0} images in {1} sequences", getData().getImages().size(), getData().getSequences().size());
    458  }
    459
    460  @Override
    461  public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
    462    if (MainApplication.getLayerManager().getActiveLayer() == this) {
    463      StreetsideUtils.updateHelpText();
    464    }
    465
    466    if (MainApplication.getLayerManager().getEditLayer() != e.getPreviousDataLayer()) {
    467      if (MainApplication.getLayerManager().getEditLayer() != null) {
    468        MainApplication.getLayerManager().getEditLayer().getDataSet().addDataSetListener(DATASET_LISTENER);
    469      }
    470      if (e.getPreviousDataLayer() != null) {
    471        e.getPreviousDataLayer().getDataSet().removeDataSetListener(DATASET_LISTENER);
    472      }
    473    }
    474  }
    475
    476  @Override
    477  public void visitBoundingBox(BoundingXYVisitor v) {
    478  }
    479
    480  /* (non-Javadoc)
    481   * @see org.openstreetmap.josm.plugins.streetside.StreetsideDataListener#imagesAdded()
    482   */
    483  @Override
    484  public void imagesAdded() {
    485    updateNearestImages();
    486  }
    487
    488  /* (non-Javadoc)
    489   * @see org.openstreetmap.josm.plugins.streetside.StreetsideDataListener#selectedImageChanged(org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage, org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage)
    490   */
    491  @Override
    492  public void selectedImageChanged(StreetsideAbstractImage oldImage, StreetsideAbstractImage newImage) {
    493    updateNearestImages();
    494  }
    495
    496  /**
    497   * Returns the closest images belonging to a different sequence and
    498   * different from the specified target image.
    499   *
    500   * @param target the image for which you want to find the nearest other images
    501   * @param limit the maximum length of the returned array
    502   * @return An array containing the closest images belonging to different sequences sorted by distance from target.
    503   */
    504  private StreetsideImage[] getNearestImagesFromDifferentSequences(StreetsideAbstractImage target, int limit) {
    505    return data.getSequences().parallelStream()
    506      .filter(seq -> seq.getId() != null && !seq.getId().equals(target.getSequence().getId()))
    507      .map(seq -> { // Maps sequence to image from sequence that is nearest to target
    508        Optional<StreetsideAbstractImage> resImg = seq.getImages().parallelStream()
    509          .filter(img -> img instanceof StreetsideImage && img.isVisible())
    510          .min(new NearestImgToTargetComparator(target));
    511        return resImg.orElse(null);
    512      })
    513      .filter(img -> // Filters out images too far away from target
    514        img != null &&
    515        img.getMovingLatLon().greatCircleDistance(target.getMovingLatLon())
    516          < StreetsideProperties.SEQUENCE_MAX_JUMP_DISTANCE.get()
    517       )
    518      .sorted(new NearestImgToTargetComparator(target))
    519      .limit(limit)
    520      .toArray(StreetsideImage[]::new);
    521  }
    522
    523  private synchronized void updateNearestImages() {
    524    final StreetsideAbstractImage selected = data.getSelectedImage();
    525    if (selected != null) {
    526      nearestImages = getNearestImagesFromDifferentSequences(selected, 2);
    527    } else {
    528      nearestImages = new StreetsideImage[0];
    529    }
    530    if (MainApplication.isDisplayingMapView()) {
    531      StreetsideMainDialog.getInstance().redButton.setEnabled(nearestImages.length >= 1);
    532      StreetsideMainDialog.getInstance().blueButton.setEnabled(nearestImages.length >= 2);
    533    }
    534    if (nearestImages.length >= 1) {
    535      CacheUtils.downloadPicture(nearestImages[0]);
    536      if (nearestImages.length >= 2) {
    537        CacheUtils.downloadPicture(nearestImages[1]);
    538      }
    539    }
    540  }
    541
    542  /**
    543   * Action used to delete images.
    544   *
    545   * @author nokutu
    546   */
    547  /*private class DeleteImageAction extends AbstractAction {
    548
    549    private static final long serialVersionUID = -982809854631863962L;
    550
    551    @Override
    552    public void actionPerformed(ActionEvent e) {
    553      if (instance != null)
    554        StreetsideRecord.getInstance().addCommand(
    555          new CommandDelete(getData().getMultiSelectedImages()));
    556    }
    557  }*/
    558
    559  private static class NearestImgToTargetComparator implements Comparator<StreetsideAbstractImage> {
    560    private final StreetsideAbstractImage target;
    561
    562    public NearestImgToTargetComparator(StreetsideAbstractImage target) {
    563      this.target = target;
    564    }
    565    /* (non-Javadoc)
    566     * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
    567     */
    568    @Override
    569    public int compare(StreetsideAbstractImage img1, StreetsideAbstractImage img2) {
    570      return (int) Math.signum(
    571        img1.getMovingLatLon().greatCircleDistance(target.getMovingLatLon()) -
    572        img2.getMovingLatLon().greatCircleDistance(target.getMovingLatLon())
    573      );
    574    }
    575  }
    576}