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