Ticket #24842: pan-reuse.patch

File pan-reuse.patch, 24.5 KB (added by mistenman, 4 weeks ago)
  • src/org/openstreetmap/josm/actions/mapmode/SelectAction.java

    diff --git a/src/org/openstreetmap/josm/actions/mapmode/SelectAction.java b/src/org/openstreetmap/josm/actions/mapmode/SelectAction.java
    index f9ee08d..8ac948f 100644
    a b import static org.openstreetmap.josm.tools.I18n.tr;  
    66import static org.openstreetmap.josm.tools.I18n.trc;
    77import static org.openstreetmap.josm.tools.I18n.trn;
    88
     9import java.awt.BasicStroke;
     10import java.awt.Color;
    911import java.awt.Cursor;
     12import java.awt.Graphics2D;
    1013import java.awt.Point;
    1114import java.awt.Rectangle;
    1215import java.awt.event.InputEvent;
    1316import java.awt.event.KeyEvent;
    1417import java.awt.event.MouseEvent;
     18import java.awt.geom.Path2D;
    1519import java.awt.geom.Point2D;
    1620import java.util.Collection;
    1721import java.util.Collections;
    import org.openstreetmap.josm.command.MoveCommand;  
    3337import org.openstreetmap.josm.command.RotateCommand;
    3438import org.openstreetmap.josm.command.ScaleCommand;
    3539import org.openstreetmap.josm.command.SequenceCommand;
     40import org.openstreetmap.josm.data.Bounds;
    3641import org.openstreetmap.josm.data.SystemOfMeasurement;
    3742import org.openstreetmap.josm.data.UndoRedoHandler;
    3843import org.openstreetmap.josm.data.coor.EastNorth;
    import org.openstreetmap.josm.data.osm.Way;  
    4449import org.openstreetmap.josm.data.osm.WaySegment;
    4550import org.openstreetmap.josm.data.osm.visitor.AllNodesVisitor;
    4651import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer;
     52import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
    4753import org.openstreetmap.josm.data.preferences.BooleanProperty;
    4854import org.openstreetmap.josm.data.preferences.CachingProperty;
    4955import org.openstreetmap.josm.gui.ExtendedDialog;
    import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;  
    5460import org.openstreetmap.josm.gui.SelectionManager;
    5561import org.openstreetmap.josm.gui.SelectionManager.SelectionEnded;
    5662import org.openstreetmap.josm.gui.layer.Layer;
     63import org.openstreetmap.josm.gui.layer.MapViewPaintable;
    5764import org.openstreetmap.josm.gui.layer.OsmDataLayer;
    5865import org.openstreetmap.josm.gui.util.GuiHelper;
    5966import org.openstreetmap.josm.gui.util.KeyPressReleaseListener;
    import org.openstreetmap.josm.tools.Utils;  
    7784 * On Mac OS X, Ctrl + mouse button 1 simulates right click (map move), so the
    7885 * feature "selection remove" is disabled on this platform.
    7986 */
    80 public class SelectAction extends MapMode implements ModifierExListener, KeyPressReleaseListener, SelectionEnded {
     87public class SelectAction extends MapMode implements ModifierExListener, KeyPressReleaseListener, SelectionEnded, MapViewPaintable {
    8188
    8289    private static final String NORMAL = /* ICON(cursor/)*/ "normal";
    8390
    public class SelectAction extends MapMode implements ModifierExListener, KeyPres  
    197204     * set would have to be checked.
    198205     */
    199206    private transient OsmPrimitive currentHighlight;
     207    // Whether currentHighlight was set via OsmPrimitive.setHighlighted (merge-mode drag).
     208    // Only then does it need to be unset again when the highlight changes.
     209    private boolean dataLayerHighlight;
    200210
    201211    /**
    202212     * Create a new SelectAction
    public class SelectAction extends MapMode implements ModifierExListener, KeyPres  
    218228        mv.addMouseMotionListener(this);
    219229        mv.setVirtualNodesEnabled(Config.getPref().getInt("mappaint.node.virtual-size", 8) != 0);
    220230        drawTargetHighlight = Config.getPref().getBoolean("draw.target-highlight", true);
     231        mv.addTemporaryLayer(this); // draws the hover highlight without invalidating the data layer
    221232        initialMoveDelay = Config.getPref().getInt("edit.initial-move-delay", 200);
    222233        initialMoveThreshold = Config.getPref().getInt("edit.initial-move-threshold", 5);
    223234        repeatedKeySwitchLassoOption = Config.getPref().getBoolean("mappaint.select.toggle-lasso-on-repeated-S", true);
    public class SelectAction extends MapMode implements ModifierExListener, KeyPres  
    242253        map.keyDetector.removeModifierExListener(this);
    243254        map.keyDetector.removeKeyListener(this);
    244255        removeHighlighting();
     256        mv.removeTemporaryLayer(this);
    245257        virtualManager.clear();
    246258    }
    247259
    public class SelectAction extends MapMode implements ModifierExListener, KeyPres  
    271283     * @return {@code true} if repaint is required
    272284     */
    273285    private boolean giveUserFeedback(MouseEvent e, int modifiers) {
     286        if (mv.isPanning()) {
     287            // Don't hover-highlight while panning: a highlight change invalidates the
     288            // data layer and forces a full re-render, breaking the pan-reuse path.
     289            return false;
     290        }
    274291        Optional<OsmPrimitive> c = Optional.ofNullable(
    275292                mv.getNearestNodeOrWay(e.getPoint(), mv.isSelectablePredicate, true));
    276293
    public class SelectAction extends MapMode implements ModifierExListener, KeyPres  
    380397        if (currentHighlight == null) {
    381398            return needsRepaint;
    382399        }
    383         currentHighlight.setHighlighted(false);
     400        if (dataLayerHighlight) {
     401            currentHighlight.setHighlighted(false);
     402            dataLayerHighlight = false;
     403        }
    384404        currentHighlight = null;
    385405        return true;
    386406    }
    public class SelectAction extends MapMode implements ModifierExListener, KeyPres  
    388408    private boolean repaintIfRequired(OsmPrimitive newHighlight) {
    389409        if (!drawTargetHighlight || Objects.equals(currentHighlight, newHighlight))
    390410            return false;
    391         if (currentHighlight != null) {
     411        // The highlighted primitive is drawn by this action's temporary layer (see paint),
     412        // not via OsmPrimitive.setHighlighted, which would invalidate the data layer.
     413        if (dataLayerHighlight) {
    392414            currentHighlight.setHighlighted(false);
    393         }
    394         if (newHighlight != null) {
    395             newHighlight.setHighlighted(true);
     415            dataLayerHighlight = false;
    396416        }
    397417        currentHighlight = newHighlight;
    398418        return true;
    399419    }
    400420
     421    /**
     422     * Draws the currently highlighted (hovered) primitive as an overlay on top of the map.
     423     * This replaces the old highlight via {@link OsmPrimitive#setHighlighted}, which
     424     * invalidated the data layer on every feature crossing and forced a full re-render.
     425     */
     426    @Override
     427    public void paint(Graphics2D g, MapView mv, Bounds bbox) {
     428        OsmPrimitive p = currentHighlight;
     429        if (p == null || !drawTargetHighlight) {
     430            return;
     431        }
     432        Color highlight = PaintColors.HIGHLIGHT.get();
     433        Color transparent = new Color(highlight.getRed(), highlight.getGreen(), highlight.getBlue(), 100);
     434        if (p instanceof Node) {
     435            Point2D pt = mv.getPoint2D((Node) p);
     436            int radius = Config.getPref().getInt("mappaint.highlight.radius", 7) + 4;
     437            int x = (int) Math.round(pt.getX());
     438            int y = (int) Math.round(pt.getY());
     439            g.setColor(transparent);
     440            g.fillOval(x - radius, y - radius, 2 * radius, 2 * radius);
     441        } else if (p instanceof Way) {
     442            Path2D path = new Path2D.Double();
     443            boolean first = true;
     444            for (Node n : ((Way) p).getNodes()) {
     445                if (n == null || n.isIncomplete()) {
     446                    continue;
     447                }
     448                Point2D pt = mv.getPoint2D(n);
     449                if (first) {
     450                    path.moveTo(pt.getX(), pt.getY());
     451                    first = false;
     452                } else {
     453                    path.lineTo(pt.getX(), pt.getY());
     454                }
     455            }
     456            g.setColor(transparent);
     457            g.setStroke(new BasicStroke(Config.getPref().getInt("mappaint.highlight.width", 4) + 4,
     458                    BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
     459            g.draw(path);
     460        }
     461    }
     462
    401463    /**
    402464     * Look, whether any object is selected. If not, select the nearest node.
    403465     * If there are no nodes in the dataset, do nothing.
    public class SelectAction extends MapMode implements ModifierExListener, KeyPres  
    531593            if (p != null) {
    532594                p.setHighlighted(true);
    533595                currentHighlight = p;
     596                dataLayerHighlight = true;
    534597                needsRepaint = true;
    535598            }
    536599            mv.setNewCursor(getCursor(p), this);
  • src/org/openstreetmap/josm/gui/MapMover.java

    diff --git a/src/org/openstreetmap/josm/gui/MapMover.java b/src/org/openstreetmap/josm/gui/MapMover.java
    index 86cbaa8..887a7a1 100644
    a b import org.openstreetmap.josm.actions.mapmode.SelectAction;  
    2121import org.openstreetmap.josm.data.coor.EastNorth;
    2222import org.openstreetmap.josm.data.preferences.BooleanProperty;
    2323import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
    24 import org.openstreetmap.josm.gui.layer.Layer;
    2524import org.openstreetmap.josm.spi.preferences.Config;
    2625import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
    2726import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
    public class MapMover extends MouseAdapter implements Destroyable {  
    244243        }
    245244        nc.resetCursor(this);
    246245        mousePosMoveStart = null;
    247         MainApplication.getLayerManager().getLayers().forEach(Layer::invalidate);
     246        // Deliberately do not invalidate the layers here: a pan only moved the view, so
     247        // the last frame's composite is still valid and the pan-reuse buffer stays alive.
    248248    }
    249249
    250250    /**
  • src/org/openstreetmap/josm/gui/MapView.java

    diff --git a/src/org/openstreetmap/josm/gui/MapView.java b/src/org/openstreetmap/josm/gui/MapView.java
    index f4034e7..10320f1 100644
    a b import org.openstreetmap.josm.gui.layer.MapViewPaintable;  
    6969import org.openstreetmap.josm.gui.layer.MapViewPaintable.LayerPainter;
    7070import org.openstreetmap.josm.gui.layer.MapViewPaintable.MapViewEvent;
    7171import org.openstreetmap.josm.gui.layer.MapViewPaintable.PaintableInvalidationEvent;
     72import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
    7273import org.openstreetmap.josm.gui.layer.MapViewPaintable.PaintableInvalidationListener;
    7374import org.openstreetmap.josm.gui.layer.OsmDataLayer;
    7475import org.openstreetmap.josm.gui.layer.markerlayer.PlayHeadMarker;
    LayerManager.LayerChangeListener, MainLayerManager.ActiveLayerChangeListener {  
    236237
    237238    private transient BufferedImage nonChangedLayersBuffer;
    238239    private transient BufferedImage offscreenBuffer;
    239     // Layers that wasn't changed since last paint
    240     private final transient List<Layer> nonChangedLayers = new ArrayList<>();
    241240    private int lastViewID;
     241    // View the composite in nonChangedLayersBuffer was painted at. Needed to reuse the
     242    // buffer for a translated view; lastViewID only detects equality, not the pan delta.
     243    private double lastPaintedScale;
     244    private EastNorth lastPaintedCenter;
     245    // The layer set the composite was painted with; hide/show/add/remove changes it.
     246    private List<Layer> lastPaintedLayers;
    242247    private final AtomicBoolean paintPreferencesChanged = new AtomicBoolean(true);
    243248    private Rectangle lastClipBounds = new Rectangle();
    244249    private transient MapMover mapMover;
    LayerManager.LayerChangeListener, MainLayerManager.ActiveLayerChangeListener {  
    491496        }
    492497    }
    493498
     499    /**
     500     * Determines if the map is currently being panned (right-button drag in progress).
     501     * @return {@code true} if a pan is in progress
     502     */
     503    public boolean isPanning() {
     504        return mapMover != null && mapMover.movementInProgress();
     505    }
     506
    494507    /**
    495508     * Draw the component.
    496509     */
    LayerManager.LayerChangeListener, MainLayerManager.ActiveLayerChangeListener {  
    551564            }
    552565        }
    553566
    554         boolean canUseBuffer = !paintPreferencesChanged.getAndSet(false)
    555                 && nonChangedLayers.size() <= nonChangedLayersCount
     567        boolean paintPrefsChanged = paintPreferencesChanged.getAndSet(false);
     568        boolean sameLayerSet = lastPaintedLayers != null && lastPaintedLayers.equals(visibleLayers);
     569        boolean canUseBuffer = !paintPrefsChanged
     570                && sameLayerSet
     571                && nonChangedLayersCount == visibleLayers.size()
    556572                && lastViewID == getViewID()
    557                 && lastClipBounds.contains(g.getClipBounds())
    558                 && nonChangedLayers.equals(visibleLayers.subList(0, nonChangedLayers.size()));
     573                && lastClipBounds.contains(g.getClipBounds());
     574
     575        // Pan-reuse: when the view moved at constant zoom, nonChangedLayersBuffer holds the
     576        // complete composite of all visible layers (copied back below), so the next frame is
     577        // the old one shifted by the pan delta plus the newly exposed strip. Layers
     578        // invalidated since the last frame (e.g. freshly loaded imagery tiles) are only
     579        // repainted inside the strip; the rest is painted when the pan stops.
     580        boolean viewMoved = lastViewID != getViewID();
     581        boolean scaleOk = Utils.equalsEpsilon(getScale(), lastPaintedScale);
     582        boolean lastClipFull = lastClipBounds.contains(0, 0, width, height);
     583        boolean canReusePanned = viewMoved
     584                && !paintPrefsChanged
     585                && sameLayerSet
     586                && scaleOk
     587                && lastClipFull
     588                && nonChangedLayersBuffer != null
     589                && nonChangedLayersBuffer.getWidth() == width && nonChangedLayersBuffer.getHeight() == height;
     590        double panDX = 0;
     591        double panDY = 0;
     592        if (canReusePanned) {
     593            EastNorth center = getCenter();
     594            panDX = (lastPaintedCenter.east() - center.east()) / lastPaintedScale;
     595            panDY = (center.north() - lastPaintedCenter.north()) / lastPaintedScale;
     596            // Sub-pixel movements (e.g. a projection change) fall back to the normal path
     597            canReusePanned = Math.abs(panDX) < getWidth() && Math.abs(panDY) < getHeight()
     598                    && (Math.abs(panDX) >= 0.5 || Math.abs(panDY) >= 0.5);
     599        }
     600        if (Config.getPref().getBoolean("mappaint.debug.pan-reuse", false)) {
     601            Logging.info("PANREUSE " + (canReusePanned ? "reuse" : "full")
     602                    + " canUse=" + canUseBuffer
     603                    + " prefs=" + paintPrefsChanged
     604                    + " count=" + nonChangedLayersCount + '/' + visibleLayers.size()
     605                    + " layerSet=" + sameLayerSet
     606                    + " buf=" + (nonChangedLayersBuffer != null
     607                            ? nonChangedLayersBuffer.getWidth() + "x" + nonChangedLayersBuffer.getHeight() : "null")
     608                    + " size=" + width + "x" + height
     609                    + " uiScale=" + uiScaleX + "x" + uiScaleY
     610                    + " clipFull=" + lastClipFull
     611                    + " scaleEq=" + scaleOk
     612                    + " delta=" + panDX + ',' + panDY
     613                    + " viewID=" + getViewID());
     614        }
    559615
    560616        if (null == offscreenBuffer || offscreenBuffer.getWidth() != width || offscreenBuffer.getHeight() != height) {
    561617            offscreenBuffer = getAcceleratedImage(this, width, height);
    562618        }
    563619
    564         if (!canUseBuffer || nonChangedLayersBuffer == null) {
     620        if (!canReusePanned && (!canUseBuffer || nonChangedLayersBuffer == null)) {
    565621            if (null == nonChangedLayersBuffer
    566622                    || nonChangedLayersBuffer.getWidth() != width || nonChangedLayersBuffer.getHeight() != height) {
    567623                nonChangedLayersBuffer = getAcceleratedImage(this, width, height);
    LayerManager.LayerChangeListener, MainLayerManager.ActiveLayerChangeListener {  
    575631            for (int i = 0; i < nonChangedLayersCount; i++) {
    576632                paintLayer(visibleLayers.get(i), g2);
    577633            }
    578         } else {
    579             // Maybe there were more unchanged layers then last time - draw them to buffer
    580             if (nonChangedLayers.size() != nonChangedLayersCount) {
    581                 Graphics2D g2 = nonChangedLayersBuffer.createGraphics();
    582                 g2.setClip(scaledClip);
    583                 g2.setTransform(trDef);
    584                 for (int i = nonChangedLayers.size(); i < nonChangedLayersCount; i++) {
    585                     paintLayer(visibleLayers.get(i), g2);
    586                 }
    587             }
    588634        }
    589635
    590         nonChangedLayers.clear();
    591         if (nonChangedLayersCount > 0)
    592             nonChangedLayers.addAll(visibleLayers.subList(0, nonChangedLayersCount));
    593636        lastViewID = getViewID();
    594637        lastClipBounds = g.getClipBounds();
     638        lastPaintedScale = getScale();
     639        lastPaintedCenter = getCenter();
     640        lastPaintedLayers = new ArrayList<>(visibleLayers);
    595641
    596642        Graphics2D tempG = offscreenBuffer.createGraphics();
    597643        tempG.setClip(scaledClip);
    598644        tempG.setTransform(new AffineTransform());
    599         tempG.drawImage(nonChangedLayersBuffer, 0, 0, null);
     645        if (canReusePanned) {
     646            // Shift the previous composite by the pan delta and repaint the exposed strips.
     647            // Strips overlap the blitted area by 1 px to hide seams; the horizontal band
     648            // excludes the vertical strip's columns so the corner is painted only once.
     649            int blitDX = (int) Math.round(panDX * uiScaleX);
     650            int blitDY = (int) Math.round(panDY * uiScaleY);
     651            tempG.setClip(0, 0, width, height);
     652            tempG.drawImage(nonChangedLayersBuffer, blitDX, blitDY, null);
     653            if (blitDX != 0) {
     654                paintPanStrip(tempG, blitDX > 0 ? 0 : width + blitDX - 1, 0,
     655                        blitDX > 0 ? blitDX + 1 : 1 - blitDX, height, visibleLayers, trDef);
     656            }
     657            if (blitDY != 0) {
     658                paintPanStrip(tempG, blitDX == 0 ? 0 : blitDX > 0 ? blitDX + 1 : 0,
     659                        blitDY > 0 ? 0 : height + blitDY - 1,
     660                        blitDX == 0 ? width : blitDX > 0 ? width - blitDX - 1 : width + blitDX - 1,
     661                        blitDY > 0 ? blitDY + 1 : 1 - blitDY, visibleLayers, trDef);
     662            }
     663            tempG.setClip(scaledClip);
     664        } else {
     665            tempG.drawImage(nonChangedLayersBuffer, 0, 0, null);
     666        }
    600667        tempG.setTransform(trDef);
    601668
    602         for (int i = nonChangedLayersCount; i < visibleLayers.size(); i++) {
    603             paintLayer(visibleLayers.get(i), tempG);
     669        // Layers invalidated since the last frame (e.g. freshly loaded imagery tiles) are
     670        // not repainted in full during a pan: a full repaint over the shifted composite
     671        // would cover the content of the layers above them. They are painted strip-only
     672        // and the remaining changes appear once the pan stops (clean rebuild).
     673        if (!canReusePanned) {
     674            for (int i = nonChangedLayersCount; i < visibleLayers.size(); i++) {
     675                paintLayer(visibleLayers.get(i), tempG);
     676            }
     677        }
     678        // Keep the buffer a complete composite of all layers so the next pan can shift it.
     679        // Only when the whole viewport was painted: a partial repaint leaves stale pixels.
     680        if (g.getClipBounds().contains(0, 0, width, height)) {
     681            Graphics2D g2 = nonChangedLayersBuffer.createGraphics();
     682            g2.drawImage(offscreenBuffer, 0, 0, null);
     683            g2.dispose();
     684        }
     685        // Layers invalidated during the pan were only repainted inside the strip. Keep them
     686        // marked so the first frame after the pan (with the view still) repaints them in
     687        // full, showing the changes that were deferred.
     688        if (canReusePanned) {
     689            for (MapViewPaintable paintable : invalidated) {
     690                invalidatedListener.invalidate(paintable);
     691            }
     692        }
     693
     694        // Imagery attribution is fixed to the viewport; painting it into the reused
     695        // composite would leave copies behind while panning
     696        for (Layer layer : visibleLayers) {
     697            if (layer instanceof AbstractTileSourceLayer) {
     698                ((AbstractTileSourceLayer<?>) layer).paintAttribution(tempG, this);
     699            }
    604700        }
    605701
    606702        try {
    LayerManager.LayerChangeListener, MainLayerManager.ActiveLayerChangeListener {  
    663759        }
    664760    }
    665761
     762    /**
     763     * Paints the layers into one of the strips exposed by a pan. The strip rectangle is
     764     * in device pixels; the layers paint in logical coordinates, so the clip is set under
     765     * the identity transform and {@code trDef} is applied for the painting. The strip is
     766     * cleared first because layers may paint with transparency over the old content.
     767     */
     768    private void paintPanStrip(Graphics2D g, int x, int y, int w, int h, List<Layer> layers, AffineTransform trDef) {
     769        g.setTransform(new AffineTransform());
     770        g.setClip(new Rectangle(x, y, w, h));
     771        g.setColor(PaintColors.getBackgroundColor());
     772        g.fillRect(x, y, w, h);
     773        g.setTransform(trDef);
     774        for (Layer layer : layers) {
     775            paintLayer(layer, g);
     776        }
     777    }
     778
    666779    private void drawTemporaryLayers(Graphics2D tempG, Bounds box) {
    667780        synchronized (temporaryLayers) {
    668781            for (MapViewPaintable mvp : temporaryLayers) {
    LayerManager.LayerChangeListener, MainLayerManager.ActiveLayerChangeListener {  
    823936        if (mapMover != null) {
    824937            mapMover.destroy();
    825938        }
    826         nonChangedLayers.clear();
    827939        synchronized (temporaryLayers) {
    828940            temporaryLayers.clear();
    829941        }
  • src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
    index 510fad3..b0edb59 100644
    a b import java.awt.Graphics2D;  
    1313import java.awt.GridBagConstraints;
    1414import java.awt.GridBagLayout;
    1515import java.awt.Image;
     16import java.awt.Rectangle;
    1617import java.awt.Shape;
    1718import java.awt.Toolkit;
    1819import java.awt.event.ActionEvent;
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    15641565        // old and unused.
    15651566    }
    15661567
     1568    /**
     1569     * Paints the imagery attribution for the current view. The map view calls this on its
     1570     * per-frame overlay: the attribution is fixed to the viewport, so painting it into the
     1571     * reused composite would leave copies behind while panning.
     1572     * @param g the overlay graphics
     1573     * @param mv the map view
     1574     */
     1575    public void paintAttribution(Graphics2D g, MapView mv) {
     1576        if (!isVisible()) {
     1577            return;
     1578        }
     1579        ProjectionBounds pb = mv.getState().getViewArea(new Rectangle(0, 0, mv.getWidth(), mv.getHeight()))
     1580                .getProjectionBounds();
     1581        EastNorth min = pb.getMin();
     1582        EastNorth max = pb.getMax();
     1583        int zoom = getDisplaySettings().isAutoZoom() ? getBestZoom() : currentZoomLevel;
     1584        attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(min), getShiftedCoord(max),
     1585                zoom, this);
     1586    }
     1587
    15671588    private void drawInViewArea(Graphics2D g, MapView mv, ProjectionBounds pb) {
    15681589        int zoom = currentZoomLevel;
    15691590        if (getDisplaySettings().isAutoZoom()) {
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    16621683            this.paintTileText(t, g);
    16631684        }
    16641685
    1665         EastNorth min = pb.getMin();
    1666         EastNorth max = pb.getMax();
    1667         attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(min), getShiftedCoord(max),
    1668                 displayZoomLevel, this);
    1669 
    16701686        g.setColor(Color.lightGray);
    16711687
    16721688        if (ts.tooLarge()) {
  • src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java b/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java
    index faaaf50..243ee31 100644
    a b public class OsmDataLayer extends AbstractOsmDataLayer  
    15071507
    15081508    @Override
    15091509    public void primitiveHovered(PrimitiveHoverEvent e) {
    1510         List<IPrimitive> primitives = new ArrayList<>(2);
    1511         primitives.add(e.getHoveredPrimitive());
    1512         primitives.add(e.getPreviousPrimitive());
    1513         primitives.removeIf(Objects::isNull);
    1514         resetTiles(primitives);
    1515         this.invalidate();
     1510        // The hover event does not change the rendered content, so nothing is invalidated
     1511        // here; listeners such as the PropertiesDialog use it to update their sidebar.
    15161512    }
    15171513
    15181514    @Override