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