001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui; 003 004import java.awt.AlphaComposite; 005import java.awt.Color; 006import java.awt.Dimension; 007import java.awt.Graphics; 008import java.awt.Graphics2D; 009import java.awt.Point; 010import java.awt.Rectangle; 011import java.awt.Shape; 012import java.awt.event.ComponentAdapter; 013import java.awt.event.ComponentEvent; 014import java.awt.event.KeyEvent; 015import java.awt.event.MouseAdapter; 016import java.awt.event.MouseEvent; 017import java.awt.event.MouseMotionListener; 018import java.awt.geom.AffineTransform; 019import java.awt.geom.Area; 020import java.awt.image.BufferedImage; 021import java.beans.PropertyChangeEvent; 022import java.beans.PropertyChangeListener; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collections; 026import java.util.HashMap; 027import java.util.IdentityHashMap; 028import java.util.LinkedHashSet; 029import java.util.List; 030import java.util.Set; 031import java.util.TreeSet; 032import java.util.concurrent.CopyOnWriteArrayList; 033import java.util.concurrent.atomic.AtomicBoolean; 034 035import javax.swing.AbstractButton; 036import javax.swing.JComponent; 037import javax.swing.SwingUtilities; 038 039import org.openstreetmap.josm.Main; 040import org.openstreetmap.josm.actions.mapmode.MapMode; 041import org.openstreetmap.josm.data.Bounds; 042import org.openstreetmap.josm.data.ProjectionBounds; 043import org.openstreetmap.josm.data.ViewportData; 044import org.openstreetmap.josm.data.coor.EastNorth; 045import org.openstreetmap.josm.data.imagery.ImageryInfo; 046import org.openstreetmap.josm.data.osm.DataSelectionListener; 047import org.openstreetmap.josm.data.osm.event.SelectionEventManager; 048import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; 049import org.openstreetmap.josm.data.osm.visitor.paint.Rendering; 050import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; 051import org.openstreetmap.josm.gui.MapViewState.MapViewRectangle; 052import org.openstreetmap.josm.gui.autofilter.AutoFilterManager; 053import org.openstreetmap.josm.gui.datatransfer.OsmTransferHandler; 054import org.openstreetmap.josm.gui.layer.GpxLayer; 055import org.openstreetmap.josm.gui.layer.ImageryLayer; 056import org.openstreetmap.josm.gui.layer.Layer; 057import org.openstreetmap.josm.gui.layer.LayerManager; 058import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; 059import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; 060import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; 061import org.openstreetmap.josm.gui.layer.MainLayerManager; 062import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; 063import org.openstreetmap.josm.gui.layer.MapViewGraphics; 064import org.openstreetmap.josm.gui.layer.MapViewPaintable; 065import org.openstreetmap.josm.gui.layer.MapViewPaintable.LayerPainter; 066import org.openstreetmap.josm.gui.layer.MapViewPaintable.MapViewEvent; 067import org.openstreetmap.josm.gui.layer.MapViewPaintable.PaintableInvalidationEvent; 068import org.openstreetmap.josm.gui.layer.MapViewPaintable.PaintableInvalidationListener; 069import org.openstreetmap.josm.gui.layer.OsmDataLayer; 070import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer; 071import org.openstreetmap.josm.gui.layer.markerlayer.PlayHeadMarker; 072import org.openstreetmap.josm.gui.mappaint.MapPaintStyles; 073import org.openstreetmap.josm.gui.mappaint.MapPaintStyles.MapPaintSylesUpdateListener; 074import org.openstreetmap.josm.io.audio.AudioPlayer; 075import org.openstreetmap.josm.spi.preferences.Config; 076import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent; 077import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener; 078import org.openstreetmap.josm.tools.JosmRuntimeException; 079import org.openstreetmap.josm.tools.Logging; 080import org.openstreetmap.josm.tools.Shortcut; 081import org.openstreetmap.josm.tools.Utils; 082import org.openstreetmap.josm.tools.bugreport.BugReport; 083 084/** 085 * This is a component used in the {@link MapFrame} for browsing the map. It use is to 086 * provide the MapMode's enough capabilities to operate.<br><br> 087 * 088 * {@code MapView} holds meta-data about the data set currently displayed, as scale level, 089 * center point viewed, what scrolling mode or editing mode is selected or with 090 * what projection the map is viewed etc..<br><br> 091 * 092 * {@code MapView} is able to administrate several layers. 093 * 094 * @author imi 095 */ 096public class MapView extends NavigatableComponent 097implements PropertyChangeListener, PreferenceChangedListener, 098LayerManager.LayerChangeListener, MainLayerManager.ActiveLayerChangeListener { 099 100 static { 101 MapPaintStyles.addMapPaintSylesUpdateListener(new MapPaintSylesUpdateListener() { 102 @Override 103 public void mapPaintStylesUpdated() { 104 SwingUtilities.invokeLater(() -> { 105 // Trigger a repaint of all data layers 106 MainApplication.getLayerManager().getLayers() 107 .stream() 108 .filter(layer -> layer instanceof OsmDataLayer) 109 .forEach(Layer::invalidate); 110 }); 111 } 112 113 @Override 114 public void mapPaintStyleEntryUpdated(int index) { 115 mapPaintStylesUpdated(); 116 } 117 }); 118 } 119 120 /** 121 * An invalidation listener that simply calls repaint() for now. 122 * @author Michael Zangl 123 * @since 10271 124 */ 125 private class LayerInvalidatedListener implements PaintableInvalidationListener { 126 private boolean ignoreRepaint; 127 128 private final Set<MapViewPaintable> invalidatedLayers = Collections.newSetFromMap(new IdentityHashMap<MapViewPaintable, Boolean>()); 129 130 @Override 131 public void paintableInvalidated(PaintableInvalidationEvent event) { 132 invalidate(event.getLayer()); 133 } 134 135 /** 136 * Invalidate contents and repaint map view 137 * @param mapViewPaintable invalidated layer 138 */ 139 public synchronized void invalidate(MapViewPaintable mapViewPaintable) { 140 ignoreRepaint = true; 141 invalidatedLayers.add(mapViewPaintable); 142 repaint(); 143 } 144 145 /** 146 * Temporary until all {@link MapViewPaintable}s support this. 147 * @param p The paintable. 148 */ 149 public synchronized void addTo(MapViewPaintable p) { 150 p.addInvalidationListener(this); 151 } 152 153 /** 154 * Temporary until all {@link MapViewPaintable}s support this. 155 * @param p The paintable. 156 */ 157 public synchronized void removeFrom(MapViewPaintable p) { 158 p.removeInvalidationListener(this); 159 invalidatedLayers.remove(p); 160 } 161 162 /** 163 * Attempts to trace repaints that did not originate from this listener. Good to find missed {@link MapView#repaint()}s in code. 164 */ 165 protected synchronized void traceRandomRepaint() { 166 if (!ignoreRepaint) { 167 System.err.println("Repaint:"); 168 Thread.dumpStack(); 169 } 170 ignoreRepaint = false; 171 } 172 173 /** 174 * Retrieves a set of all layers that have been marked as invalid since the last call to this method. 175 * @return The layers 176 */ 177 protected synchronized Set<MapViewPaintable> collectInvalidatedLayers() { 178 Set<MapViewPaintable> layers = Collections.newSetFromMap(new IdentityHashMap<MapViewPaintable, Boolean>()); 179 layers.addAll(invalidatedLayers); 180 invalidatedLayers.clear(); 181 return layers; 182 } 183 } 184 185 /** 186 * A layer painter that issues a warning when being called. 187 * @author Michael Zangl 188 * @since 10474 189 */ 190 private static class WarningLayerPainter implements LayerPainter { 191 boolean warningPrinted; 192 private final Layer layer; 193 194 WarningLayerPainter(Layer layer) { 195 this.layer = layer; 196 } 197 198 @Override 199 public void paint(MapViewGraphics graphics) { 200 if (!warningPrinted) { 201 Logging.debug("A layer triggered a repaint while being added: " + layer); 202 warningPrinted = true; 203 } 204 } 205 206 @Override 207 public void detachFromMapView(MapViewEvent event) { 208 // ignored 209 } 210 } 211 212 /** 213 * A list of all layers currently loaded. If we support multiple map views, this list may be different for each of them. 214 */ 215 private final MainLayerManager layerManager; 216 217 /** 218 * The play head marker: there is only one of these so it isn't in any specific layer 219 */ 220 public transient PlayHeadMarker playHeadMarker; 221 222 /** 223 * The last event performed by mouse. 224 */ 225 public MouseEvent lastMEvent = new MouseEvent(this, 0, 0, 0, 0, 0, 0, false); // In case somebody reads it before first mouse move 226 227 /** 228 * Temporary layers (selection rectangle, etc.) that are never cached and 229 * drawn on top of regular layers. 230 * Access must be synchronized. 231 */ 232 private final transient Set<MapViewPaintable> temporaryLayers = new LinkedHashSet<>(); 233 234 private transient BufferedImage nonChangedLayersBuffer; 235 private transient BufferedImage offscreenBuffer; 236 // Layers that wasn't changed since last paint 237 private final transient List<Layer> nonChangedLayers = new ArrayList<>(); 238 private int lastViewID; 239 private final AtomicBoolean paintPreferencesChanged = new AtomicBoolean(true); 240 private Rectangle lastClipBounds = new Rectangle(); 241 private transient MapMover mapMover; 242 243 /** 244 * The listener that listens to invalidations of all layers. 245 */ 246 private final LayerInvalidatedListener invalidatedListener = new LayerInvalidatedListener(); 247 248 /** 249 * This is a map of all Layers that have been added to this view. 250 */ 251 private final HashMap<Layer, LayerPainter> registeredLayers = new HashMap<>(); 252 253 /** 254 * Constructs a new {@code MapView}. 255 * @param layerManager The layers to display. 256 * @param viewportData the initial viewport of the map. Can be null, then 257 * the viewport is derived from the layer data. 258 * @since 11713 259 */ 260 public MapView(MainLayerManager layerManager, final ViewportData viewportData) { 261 this.layerManager = layerManager; 262 initialViewport = viewportData; 263 layerManager.addAndFireLayerChangeListener(this); 264 layerManager.addActiveLayerChangeListener(this); 265 Config.getPref().addPreferenceChangeListener(this); 266 267 addComponentListener(new ComponentAdapter() { 268 @Override 269 public void componentResized(ComponentEvent e) { 270 removeComponentListener(this); 271 mapMover = new MapMover(MapView.this); 272 } 273 }); 274 275 // listens to selection changes to redraw the map 276 SelectionEventManager.getInstance().addSelectionListenerForEdt(repaintSelectionChangedListener); 277 278 //store the last mouse action 279 this.addMouseMotionListener(new MouseMotionListener() { 280 @Override 281 public void mouseDragged(MouseEvent e) { 282 mouseMoved(e); 283 } 284 285 @Override 286 public void mouseMoved(MouseEvent e) { 287 lastMEvent = e; 288 } 289 }); 290 this.addMouseListener(new MouseAdapter() { 291 @Override 292 public void mousePressed(MouseEvent me) { 293 // focus the MapView component when mouse is pressed inside it 294 requestFocus(); 295 } 296 }); 297 298 setFocusTraversalKeysEnabled(!Shortcut.findShortcut(KeyEvent.VK_TAB, 0).isPresent()); 299 300 for (JComponent c : getMapNavigationComponents(this)) { 301 add(c); 302 } 303 if (AutoFilterManager.PROP_AUTO_FILTER_ENABLED.get()) { 304 AutoFilterManager.getInstance().enableAutoFilterRule(AutoFilterManager.PROP_AUTO_FILTER_RULE.get()); 305 } 306 setTransferHandler(new OsmTransferHandler()); 307 } 308 309 /** 310 * Adds the map navigation components to a 311 * @param forMapView The map view to get the components for. 312 * @return A list containing the correctly positioned map navigation components. 313 */ 314 public static List<? extends JComponent> getMapNavigationComponents(MapView forMapView) { 315 MapSlider zoomSlider = new MapSlider(forMapView); 316 Dimension size = zoomSlider.getPreferredSize(); 317 zoomSlider.setSize(size); 318 zoomSlider.setLocation(3, 0); 319 zoomSlider.setFocusTraversalKeysEnabled(!Shortcut.findShortcut(KeyEvent.VK_TAB, 0).isPresent()); 320 321 MapScaler scaler = new MapScaler(forMapView); 322 scaler.setPreferredLineLength(size.width - 10); 323 scaler.setSize(scaler.getPreferredSize()); 324 scaler.setLocation(3, size.height); 325 326 return Arrays.asList(zoomSlider, scaler); 327 } 328 329 // remebered geometry of the component 330 private Dimension oldSize; 331 private Point oldLoc; 332 333 /** 334 * Call this method to keep map position on screen during next repaint 335 */ 336 public void rememberLastPositionOnScreen() { 337 oldSize = getSize(); 338 oldLoc = getLocationOnScreen(); 339 } 340 341 @Override 342 public void layerAdded(LayerAddEvent e) { 343 try { 344 Layer layer = e.getAddedLayer(); 345 registeredLayers.put(layer, new WarningLayerPainter(layer)); 346 // Layers may trigger a redraw during this call if they open dialogs. 347 LayerPainter painter = layer.attachToMapView(new MapViewEvent(this, false)); 348 if (!registeredLayers.containsKey(layer)) { 349 // The layer may have removed itself during attachToMapView() 350 Logging.warn("Layer was removed during attachToMapView()"); 351 } else { 352 registeredLayers.put(layer, painter); 353 354 if (e.isZoomRequired()) { 355 ProjectionBounds viewProjectionBounds = layer.getViewProjectionBounds(); 356 if (viewProjectionBounds != null) { 357 scheduleZoomTo(new ViewportData(viewProjectionBounds)); 358 } 359 } 360 361 layer.addPropertyChangeListener(this); 362 Main.addProjectionChangeListener(layer); 363 invalidatedListener.addTo(layer); 364 AudioPlayer.reset(); 365 366 repaint(); 367 } 368 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) { 369 throw BugReport.intercept(t).put("layer", e.getAddedLayer()); 370 } 371 } 372 373 /** 374 * Replies true if the active data layer (edit layer) is drawable. 375 * 376 * @return true if the active data layer (edit layer) is drawable, false otherwise 377 */ 378 public boolean isActiveLayerDrawable() { 379 return layerManager.getEditLayer() != null; 380 } 381 382 /** 383 * Replies true if the active data layer is visible. 384 * 385 * @return true if the active data layer is visible, false otherwise 386 */ 387 public boolean isActiveLayerVisible() { 388 OsmDataLayer e = layerManager.getActiveDataLayer(); 389 return e != null && e.isVisible(); 390 } 391 392 @Override 393 public void layerRemoving(LayerRemoveEvent e) { 394 Layer layer = e.getRemovedLayer(); 395 396 LayerPainter painter = registeredLayers.remove(layer); 397 if (painter == null) { 398 Logging.error("The painter for layer " + layer + " was not registered."); 399 return; 400 } 401 painter.detachFromMapView(new MapViewEvent(this, false)); 402 Main.removeProjectionChangeListener(layer); 403 layer.removePropertyChangeListener(this); 404 invalidatedListener.removeFrom(layer); 405 layer.destroy(); 406 AudioPlayer.reset(); 407 408 repaint(); 409 } 410 411 private boolean virtualNodesEnabled; 412 413 /** 414 * Enables or disables drawing of the virtual nodes. 415 * @param enabled if virtual nodes are enabled 416 */ 417 public void setVirtualNodesEnabled(boolean enabled) { 418 if (virtualNodesEnabled != enabled) { 419 virtualNodesEnabled = enabled; 420 repaint(); 421 } 422 } 423 424 /** 425 * Checks if virtual nodes should be drawn. Default is <code>false</code> 426 * @return The virtual nodes property. 427 * @see Rendering#render 428 */ 429 public boolean isVirtualNodesEnabled() { 430 return virtualNodesEnabled; 431 } 432 433 /** 434 * Moves the layer to the given new position. No event is fired, but repaints 435 * according to the new Z-Order of the layers. 436 * 437 * @param layer The layer to move 438 * @param pos The new position of the layer 439 */ 440 public void moveLayer(Layer layer, int pos) { 441 layerManager.moveLayer(layer, pos); 442 } 443 444 @Override 445 public void layerOrderChanged(LayerOrderChangeEvent e) { 446 AudioPlayer.reset(); 447 repaint(); 448 } 449 450 /** 451 * Paints the given layer to the graphics object, using the current state of this map view. 452 * @param layer The layer to draw. 453 * @param g A graphics object. It should have the width and height of this component 454 * @throws IllegalArgumentException If the layer is not part of this map view. 455 * @since 11226 456 */ 457 public void paintLayer(Layer layer, Graphics2D g) { 458 try { 459 LayerPainter painter = registeredLayers.get(layer); 460 if (painter == null) { 461 Logging.warn("Cannot paint layer, it is not registered: {0}", layer); 462 return; 463 } 464 MapViewRectangle clipBounds = getState().getViewArea(g.getClipBounds()); 465 MapViewGraphics paintGraphics = new MapViewGraphics(this, g, clipBounds); 466 467 if (layer.getOpacity() < 1) { 468 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) layer.getOpacity())); 469 } 470 painter.paint(paintGraphics); 471 g.setPaintMode(); 472 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) { 473 BugReport.intercept(t).put("layer", layer).warn(); 474 } 475 } 476 477 /** 478 * Draw the component. 479 */ 480 @Override 481 public void paint(Graphics g) { 482 try { 483 if (!prepareToDraw()) { 484 return; 485 } 486 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) { 487 BugReport.intercept(e).put("center", this::getCenter).warn(); 488 return; 489 } 490 491 try { 492 drawMapContent((Graphics2D) g); 493 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) { 494 throw BugReport.intercept(e).put("visibleLayers", layerManager::getVisibleLayersInZOrder) 495 .put("temporaryLayers", temporaryLayers); 496 } 497 super.paint(g); 498 } 499 500 private void drawMapContent(Graphics2D g) { 501 // In HiDPI-mode, the Graphics g will have a transform that scales 502 // everything by a factor of 2.0 or so. At the same time, the value returned 503 // by getWidth()/getHeight will be reduced by that factor. 504 // 505 // This would work as intended, if we were to draw directly on g. But 506 // with a temporary buffer image, we need to move the scale transform to 507 // the Graphics of the buffer image and (in the end) transfer the content 508 // of the temporary buffer pixel by pixel onto g, without scaling. 509 // (Otherwise, we would upscale a small buffer image and the result would be 510 // blurry, with 2x2 pixel blocks.) 511 AffineTransform trOrig = g.getTransform(); 512 double uiScaleX = g.getTransform().getScaleX(); 513 double uiScaleY = g.getTransform().getScaleY(); 514 // width/height in full-resolution screen pixels 515 int width = (int) Math.round(getWidth() * uiScaleX); 516 int height = (int) Math.round(getHeight() * uiScaleY); 517 // This transformation corresponds to the original transformation of g, 518 // except for the translation part. It will be applied to the temporary 519 // buffer images. 520 AffineTransform trDef = AffineTransform.getScaleInstance(uiScaleX, uiScaleY); 521 // The goal is to create the temporary image at full pixel resolution, 522 // so scale up the clip shape 523 Shape scaledClip = trDef.createTransformedShape(g.getClip()); 524 525 List<Layer> visibleLayers = layerManager.getVisibleLayersInZOrder(); 526 527 int nonChangedLayersCount = 0; 528 Set<MapViewPaintable> invalidated = invalidatedListener.collectInvalidatedLayers(); 529 for (Layer l: visibleLayers) { 530 if (invalidated.contains(l)) { 531 break; 532 } else { 533 nonChangedLayersCount++; 534 } 535 } 536 537 boolean canUseBuffer = !paintPreferencesChanged.getAndSet(false) 538 && nonChangedLayers.size() <= nonChangedLayersCount 539 && lastViewID == getViewID() 540 && lastClipBounds.contains(g.getClipBounds()) 541 && nonChangedLayers.equals(visibleLayers.subList(0, nonChangedLayers.size())); 542 543 if (null == offscreenBuffer || offscreenBuffer.getWidth() != width || offscreenBuffer.getHeight() != height) { 544 offscreenBuffer = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); 545 } 546 547 if (!canUseBuffer || nonChangedLayersBuffer == null) { 548 if (null == nonChangedLayersBuffer 549 || nonChangedLayersBuffer.getWidth() != width || nonChangedLayersBuffer.getHeight() != height) { 550 nonChangedLayersBuffer = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); 551 } 552 Graphics2D g2 = nonChangedLayersBuffer.createGraphics(); 553 g2.setClip(scaledClip); 554 g2.setTransform(trDef); 555 g2.setColor(PaintColors.getBackgroundColor()); 556 g2.fillRect(0, 0, width, height); 557 558 for (int i = 0; i < nonChangedLayersCount; i++) { 559 paintLayer(visibleLayers.get(i), g2); 560 } 561 } else { 562 // Maybe there were more unchanged layers then last time - draw them to buffer 563 if (nonChangedLayers.size() != nonChangedLayersCount) { 564 Graphics2D g2 = nonChangedLayersBuffer.createGraphics(); 565 g2.setClip(scaledClip); 566 g2.setTransform(trDef); 567 for (int i = nonChangedLayers.size(); i < nonChangedLayersCount; i++) { 568 paintLayer(visibleLayers.get(i), g2); 569 } 570 } 571 } 572 573 nonChangedLayers.clear(); 574 nonChangedLayers.addAll(visibleLayers.subList(0, nonChangedLayersCount)); 575 lastViewID = getViewID(); 576 lastClipBounds = g.getClipBounds(); 577 578 Graphics2D tempG = offscreenBuffer.createGraphics(); 579 tempG.setClip(scaledClip); 580 tempG.setTransform(new AffineTransform()); 581 tempG.drawImage(nonChangedLayersBuffer, 0, 0, null); 582 tempG.setTransform(trDef); 583 584 for (int i = nonChangedLayersCount; i < visibleLayers.size(); i++) { 585 paintLayer(visibleLayers.get(i), tempG); 586 } 587 588 try { 589 drawTemporaryLayers(tempG, getLatLonBounds(new Rectangle( 590 (int) Math.round(g.getClipBounds().x * uiScaleX), 591 (int) Math.round(g.getClipBounds().y * uiScaleY)))); 592 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) { 593 BugReport.intercept(e).put("temporaryLayers", temporaryLayers).warn(); 594 } 595 596 // draw world borders 597 try { 598 drawWorldBorders(tempG); 599 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) { 600 // getProjection() needs to be inside lambda to catch errors. 601 BugReport.intercept(e).put("bounds", () -> getProjection().getWorldBoundsLatLon()).warn(); 602 } 603 604 MapFrame map = MainApplication.getMap(); 605 if (AutoFilterManager.getInstance().getCurrentAutoFilter() != null) { 606 AutoFilterManager.getInstance().drawOSDText(tempG); 607 } else if (MainApplication.isDisplayingMapView() && map.filterDialog != null) { 608 map.filterDialog.drawOSDText(tempG); 609 } 610 611 if (playHeadMarker != null) { 612 playHeadMarker.paint(tempG, this); 613 } 614 615 try { 616 g.setTransform(new AffineTransform(1, 0, 0, 1, trOrig.getTranslateX(), trOrig.getTranslateY())); 617 g.drawImage(offscreenBuffer, 0, 0, null); 618 } catch (ClassCastException e) { 619 // See #11002 and duplicate tickets. On Linux with Java >= 8 Many users face this error here: 620 // 621 // java.lang.ClassCastException: sun.awt.image.BufImgSurfaceData cannot be cast to sun.java2d.xr.XRSurfaceData 622 // at sun.java2d.xr.XRPMBlitLoops.cacheToTmpSurface(XRPMBlitLoops.java:145) 623 // at sun.java2d.xr.XrSwToPMBlit.Blit(XRPMBlitLoops.java:353) 624 // at sun.java2d.pipe.DrawImage.blitSurfaceData(DrawImage.java:959) 625 // at sun.java2d.pipe.DrawImage.renderImageCopy(DrawImage.java:577) 626 // at sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:67) 627 // at sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:1014) 628 // at sun.java2d.pipe.ValidatePipe.copyImage(ValidatePipe.java:186) 629 // at sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3318) 630 // at sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3296) 631 // at org.openstreetmap.josm.gui.MapView.paint(MapView.java:834) 632 // 633 // It seems to be this JDK bug, but Oracle does not seem to be fixing it: 634 // https://bugs.openjdk.java.net/browse/JDK-7172749 635 // 636 // According to bug reports it can happen for a variety of reasons such as: 637 // - long period of time 638 // - change of screen resolution 639 // - addition/removal of a secondary monitor 640 // 641 // But the application seems to work fine after, so let's just log the error 642 Logging.error(e); 643 } finally { 644 g.setTransform(trOrig); 645 } 646 } 647 648 private void drawTemporaryLayers(Graphics2D tempG, Bounds box) { 649 synchronized (temporaryLayers) { 650 for (MapViewPaintable mvp : temporaryLayers) { 651 try { 652 mvp.paint(tempG, this, box); 653 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) { 654 throw BugReport.intercept(e).put("mvp", mvp); 655 } 656 } 657 } 658 } 659 660 private void drawWorldBorders(Graphics2D tempG) { 661 tempG.setColor(Color.WHITE); 662 Bounds b = getProjection().getWorldBoundsLatLon(); 663 664 int w = getWidth(); 665 int h = getHeight(); 666 667 // Work around OpenJDK having problems when drawing out of bounds 668 final Area border = getState().getArea(b); 669 // Make the viewport 1px larger in every direction to prevent an 670 // additional 1px border when zooming in 671 final Area viewport = new Area(new Rectangle(-1, -1, w + 2, h + 2)); 672 border.intersect(viewport); 673 tempG.draw(border); 674 } 675 676 /** 677 * Sets up the viewport to prepare for drawing the view. 678 * @return <code>true</code> if the view can be drawn, <code>false</code> otherwise. 679 */ 680 public boolean prepareToDraw() { 681 updateLocationState(); 682 if (initialViewport != null) { 683 zoomTo(initialViewport); 684 initialViewport = null; 685 } 686 687 if (getCenter() == null) 688 return false; // no data loaded yet. 689 690 // if the position was remembered, we need to adjust center once before repainting 691 if (oldLoc != null && oldSize != null) { 692 Point l1 = getLocationOnScreen(); 693 final EastNorth newCenter = new EastNorth( 694 getCenter().getX()+ (l1.x-oldLoc.x - (oldSize.width-getWidth())/2.0)*getScale(), 695 getCenter().getY()+ (oldLoc.y-l1.y + (oldSize.height-getHeight())/2.0)*getScale() 696 ); 697 oldLoc = null; oldSize = null; 698 zoomTo(newCenter); 699 } 700 701 return true; 702 } 703 704 @Override 705 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 706 MapFrame map = MainApplication.getMap(); 707 if (map != null) { 708 /* This only makes the buttons look disabled. Disabling the actions as well requires 709 * the user to re-select the tool after i.e. moving a layer. While testing I found 710 * that I switch layers and actions at the same time and it was annoying to mind the 711 * order. This way it works as visual clue for new users */ 712 // FIXME: This does not belong here. 713 for (final AbstractButton b: map.allMapModeButtons) { 714 MapMode mode = (MapMode) b.getAction(); 715 final boolean activeLayerSupported = mode.layerIsSupported(layerManager.getActiveLayer()); 716 if (activeLayerSupported) { 717 MainApplication.registerActionShortcut(mode, mode.getShortcut()); //fix #6876 718 } else { 719 MainApplication.unregisterShortcut(mode.getShortcut()); 720 } 721 b.setEnabled(activeLayerSupported); 722 } 723 } 724 // invalidate repaint cache. The layer order may have changed by this, so we invalidate every layer 725 getLayerManager().getLayers().forEach(invalidatedListener::invalidate); 726 AudioPlayer.reset(); 727 } 728 729 /** 730 * Adds a new temporary layer. 731 * <p> 732 * A temporary layer is a layer that is painted above all normal layers. Layers are painted in the order they are added. 733 * 734 * @param mvp The layer to paint. 735 * @return <code>true</code> if the layer was added. 736 */ 737 public boolean addTemporaryLayer(MapViewPaintable mvp) { 738 synchronized (temporaryLayers) { 739 boolean added = temporaryLayers.add(mvp); 740 if (added) { 741 invalidatedListener.addTo(mvp); 742 } 743 repaint(); 744 return added; 745 } 746 } 747 748 /** 749 * Removes a layer previously added as temporary layer. 750 * @param mvp The layer to remove. 751 * @return <code>true</code> if that layer was removed. 752 */ 753 public boolean removeTemporaryLayer(MapViewPaintable mvp) { 754 synchronized (temporaryLayers) { 755 boolean removed = temporaryLayers.remove(mvp); 756 if (removed) { 757 invalidatedListener.removeFrom(mvp); 758 } 759 repaint(); 760 return removed; 761 } 762 } 763 764 /** 765 * Gets a list of temporary layers. 766 * @return The layers in the order they are added. 767 */ 768 public List<MapViewPaintable> getTemporaryLayers() { 769 synchronized (temporaryLayers) { 770 return Collections.unmodifiableList(new ArrayList<>(temporaryLayers)); 771 } 772 } 773 774 @Override 775 public void propertyChange(PropertyChangeEvent evt) { 776 if (evt.getPropertyName().equals(Layer.VISIBLE_PROP)) { 777 repaint(); 778 } else if (evt.getPropertyName().equals(Layer.OPACITY_PROP) || 779 evt.getPropertyName().equals(Layer.FILTER_STATE_PROP)) { 780 Layer l = (Layer) evt.getSource(); 781 if (l.isVisible()) { 782 invalidatedListener.invalidate(l); 783 } 784 } 785 } 786 787 @Override 788 public void preferenceChanged(PreferenceChangeEvent e) { 789 paintPreferencesChanged.set(true); 790 } 791 792 private final transient DataSelectionListener repaintSelectionChangedListener = event -> repaint(); 793 794 /** 795 * Destroy this map view panel. Should be called once when it is not needed any more. 796 */ 797 public void destroy() { 798 layerManager.removeAndFireLayerChangeListener(this); 799 layerManager.removeActiveLayerChangeListener(this); 800 Config.getPref().removePreferenceChangeListener(this); 801 SelectionEventManager.getInstance().removeSelectionListener(repaintSelectionChangedListener); 802 MultipolygonCache.getInstance().clear(); 803 if (mapMover != null) { 804 mapMover.destroy(); 805 } 806 nonChangedLayers.clear(); 807 synchronized (temporaryLayers) { 808 temporaryLayers.clear(); 809 } 810 nonChangedLayersBuffer = null; 811 offscreenBuffer = null; 812 } 813 814 /** 815 * Get a string representation of all layers suitable for the {@code source} changeset tag. 816 * @return A String of sources separated by ';' 817 */ 818 public String getLayerInformationForSourceTag() { 819 final Set<String> layerInfo = new TreeSet<>(); 820 if (!layerManager.getLayersOfType(GpxLayer.class).isEmpty()) { 821 // no i18n for international values 822 layerInfo.add("survey"); 823 } 824 for (final GeoImageLayer i : layerManager.getLayersOfType(GeoImageLayer.class)) { 825 if (i.isVisible()) { 826 layerInfo.add(i.getName()); 827 } 828 } 829 for (final ImageryLayer i : layerManager.getLayersOfType(ImageryLayer.class)) { 830 if (i.isVisible()) { 831 layerInfo.add(ImageryInfo.ImageryType.BING.equals(i.getInfo().getImageryType()) ? "Bing" : i.getName()); 832 } 833 } 834 return Utils.join("; ", layerInfo); 835 } 836 837 /** 838 * This is a listener that gets informed whenever repaint is called for this MapView. 839 * <p> 840 * This is the only safe method to find changes to the map view, since many components call MapView.repaint() directly. 841 * @author Michael Zangl 842 * @since 10600 (functional interface) 843 */ 844 @FunctionalInterface 845 public interface RepaintListener { 846 /** 847 * Called when any repaint method is called (using default arguments if required). 848 * @param tm see {@link JComponent#repaint(long, int, int, int, int)} 849 * @param x see {@link JComponent#repaint(long, int, int, int, int)} 850 * @param y see {@link JComponent#repaint(long, int, int, int, int)} 851 * @param width see {@link JComponent#repaint(long, int, int, int, int)} 852 * @param height see {@link JComponent#repaint(long, int, int, int, int)} 853 */ 854 void repaint(long tm, int x, int y, int width, int height); 855 } 856 857 private final transient CopyOnWriteArrayList<RepaintListener> repaintListeners = new CopyOnWriteArrayList<>(); 858 859 /** 860 * Adds a listener that gets informed whenever repaint() is called for this class. 861 * @param l The listener. 862 */ 863 public void addRepaintListener(RepaintListener l) { 864 repaintListeners.add(l); 865 } 866 867 /** 868 * Removes a registered repaint listener. 869 * @param l The listener. 870 */ 871 public void removeRepaintListener(RepaintListener l) { 872 repaintListeners.remove(l); 873 } 874 875 @Override 876 public void repaint(long tm, int x, int y, int width, int height) { 877 // This is the main repaint method, all other methods are convenience methods and simply call this method. 878 // This is just an observation, not a must, but seems to be true for all implementations I found so far. 879 if (repaintListeners != null) { 880 // Might get called early in super constructor 881 for (RepaintListener l : repaintListeners) { 882 l.repaint(tm, x, y, width, height); 883 } 884 } 885 super.repaint(tm, x, y, width, height); 886 } 887 888 @Override 889 public void repaint() { 890 if (Logging.isTraceEnabled()) { 891 invalidatedListener.traceRandomRepaint(); 892 } 893 super.repaint(); 894 } 895 896 /** 897 * Returns the layer manager. 898 * @return the layer manager 899 * @since 10282 900 */ 901 public final MainLayerManager getLayerManager() { 902 return layerManager; 903 } 904 905 /** 906 * Schedule a zoom to the given position on the next redraw. 907 * Temporary, may be removed without warning. 908 * @param viewportData the viewport to zoom to 909 * @since 10394 910 */ 911 public void scheduleZoomTo(ViewportData viewportData) { 912 initialViewport = viewportData; 913 } 914 915 /** 916 * Returns the internal {@link MapMover}. 917 * @return the internal {@code MapMover} 918 * @since 13126 919 */ 920 public final MapMover getMapMover() { 921 return mapMover; 922 } 923}