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

Last change on this file since 12939 was 12881, checked in by bastiK, 7 years ago

see #15229 - move remaining classes to spi.preferences package, to make it self-contained

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