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}