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}