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}