source: josm/trunk/src/org/openstreetmap/josm/gui/MapView.java@ 17445

Last change on this file since 17445 was 17445, checked in by GerdP, 3 years ago

see #20272: Confusing handling of native scale layer and "zoom to download"

  • if imageary layer is removed which was enabled for "scale follows native resolution ..." set the native scale layer to null.

An alternative might be to check if another layer could be enabled, but that would requure more logic and a history.

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