Ticket #11487: 11487.3.patch

File 11487.3.patch, 35.9 KB (added by taylor.smock, 2 years ago)

Keep invalidated tiles until they can be painted again, refactor so that there is a new renderer class (StyledTiledMapRenderer, set mappaint.renderer-class-name to org.openstreetmap.josm.data.osm.visitor.paint.StyledTiledMapRenderer to use), various render optimizations (use "native" BufferedImage for images), add Rendering status text, avoid invalidating mapview everytime a tile finishes rendering, render around mouse preferentially

  • new file src/org/openstreetmap/josm/data/osm/visitor/paint/ImageCache.java

    Subject: [PATCH] #11487: Try to improve render performance
    ---
    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/osm/visitor/paint/ImageCache.java b/src/org/openstreetmap/josm/data/osm/visitor/paint/ImageCache.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.osm.visitor.paint;
     3
     4import java.awt.Image;
     5
     6import javax.annotation.Nullable;
     7
     8/**
     9 * A record for keeping the image information for a tile. Used in conjunction with {@link TileZXY} for
     10 * {@link org.openstreetmap.josm.data.cache.JCSCacheManager}.
     11 * @since xxx
     12 */
     13public final class ImageCache {
     14    private final boolean isDirty;
     15    private final StyledTiledMapRenderer.TileLoader imageFuture;
     16    private final Image image;
     17    /**
     18     * Create a new {@link ImageCache} object
     19     * @param image The image to paint (optional; either this or {@link #imageFuture} must be specified)
     20     * @param imageFuture The future for the image (optional; either this or {@link #image} must be specified)
     21     * @param isDirty {@code true} if the tile needs to be repainted
     22     */
     23    public ImageCache(Image image, StyledTiledMapRenderer.TileLoader imageFuture, boolean isDirty) {
     24        this.image = image;
     25        this.imageFuture = imageFuture;
     26        this.isDirty = isDirty;
     27        if (image == null && imageFuture == null) {
     28            throw new IllegalArgumentException("Either image or imageFuture must be non-null");
     29        }
     30    }
     31
     32    /**
     33     * Check if this tile is dirty
     34     * @return {@code true} if this is a dirty tile
     35     */
     36    public boolean isDirty() {
     37        return this.isDirty;
     38    }
     39
     40    /**
     41     * Mark this tile as dirty
     42     * @return The tile to put in the cache
     43     */
     44    public ImageCache becomeDirty() {
     45        if (this.isDirty) {
     46            return this;
     47        }
     48        return new ImageCache(this.image, this.imageFuture, true);
     49    }
     50
     51    /**
     52     * Get the image to paint
     53     * @return The image (may be {@code null})
     54     */
     55    @Nullable
     56    public Image image() {
     57        return this.image;
     58    }
     59
     60    /**
     61     * Get the image future
     62     * @return The image future (may be {@code null})
     63     */
     64    @Nullable
     65    public StyledTiledMapRenderer.TileLoader imageFuture() {
     66        return this.imageFuture;
     67    }
     68}
  • src/org/openstreetmap/josm/data/osm/visitor/paint/MapRendererFactory.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/osm/visitor/paint/MapRendererFactory.java b/src/org/openstreetmap/josm/data/osm/visitor/paint/MapRendererFactory.java
    a b  
    191191                tr("Styled Map Renderer"),
    192192                tr("Renders the map using style rules in a set of style sheets.")
    193193        );
     194        register(
     195                StyledTiledMapRenderer.class,
     196                tr("Styled Map Renderer (tiled)"),
     197                tr("Renders the map using style rules in a set of style sheets by tile.")
     198        );
    194199    }
    195200
    196201    /**
  • new file src/org/openstreetmap/josm/data/osm/visitor/paint/StyledTiledMapRenderer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledTiledMapRenderer.java b/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledTiledMapRenderer.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.osm.visitor.paint;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.awt.AlphaComposite;
     7import java.awt.Color;
     8import java.awt.Font;
     9import java.awt.Graphics2D;
     10import java.awt.Image;
     11import java.awt.Point;
     12import java.awt.Transparency;
     13import java.awt.event.MouseEvent;
     14import java.awt.geom.AffineTransform;
     15import java.awt.image.BufferedImage;
     16import java.util.Comparator;
     17import java.util.List;
     18import java.util.Set;
     19import java.util.function.Consumer;
     20import java.util.stream.Collectors;
     21
     22import org.apache.commons.jcs3.access.CacheAccess;
     23import org.openstreetmap.josm.data.Bounds;
     24import org.openstreetmap.josm.data.coor.LatLon;
     25import org.openstreetmap.josm.data.osm.OsmData;
     26import org.openstreetmap.josm.data.projection.ProjectionRegistry;
     27import org.openstreetmap.josm.gui.MainApplication;
     28import org.openstreetmap.josm.gui.MapView;
     29import org.openstreetmap.josm.gui.NavigatableComponent;
     30import org.openstreetmap.josm.spi.preferences.Config;
     31import org.openstreetmap.josm.tools.Logging;
     32
     33/**
     34 * A styled render that does the rendering on a tile basis
     35 */
     36public class StyledTiledMapRenderer extends StyledMapRenderer {
     37    private CacheAccess<TileZXY, ImageCache> cache;
     38    private int zoom;
     39    private Consumer<TileZXY> notifier;
     40
     41    /**
     42     * Constructs a new {@code StyledMapRenderer}.
     43     *
     44     * @param g              the graphics context. Must not be null.
     45     * @param nc             the map viewport. Must not be null.
     46     * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they
     47     *                       look inactive. Example: rendering of data in an inactive layer using light gray as color only.
     48     * @throws IllegalArgumentException if {@code g} is null
     49     * @throws IllegalArgumentException if {@code nc} is null
     50     */
     51    public StyledTiledMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) {
     52        super(g, nc, isInactiveMode);
     53    }
     54
     55    @Override
     56    public void render(OsmData<?, ?, ?, ?> data, boolean renderVirtualNodes, Bounds bounds) {
     57        // If there is no cache, fall back to old behavior
     58        if (this.cache == null) {
     59            super.render(data, renderVirtualNodes, bounds);
     60            return;
     61        }
     62        final BufferedImage tempImage;
     63        final Graphics2D tempG2d;
     64        // I'd like to avoid two image copies, but there are some issues using the original g2d object
     65        tempImage = nc.getGraphicsConfiguration().createCompatibleImage(this.nc.getWidth(), this.nc.getHeight(), Transparency.TRANSLUCENT);
     66        tempG2d = tempImage.createGraphics();
     67        tempG2d.setComposite(AlphaComposite.DstAtop); // Avoid tile lines in large areas
     68
     69        final List<TileZXY> toRender = TileZXY.boundsToTiles(bounds.getMinLat(), bounds.getMinLon(),
     70                bounds.getMaxLat(), bounds.getMaxLon(), zoom).collect(Collectors.toList());
     71        final Bounds box = new Bounds(bounds);
     72        toRender.stream().map(TileZXY::tileToBounds).forEach(box::extend);
     73        final int tileSize;
     74        if (toRender.isEmpty()) {
     75            tileSize = Config.getPref().getInt("mappaint.fast_render.tile_size", 256); // Mostly to keep the compiler happy
     76        } else {
     77            final TileZXY tile = toRender.get(0);
     78            final Bounds box2 = TileZXY.tileToBounds(tile);
     79            final Point min = this.nc.getPoint(box2.getMin());
     80            final Point max = this.nc.getPoint(box2.getMax());
     81            tileSize = max.x - min.x + 2; // Buffer by 2 pixels to avoid black lines
     82        }
     83
     84        // Sort the tiles based off of proximity to the mouse pointer
     85        if (nc instanceof MapView) { // Ideally this would either be an interface or a method in NavigableComponent
     86            final MapView mv = (MapView) nc;
     87            final MouseEvent mouseEvent = mv.lastMEvent;
     88            final LatLon mousePosition = nc.getLatLon(mouseEvent.getX(), mouseEvent.getY());
     89            final TileZXY mouseTile = TileZXY.latLonToTile(mousePosition.lat(), mousePosition.lon(), zoom);
     90            toRender.sort(Comparator.comparingInt(tile -> {
     91                final int x = tile.x() - mouseTile.x();
     92                final int y = tile.y() - mouseTile.y();
     93                return x * x + y * y;
     94            }));
     95        }
     96
     97        // We want to prioritize where the mouse is, but having some in the queue will reduce overall paint time
     98        int submittedTile = 5;
     99        int painted = 0;
     100        for (TileZXY tile : toRender) {
     101            final Image tileImage;
     102            // Needed to avoid having tiles that aren't rendered properly
     103            final ImageCache tImg = this.cache.get(tile);
     104            final boolean wasDirty = tImg != null && tImg.isDirty();
     105            if (tImg != null && !tImg.isDirty() && tImg.imageFuture() != null) {
     106                submittedTile = 0; // Don't submit new tiles if there are futures already in the queue. Not perfect.
     107            }
     108            if (submittedTile > 0 && (tImg == null || tImg.isDirty())) {
     109                // Ensure that we don't add a large number of render calls
     110                if (tImg != null && tImg.imageFuture() != null) {
     111                    tImg.imageFuture().cancel();
     112                }
     113                submittedTile--;
     114                // Note that the paint code is *not* thread safe, so all tiles must be painted on the same thread.
     115                // FIXME figure out how to make this thread safe? Probably not necessary, since UI isn't blocked, but it would be a nice to have
     116                TileLoader loader = new TileLoader(data, tile, tileSize);
     117                MainApplication.worker.submit(loader);
     118                if (tImg == null) {
     119                    this.cache.put(tile, new ImageCache(null, loader, false));
     120                } else {
     121                    // This might cause some extra renders, but *probably* ok
     122                    this.cache.put(tile, new ImageCache(tImg.image(), loader, true));
     123                }
     124                tileImage = tImg != null ? tImg.image() : null;
     125            } else if (tImg != null) {
     126                tileImage = tImg.image();
     127            } else {
     128                tileImage = null;
     129            }
     130            final Point point = this.nc.getPoint(tile);
     131            if (tileImage != null) {
     132                // FIXME move to isTraceEnabled prior to commit
     133                if ((wasDirty && Logging.isTraceEnabled()) || this.isInactiveMode) {
     134                    tempG2d.setColor(Color.DARK_GRAY);
     135                    tempG2d.fillRect(point.x, point.y, tileSize, tileSize);
     136                } else {
     137                    painted++;
     138                }
     139                tempG2d.drawImage(tileImage, point.x, point.y, tileSize, tileSize, null, null);
     140            } else {
     141                Logging.trace("StyledMapRenderer did not paint tile {1}", tile);
     142            }
     143        }
     144        final double percentDrawn = 100 * painted / (double) toRender.size();
     145        if (percentDrawn < 99.99) {
     146            final int x = 0;
     147            final int y = nc.getHeight() / 8;
     148            final String message = tr("Rendering Status: {0}%", Math.floor(percentDrawn));
     149            tempG2d.setComposite(AlphaComposite.SrcOver);
     150            tempG2d.setFont(new Font("sansserif", Font.BOLD, 13));
     151            tempG2d.setColor(Color.BLACK);
     152            tempG2d.drawString(message, x + 1, y);
     153            tempG2d.setColor(Color.LIGHT_GRAY);
     154            tempG2d.drawString(message, x, y);
     155        }
     156        tempG2d.dispose();
     157        g.drawImage(tempImage, 0, 0, null);
     158    }
     159
     160    /**
     161     * Set the cache for this painter. If not set, this acts like {@link StyledMapRenderer}.
     162     * @param box The box we will be rendering -- any jobs for tiles outside of this box will be cancelled
     163     * @param cache The cache to use
     164     * @param zoom The zoom level to use for creating the tiles
     165     * @param notifier The method to call when a tile has been updated. This may or may not be called in the EDT.
     166     */
     167    public void setCache(Bounds box, CacheAccess<TileZXY, ImageCache> cache, int zoom, Consumer<TileZXY> notifier) {
     168        this.cache = cache;
     169        this.zoom = zoom;
     170        this.notifier = notifier != null ? notifier : tile -> { /* Do nothing */ };
     171
     172        Set<TileZXY> tiles = TileZXY.boundsToTiles(box.getMinLat(), box.getMinLon(), box.getMaxLat(), box.getMaxLon(), zoom)
     173                .collect(Collectors.toSet());
     174        cache.getMatching(".*").forEach((key, value) -> {
     175            if (!tiles.contains(key)) {
     176                cancelImageFuture(cache, key, value);
     177            }
     178        });
     179    }
     180
     181    private static void cancelImageFuture(CacheAccess<TileZXY, ImageCache> cache, TileZXY key, ImageCache value) {
     182        if (value.imageFuture() != null) {
     183            value.imageFuture().cancel();
     184            if (value.image() == null) {
     185                cache.remove(key);
     186            } else {
     187                cache.put(key, new ImageCache(value.image(), null, value.isDirty()));
     188            }
     189        }
     190    }
     191
     192    private BufferedImage generateTile(OsmData<?, ?, ?, ?> data, TileZXY tile, int tileSize) {
     193        BufferedImage bufferedImage = nc.getGraphicsConfiguration().createCompatibleImage(tileSize, tileSize, Transparency.TRANSLUCENT);
     194        Graphics2D g2d = bufferedImage.createGraphics();
     195        try {
     196            // Render to the surrounding tiles for continuity -- this probably needs to be tweaked
     197            final int buffer = 2;
     198            Bounds bounds = TileZXY.tileToBounds(new TileZXY(zoom, tile.x() - buffer, tile.y() - buffer));
     199            bounds.extend(TileZXY.tileToBounds(new TileZXY(zoom, tile.x() + buffer, tile.y() + buffer)));
     200
     201            final int renderSize = tileSize * (2 * buffer + 1);
     202            final NavigatableComponent temporaryView = new NavigatableComponent() {
     203                @Override
     204                public int getWidth() {
     205                    return renderSize;
     206                }
     207
     208                @Override
     209                public int getHeight() {
     210                    return renderSize;
     211                }
     212            };
     213            temporaryView.setSize(renderSize, renderSize);
     214            temporaryView.zoomTo(TileZXY.tileToBounds(tile).getCenter().getEastNorth(ProjectionRegistry.getProjection()), mapState.getScale());
     215            g2d.setTransform(AffineTransform.getTranslateInstance(-buffer * (double) tileSize, -buffer * (double) tileSize));
     216            final AbstractMapRenderer tilePainter = MapRendererFactory.getInstance().createActiveRenderer(g2d, temporaryView, false);
     217            tilePainter.render(data, true, bounds);
     218        } finally {
     219            g2d.dispose();
     220        }
     221        return bufferedImage;
     222    }
     223
     224    class TileLoader implements Runnable {
     225        private final TileZXY tile;
     226        private final int tileSize;
     227        private final OsmData<?, ?, ?, ?> data;
     228        private boolean cancel;
     229
     230        TileLoader(OsmData<?, ?, ?, ?> data, TileZXY tile, int tileSize) {
     231            this.data = data;
     232            this.tile = tile;
     233            this.tileSize = tileSize;
     234        }
     235
     236        @Override
     237        public void run() {
     238            if (!cancel) {
     239                final BufferedImage tImage = generateTile(data, tile, tileSize);
     240                cache.put(tile, new ImageCache(tImage, null, false));
     241                notifier.accept(tile);
     242            }
     243        }
     244
     245        /**
     246         * Cancel this job without causing a {@link java.util.concurrent.CancellationException}
     247         */
     248        void cancel() {
     249            this.cancel = true;
     250        }
     251    }
     252}
  • new file src/org/openstreetmap/josm/data/osm/visitor/paint/TileZXY.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/osm/visitor/paint/TileZXY.java b/src/org/openstreetmap/josm/data/osm/visitor/paint/TileZXY.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.osm.visitor.paint;
     3
     4import java.util.stream.IntStream;
     5import java.util.stream.Stream;
     6
     7import org.openstreetmap.josm.data.Bounds;
     8import org.openstreetmap.josm.data.coor.ILatLon;
     9
     10/**
     11 * A record used for storing tile information for painting
     12 * @since xxx
     13 */
     14public final class TileZXY implements ILatLon {
     15    private final int zoom;
     16    private final int x;
     17    private final int y;
     18
     19    /**
     20     * Create a new {@link TileZXY} object
     21     * @param zoom The zoom for which this tile was created
     22     * @param x The x coordinate at the specified zoom level
     23     * @param y The y coordinate at the specified zoom level
     24     */
     25    public TileZXY(int zoom, int x, int y) {
     26        this.zoom = zoom;
     27        this.x = x;
     28        this.y = y;
     29    }
     30
     31    /**
     32     * Get the zoom level
     33     * @return The zoom level for which this tile was created
     34     */
     35    public int zoom() {
     36        return this.zoom;
     37    }
     38
     39    /**
     40     * Get the x coordinate
     41     * @return The x coordinate for this tile
     42     */
     43    public int x() {
     44        return this.x;
     45    }
     46
     47    /**
     48     * Get the y coordinate
     49     * @return The y coordinate for this tile
     50     */
     51    public int y() {
     52        return this.y;
     53    }
     54
     55    /**
     56     * Get the latitude for upper-left corner of this tile
     57     * @return The latitude
     58     */
     59    @Override
     60    public double lat() {
     61        return yToLat(this.y(), this.zoom());
     62    }
     63
     64    /**
     65     * Get the longitude for the upper-left corner of this tile
     66     * @return The longitude
     67     */
     68    @Override
     69    public double lon() {
     70        return xToLon(this.x(), this.zoom());
     71    }
     72
     73    /**
     74     * Convert a bounds to a series of tiles that entirely cover the bounds
     75     * @param minLat The minimum latitude
     76     * @param minLon The minimum longitude
     77     * @param maxLat The maximum latitude
     78     * @param maxLon The maximum longitude
     79     * @param zoom The zoom level to generate the tiles for
     80     * @return The stream of tiles
     81     */
     82    public static Stream<TileZXY> boundsToTiles(double minLat, double minLon, double maxLat, double maxLon, int zoom) {
     83        return boundsToTiles(minLat, minLon, maxLat, maxLon, zoom, 0);
     84    }
     85
     86    /**
     87     * Convert a bounds to a series of tiles that entirely cover the bounds
     88     * @param minLat The minimum latitude
     89     * @param minLon The minimum longitude
     90     * @param maxLat The maximum latitude
     91     * @param maxLon The maximum longitude
     92     * @param zoom The zoom level to generate the tiles for
     93     * @param expansion The number of tiles to expand on the x/y axis (1 row north, 1 row south, 1 column left, 1 column right)
     94     * @return The stream of tiles
     95     */
     96    public static Stream<TileZXY> boundsToTiles(double minLat, double minLon, double maxLat, double maxLon, int zoom, int expansion) {
     97        final TileZXY upperRight = latLonToTile(maxLat, maxLon, zoom);
     98        final TileZXY lowerLeft = latLonToTile(minLat, minLon, zoom);
     99        return IntStream.rangeClosed(lowerLeft.x() - expansion, upperRight.x() + expansion)
     100                .mapToObj(x -> IntStream.rangeClosed(upperRight.y() - expansion, lowerLeft.y() + expansion)
     101                        .mapToObj(y -> new TileZXY(zoom, x, y)))
     102                .flatMap(stream -> stream);
     103    }
     104
     105    /**
     106     * Convert a tile to the bounds for that tile
     107     * @param tile The tile to get the bounds for
     108     * @return The bounds
     109     */
     110    public static Bounds tileToBounds(TileZXY tile) {
     111        return new Bounds(yToLat(tile.y() + 1, tile.zoom()), xToLon(tile.x(), tile.zoom()),
     112                yToLat(tile.y(), tile.zoom()), xToLon(tile.x() + 1, tile.zoom()));
     113    }
     114
     115    /**
     116     * Convert a x tile coordinate to a latitude
     117     * @param x The x coordinate
     118     * @param zoom The zoom level to use for the calculation
     119     * @return The latitude for the x coordinate (upper-left of the tile)
     120     */
     121    public static double xToLon(int x, int zoom) {
     122        return (x / Math.pow(2, zoom)) * 360 - 180;
     123    }
     124
     125    /**
     126     * Convert a y tile coordinate to a latitude
     127     * @param y The y coordinate
     128     * @param zoom The zoom level to use for the calculation
     129     * @return The latitude for the y coordinate (upper-left of the tile)
     130     */
     131    public static double yToLat(int y, int zoom) {
     132        double t = Math.PI - (2 * Math.PI * y) / Math.pow(2, zoom);
     133        return 180 / Math.PI * Math.atan((Math.exp(t) - Math.exp(-t)) / 2);
     134    }
     135
     136    /**
     137     * Convert a lat, lon, and zoom to a tile coordiante
     138     * @param lat The latitude
     139     * @param lon The longitude
     140     * @param zoom The zoom level
     141     * @return The specified tile coordinates at the specified zoom
     142     */
     143    public static TileZXY latLonToTile(double lat, double lon, int zoom) {
     144        int xCoord = (int) Math.floor(Math.pow(2, zoom) * (180 + lon) / 360);
     145        int yCoord = (int) Math.floor(Math.pow(2, zoom) *
     146                (1 - Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI) / 2);
     147        return new TileZXY(zoom, xCoord, yCoord);
     148    }
     149
     150    @Override
     151    public String toString() {
     152        return "TileZXY{" + zoom + "/" + x + "/" + y + "}";
     153    }
     154
     155    @Override
     156    public int hashCode() {
     157        // We only care about comparing zoom, x, and y
     158        return Integer.hashCode(this.zoom) + 31 * (Integer.hashCode(this.x) + 31 * Integer.hashCode(this.y));
     159    }
     160
     161    @Override
     162    public boolean equals(Object obj) {
     163        if (obj instanceof TileZXY) {
     164            TileZXY o = (TileZXY) obj;
     165            return this.zoom == o.zoom && this.x == o.x && this.y == o.y;
     166        }
     167        return false;
     168    }
     169}
  • src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java b/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java
    a b  
    4949import javax.swing.JPanel;
    5050import javax.swing.JScrollPane;
    5151
     52import org.apache.commons.jcs3.access.CacheAccess;
     53import org.openstreetmap.gui.jmapviewer.OsmMercator;
    5254import org.openstreetmap.josm.actions.AutoScaleAction;
    5355import org.openstreetmap.josm.actions.ExpertToggleAction;
    5456import org.openstreetmap.josm.actions.RenameLayerAction;
     
    5860import org.openstreetmap.josm.data.Data;
    5961import org.openstreetmap.josm.data.ProjectionBounds;
    6062import org.openstreetmap.josm.data.UndoRedoHandler;
     63import org.openstreetmap.josm.data.cache.JCSCacheManager;
    6164import org.openstreetmap.josm.data.conflict.Conflict;
    6265import org.openstreetmap.josm.data.conflict.ConflictCollection;
    6366import org.openstreetmap.josm.data.coor.EastNorth;
     
    7073import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
    7174import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
    7275import org.openstreetmap.josm.data.gpx.WayPoint;
     76import org.openstreetmap.josm.data.osm.BBox;
    7377import org.openstreetmap.josm.data.osm.DataIntegrityProblemException;
    7478import org.openstreetmap.josm.data.osm.DataSelectionListener;
    7579import org.openstreetmap.josm.data.osm.DataSet;
     
    7781import org.openstreetmap.josm.data.osm.DatasetConsistencyTest;
    7882import org.openstreetmap.josm.data.osm.DownloadPolicy;
    7983import org.openstreetmap.josm.data.osm.HighlightUpdateListener;
     84import org.openstreetmap.josm.data.osm.INode;
    8085import org.openstreetmap.josm.data.osm.IPrimitive;
     86import org.openstreetmap.josm.data.osm.IWay;
    8187import org.openstreetmap.josm.data.osm.Node;
    8288import org.openstreetmap.josm.data.osm.OsmPrimitive;
    8389import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator;
     
    9197import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
    9298import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
    9399import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer;
     100import org.openstreetmap.josm.data.osm.visitor.paint.ImageCache;
    94101import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory;
     102import org.openstreetmap.josm.data.osm.visitor.paint.StyledTiledMapRenderer;
     103import org.openstreetmap.josm.data.osm.visitor.paint.TileZXY;
    95104import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
    96105import org.openstreetmap.josm.data.preferences.BooleanProperty;
    97106import org.openstreetmap.josm.data.preferences.IntegerProperty;
     
    104113import org.openstreetmap.josm.gui.MapFrame;
    105114import org.openstreetmap.josm.gui.MapView;
    106115import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
     116import org.openstreetmap.josm.gui.NavigatableComponent;
     117import org.openstreetmap.josm.gui.PrimitiveHoverListener;
    107118import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
    108119import org.openstreetmap.josm.gui.datatransfer.data.OsmLayerTransferData;
    109120import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
     
    144155 * @author imi
    145156 * @since 17
    146157 */
    147 public class OsmDataLayer extends AbstractOsmDataLayer implements Listener, DataSelectionListener, HighlightUpdateListener {
     158public class OsmDataLayer extends AbstractOsmDataLayer
     159        implements Listener, DataSelectionListener, HighlightUpdateListener, PrimitiveHoverListener {
     160    private static final int MAX_ZOOM = 30;
     161    private static final int OVER_ZOOM = 2;
    148162    private static final int HATCHED_SIZE = 15;
    149163    // U+2205 EMPTY SET
    150164    private static final String IS_EMPTY_SYMBOL = "\u2205";
     
    155169    private boolean requiresUploadToServer;
    156170    /** Flag used to know if the layer is being uploaded */
    157171    private final AtomicBoolean isUploadInProgress = new AtomicBoolean(false);
     172    /**
     173     * A cache used for painting
     174     */
     175    private final CacheAccess<TileZXY, ImageCache> cache = JCSCacheManager.getCache("osmDataLayer:" + System.identityHashCode(this));
     176    /** The map paint index that was painted (used to invalidate {@link #cache}) */
     177    private int lastDataIdx;
     178    /** The last zoom level (we invalidate all tiles when switching layers) */
     179    private int lastZoom;
     180    private boolean hoverListenerAdded;
    158181
    159182    /**
    160183     * List of validation errors in this layer.
     
    497520     * Draw nodes last to overlap the ways they belong to.
    498521     */
    499522    @Override public void paint(final Graphics2D g, final MapView mv, Bounds box) {
     523        if (!hoverListenerAdded) {
     524            MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
     525            hoverListenerAdded = true;
     526        }
    500527        boolean active = mv.getLayerManager().getActiveLayer() == this;
    501528        boolean inactive = !active && Config.getPref().getBoolean("draw.data.inactive_color", true);
    502529        boolean virtual = !inactive && mv.isVirtualNodesEnabled();
     
    537564            }
    538565        }
    539566
     567        // Used to invalidate cache
     568        int zoom = getZoom(mv);
     569        if (zoom != lastZoom) {
     570            // We just mark the previous zoom as dirty before moving in.
     571            // It means we don't have to traverse up/down z-levels marking tiles as dirty.
     572            this.cache.getMatching("TileZXY\\{" + lastZoom + "/.*")
     573                    .forEach((tile, imageCache) -> this.cache.put(tile, imageCache.becomeDirty()));
     574            lastZoom = zoom;
     575        }
    540576        AbstractMapRenderer painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, inactive);
    541         painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress()
    542                 || !PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get());
    543         painter.render(data, virtual, box);
     577        if (!(painter instanceof StyledTiledMapRenderer) || zoom - OVER_ZOOM > Config.getPref().getInt("mappaint.fast_render.zlevel", 16)) {
     578            painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress()
     579                    || !PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get());
     580        } else {
     581            StyledTiledMapRenderer renderer = (StyledTiledMapRenderer) painter;
     582            renderer.setCache(box, this.cache, zoom, (tile) -> {
     583                /* This causes "bouncing". I'm not certain why.
     584                if (oldState.equalsInWindow(mv.getState())) { (oldstate = mv.getState())
     585                    final Point upperLeft = mv.getPoint(tile);
     586                    final Point lowerRight = mv.getPoint(new TileZXY(tile.zoom(), tile.x() + 1, tile.y() + 1));
     587                    GuiHelper.runInEDT(() -> mv.repaint(0, upperLeft.x, upperLeft.y, lowerRight.x - upperLeft.x, lowerRight.y - upperLeft.y));
     588                }
     589                 */
     590                // Invalidate doesn't trigger an instant repaint, but putting this off lets us batch the repaints needed for multiple tiles
     591                MainApplication.worker.submit(this::invalidate);
     592            });
     593
     594            if (this.data.getMappaintCacheIndex() != this.lastDataIdx) {
     595                this.cache.clear();
     596                this.lastDataIdx = this.data.getMappaintCacheIndex();
     597                Logging.trace("OsmDataLayer {0} paint cache cleared", this.getName());
     598            }
     599        }
     600        painter.render(this.data, virtual, box);
    544601        MainApplication.getMap().conflictDialog.paintConflicts(g, mv);
    545602    }
    546603
     
    11471204        validationErrors.clear();
    11481205        removeClipboardDataFor(this);
    11491206        recentRelations.clear();
     1207        if (hoverListenerAdded) {
     1208            hoverListenerAdded = false;
     1209            MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
     1210        }
    11501211    }
    11511212
    11521213    protected static void removeClipboardDataFor(OsmDataLayer osm) {
     
    11651226
    11661227    @Override
    11671228    public void processDatasetEvent(AbstractDatasetChangedEvent event) {
     1229        resetTiles(event.getPrimitives());
    11681230        invalidate();
    11691231        setRequiresSaveToFile(true);
    11701232        setRequiresUploadToServer(event.getDataset().requiresUploadToServer());
     
    11721234
    11731235    @Override
    11741236    public void selectionChanged(SelectionChangeEvent event) {
     1237        Set<IPrimitive> primitives = new HashSet<>(event.getAdded());
     1238        primitives.addAll(event.getRemoved());
     1239        resetTiles(primitives);
    11751240        invalidate();
    11761241    }
    11771242
     1243    private void resetTiles(Iterable<? extends IPrimitive> primitives) {
     1244        for (IPrimitive primitive : primitives) {
     1245            if (primitive == null || primitive.getDataSet() != this.getDataSet()) continue;
     1246            final Collection<? extends IPrimitive> referrers = primitive.getReferrers();
     1247            // We can usually avoid invalidating a bunch of tiles when the way is not an area or part of a multipolygon
     1248            if (primitive instanceof IWay<?> && !((IWay<?>) primitive).isClosed() && referrers.stream().noneMatch(IPrimitive::isMultipolygon)) {
     1249                double lastLat = Double.NaN;
     1250                double lastLon = Double.NaN;
     1251                for (INode n : ((IWay<?>) primitive).getNodes()) {
     1252                    final double lat = n.lat();
     1253                    final double lon = n.lon();
     1254                    if (!Double.isNaN(lastLat)) {
     1255                        resetBounds(Math.min(lat, lastLat), Math.min(lon, lastLon),
     1256                                Math.max(lat, lastLat), Math.max(lon, lastLon));
     1257                    } else { // Just in case there is a 1 node way (illegal data)
     1258                        resetBounds(lat, lon, lat, lon);
     1259                    }
     1260                    lastLat = lat;
     1261                    lastLon = lon;
     1262                }
     1263            } else {
     1264                final BBox box = primitive.getBBox();
     1265                resetBounds(box.getMinLat(), box.getMinLon(), box.getMaxLat(), box.getMaxLon());
     1266                if (!referrers.isEmpty()) {
     1267                    resetTiles(referrers);
     1268                }
     1269            }
     1270        }
     1271    }
     1272
     1273    private void resetBounds(double minLat, double minLon, double maxLat, double maxLon) {
     1274        // Get the current zoom. Hopefully we aren't painting with a different navigatable component
     1275        final int currentZoom = lastZoom;
     1276        TileZXY.boundsToTiles(minLat, minLon, maxLat, maxLon, currentZoom, 1).forEach(tile -> {
     1277            final ImageCache imageCache = this.cache.get(tile);
     1278            if (imageCache != null && !imageCache.isDirty()) {
     1279                this.cache.put(tile, imageCache.becomeDirty());
     1280            }
     1281        });
     1282    }
     1283
     1284    private static int getZoom(NavigatableComponent navigatableComponent) {
     1285        final double scale = navigatableComponent.getScale();
     1286        // We might have to fall back to the old method if user is reprojecting
     1287        // 256 is the "target" size, (TODO check HiDPI!)
     1288        final int targetSize = Config.getPref().getInt("mappaint.fast_render.tile_size", 256);
     1289        final double topResolution = 2 * Math.PI * OsmMercator.EARTH_RADIUS / targetSize;
     1290        int zoom;
     1291        for (zoom = 0; zoom < MAX_ZOOM; zoom++) { // Use something like imagery.{generic|tms}.max_zoom_lvl (20 is a bit too low for our needs)
     1292            if (scale > topResolution / Math.pow(2, zoom)) {
     1293                zoom = zoom > 0 ? zoom - 1 : zoom;
     1294                break;
     1295            }
     1296        }
     1297        // We paint at a few levels higher, note that the tiles are appropriately sized (if 256 is the "target" size, the tiles should be
     1298        // 64px square).
     1299        zoom += OVER_ZOOM;
     1300        return zoom;
     1301    }
     1302
    11781303    @Override
    11791304    public void projectionChanged(Projection oldValue, Projection newValue) {
    11801305         // No reprojection required. The dataset itself is registered as projection
     
    13071432        invalidate();
    13081433    }
    13091434
     1435    @Override
     1436    public void primitiveHovered(PrimitiveHoverEvent e) {
     1437        resetTiles(Arrays.asList(e.getHoveredPrimitive(), e.getPreviousPrimitive()));
     1438        this.invalidate();
     1439    }
     1440
    13101441    @Override
    13111442    public void setName(String name) {
    13121443        if (data != null) {
  • src/org/openstreetmap/josm/gui/MapView.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/MapView.java b/src/org/openstreetmap/josm/gui/MapView.java
    a b  
    99import java.awt.Point;
    1010import java.awt.Rectangle;
    1111import java.awt.Shape;
     12import java.awt.Transparency;
    1213import java.awt.event.ComponentAdapter;
    1314import java.awt.event.ComponentEvent;
    1415import java.awt.event.KeyEvent;
     
    540541                && nonChangedLayers.equals(visibleLayers.subList(0, nonChangedLayers.size()));
    541542
    542543        if (null == offscreenBuffer || offscreenBuffer.getWidth() != width || offscreenBuffer.getHeight() != height) {
    543             offscreenBuffer = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
     544            offscreenBuffer = this.getGraphicsConfiguration().createCompatibleImage(width, height, Transparency.OPAQUE);
    544545        }
    545546
    546547        if (!canUseBuffer || nonChangedLayersBuffer == null) {
    547548            if (null == nonChangedLayersBuffer
    548549                    || nonChangedLayersBuffer.getWidth() != width || nonChangedLayersBuffer.getHeight() != height) {
    549                 nonChangedLayersBuffer = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
     550                nonChangedLayersBuffer = this.getGraphicsConfiguration().createCompatibleImage(width, height, Transparency.OPAQUE);
    550551            }
    551552            Graphics2D g2 = nonChangedLayersBuffer.createGraphics();
    552553            g2.setClip(scaledClip);