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}