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