Ticket #11487: 11487.4.patch

File 11487.4.patch, 45.3 KB (added by taylor.smock, 10 months ago)

Add UI element to enable turning tiled rendering off/on, initial work on batching tile rendering

  • new file src/org/openstreetmap/josm/actions/TiledRenderToggleAction.java

    Subject: [PATCH] #11487
    ---
    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/actions/TiledRenderToggleAction.java b/src/org/openstreetmap/josm/actions/TiledRenderToggleAction.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.actions;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.awt.event.ActionEvent;
     7
     8import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory;
     9import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer;
     10import org.openstreetmap.josm.data.osm.visitor.paint.StyledTiledMapRenderer;
     11import org.openstreetmap.josm.gui.MainApplication;
     12import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     13
     14/**
     15 * This class enables and disables tiled rendering mode.
     16 * This is intended to be short-term until the tiled rendering
     17 * has not significant issues at high zoom levels.
     18 * @since xxx
     19 */
     20public class TiledRenderToggleAction extends ToggleAction implements ExpertToggleAction.ExpertModeChangeListener {
     21    /**
     22     * Create a new action for toggling render methods
     23     */
     24    public TiledRenderToggleAction() {
     25        super(tr("Tiled Rendering"),
     26                null,
     27                tr("Enable/disable rendering the map in tiles"),
     28                null,
     29                false /* register toolbar */
     30            );
     31        setToolbarId("tiledRendering");
     32        MainApplication.getToolbar().register(this);
     33        setSelected(false); // Always start disabled
     34        ExpertToggleAction.addExpertModeChangeListener(this, true);
     35    }
     36
     37    @Override
     38    protected boolean listenToSelectionChange() {
     39        return false;
     40    }
     41
     42    @Override
     43    public void expertChanged(boolean isExpert) {
     44        this.updateEnabledState();
     45    }
     46
     47    @Override
     48    protected void updateEnabledState() {
     49        setEnabled(getLayerManager().getActiveData() != null && ExpertToggleAction.isExpert());
     50    }
     51
     52    @Override
     53    public void actionPerformed(ActionEvent e) {
     54        toggleSelectedState(e);
     55        if (isSelected()) {
     56            MapRendererFactory.getInstance().activate(StyledTiledMapRenderer.class);
     57        } else {
     58            MapRendererFactory.getInstance().activate(StyledMapRenderer.class);
     59        }
     60
     61        notifySelectedState();
     62        getLayerManager().getLayersOfType(OsmDataLayer.class).forEach(OsmDataLayer::invalidate);
     63    }
     64}
  • new file src/org/openstreetmap/josm/data/osm/visitor/paint/ImageCache.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/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 jakarta.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.ArrayList;
     17import java.util.Collection;
     18import java.util.Comparator;
     19import java.util.IntSummaryStatistics;
     20import java.util.List;
     21import java.util.Objects;
     22import java.util.Optional;
     23import java.util.Set;
     24import java.util.concurrent.Executor;
     25import java.util.function.Consumer;
     26import java.util.stream.Collectors;
     27
     28import org.apache.commons.jcs3.access.CacheAccess;
     29import org.openstreetmap.josm.data.Bounds;
     30import org.openstreetmap.josm.data.coor.LatLon;
     31import org.openstreetmap.josm.data.osm.OsmData;
     32import org.openstreetmap.josm.data.projection.ProjectionRegistry;
     33import org.openstreetmap.josm.gui.MainApplication;
     34import org.openstreetmap.josm.gui.MapView;
     35import org.openstreetmap.josm.gui.NavigatableComponent;
     36import org.openstreetmap.josm.spi.preferences.Config;
     37import org.openstreetmap.josm.tools.Logging;
     38
     39/**
     40 * A styled render that does the rendering on a tile basis. Note: this is currently experimental!
     41 * It may be extracted to an interface at a later date.
     42 * @since xxx
     43 */
     44public final class StyledTiledMapRenderer extends StyledMapRenderer {
     45    // Render to the surrounding tiles for continuity -- this probably needs to be tweaked
     46    private static final int BUFFER_TILES = 2;
     47    // The number of extra pixels to render per tile (avoids black lines in render result)
     48    private static final int BUFFER_PIXELS = 16;
     49    private CacheAccess<TileZXY, ImageCache> cache;
     50    private int zoom;
     51    private Consumer<TileZXY> notifier;
     52
     53    /**
     54     * Constructs a new {@code StyledMapRenderer}.
     55     *
     56     * @param g              the graphics context. Must not be null.
     57     * @param nc             the map viewport. Must not be null.
     58     * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they
     59     *                       look inactive. Example: rendering of data in an inactive layer using light gray as color only.
     60     * @throws IllegalArgumentException if {@code g} is null
     61     * @throws IllegalArgumentException if {@code nc} is null
     62     */
     63    public StyledTiledMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) {
     64        super(g, nc, isInactiveMode);
     65    }
     66
     67    @Override
     68    public void render(OsmData<?, ?, ?, ?> data, boolean renderVirtualNodes, Bounds bounds) {
     69        // If there is no cache, fall back to old behavior
     70        if (this.cache == null) {
     71            super.render(data, renderVirtualNodes, bounds);
     72            return;
     73        }
     74        final Executor worker = MainApplication.worker;
     75        final BufferedImage tempImage;
     76        final Graphics2D tempG2d;
     77        // I'd like to avoid two image copies, but there are some issues using the original g2d object
     78        tempImage = nc.getGraphicsConfiguration().createCompatibleImage(this.nc.getWidth(), this.nc.getHeight(), Transparency.TRANSLUCENT);
     79        tempG2d = tempImage.createGraphics();
     80        tempG2d.setComposite(AlphaComposite.DstAtop); // Avoid tile lines in large areas
     81
     82        final List<TileZXY> toRender = TileZXY.boundsToTiles(bounds.getMinLat(), bounds.getMinLon(),
     83                bounds.getMaxLat(), bounds.getMaxLon(), zoom).collect(Collectors.toList());
     84        final Bounds box = new Bounds(bounds);
     85        toRender.stream().map(TileZXY::tileToBounds).forEach(box::extend);
     86        final int tileSize;
     87        if (toRender.isEmpty()) {
     88            tileSize = Config.getPref().getInt("mappaint.fast_render.tile_size", 256); // Mostly to keep the compiler happy
     89        } else {
     90            final TileZXY tile = toRender.get(0);
     91            final Bounds box2 = TileZXY.tileToBounds(tile);
     92            final Point min = this.nc.getPoint(box2.getMin());
     93            final Point max = this.nc.getPoint(box2.getMax());
     94            tileSize = max.x - min.x + BUFFER_PIXELS;
     95        }
     96
     97        // Sort the tiles based off of proximity to the mouse pointer
     98        if (nc instanceof MapView) { // Ideally this would either be an interface or a method in NavigableComponent
     99            final MapView mv = (MapView) nc;
     100            final MouseEvent mouseEvent = mv.lastMEvent;
     101            final LatLon mousePosition = nc.getLatLon(mouseEvent.getX(), mouseEvent.getY());
     102            final TileZXY mouseTile = TileZXY.latLonToTile(mousePosition.lat(), mousePosition.lon(), zoom);
     103            toRender.sort(Comparator.comparingInt(tile -> {
     104                final int x = tile.x() - mouseTile.x();
     105                final int y = tile.y() - mouseTile.y();
     106                return x * x + y * y;
     107            }));
     108        }
     109
     110        // We want to prioritize where the mouse is, but having some in the queue will reduce overall paint time
     111        int submittedTile = 5;
     112        int painted = 0;
     113        for (TileZXY tile : toRender) {
     114            final Image tileImage;
     115            // Needed to avoid having tiles that aren't rendered properly
     116            final ImageCache tImg = this.cache.get(tile);
     117            final boolean wasDirty = tImg != null && tImg.isDirty();
     118            if (tImg != null && !tImg.isDirty() && tImg.imageFuture() != null) {
     119                submittedTile = 0; // Don't submit new tiles if there are futures already in the queue. Not perfect.
     120            }
     121            if (submittedTile > 0 && (tImg == null || tImg.isDirty())) {
     122                // Ensure that we don't add a large number of render calls
     123                if (tImg != null && tImg.imageFuture() != null) {
     124                    tImg.imageFuture().cancel();
     125                }
     126                submittedTile--;
     127                // Note that the paint code is *not* thread safe, so all tiles must be painted on the same thread.
     128                // 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
     129                TileLoader loader = new TileLoader(data, tile, tileSize, new ArrayList<>());
     130                worker.execute(loader);
     131                if (tImg == null) {
     132                    this.cache.put(tile, new ImageCache(null, loader, false));
     133                } else {
     134                    // This might cause some extra renders, but *probably* ok
     135                    this.cache.put(tile, new ImageCache(tImg.image(), loader, true));
     136                }
     137                tileImage = tImg != null ? tImg.image() : null;
     138            } else if (tImg != null) {
     139                tileImage = tImg.image();
     140            } else {
     141                tileImage = null;
     142            }
     143            final Point point = this.nc.getPoint(tile);
     144            if (tileImage != null) {
     145                // FIXME move to isTraceEnabled prior to commit
     146                if ((wasDirty && Logging.isTraceEnabled()) || this.isInactiveMode) {
     147                    tempG2d.setColor(Color.DARK_GRAY);
     148                    tempG2d.fillRect(point.x, point.y, tileSize, tileSize);
     149                } else {
     150                    painted++;
     151                }
     152                // There seems to be an off-by-one error somewhere.
     153                tempG2d.drawImage(tileImage, point.x + 1, point.y + 1, null, null);
     154            } else {
     155                Logging.trace("StyledMapRenderer did not paint tile {1}", tile);
     156            }
     157        }
     158        // Force another render pass if there may be more tiles to render
     159        if (submittedTile <= 0) {
     160            worker.execute(nc::invalidate);
     161        }
     162        final double percentDrawn = 100 * painted / (double) toRender.size();
     163        if (percentDrawn < 99.99) {
     164            final int x = 0;
     165            final int y = nc.getHeight() / 8;
     166            final String message = tr("Rendering Status: {0}%", Math.floor(percentDrawn));
     167            tempG2d.setComposite(AlphaComposite.SrcOver);
     168            tempG2d.setFont(new Font("sansserif", Font.BOLD, 13));
     169            tempG2d.setColor(Color.BLACK);
     170            tempG2d.drawString(message, x + 1, y);
     171            tempG2d.setColor(Color.LIGHT_GRAY);
     172            tempG2d.drawString(message, x, y);
     173        }
     174        tempG2d.dispose();
     175        g.drawImage(tempImage, 0, 0, null);
     176    }
     177
     178    /**
     179     * Set the cache for this painter. If not set, this acts like {@link StyledMapRenderer}.
     180     * @param box The box we will be rendering -- any jobs for tiles outside of this box will be cancelled
     181     * @param cache The cache to use
     182     * @param zoom The zoom level to use for creating the tiles
     183     * @param notifier The method to call when a tile has been updated. This may or may not be called in the EDT.
     184     */
     185    public void setCache(Bounds box, CacheAccess<TileZXY, ImageCache> cache, int zoom, Consumer<TileZXY> notifier) {
     186        this.cache = cache;
     187        this.zoom = zoom;
     188        this.notifier = notifier != null ? notifier : tile -> { /* Do nothing */ };
     189
     190        Set<TileZXY> tiles = TileZXY.boundsToTiles(box.getMinLat(), box.getMinLon(), box.getMaxLat(), box.getMaxLon(), zoom)
     191                .collect(Collectors.toSet());
     192        cache.getMatching(".*").forEach((key, value) -> {
     193            if (!tiles.contains(key)) {
     194                cancelImageFuture(cache, key, value);
     195            }
     196        });
     197    }
     198
     199    /**
     200     * Cancel a job for a tile
     201     * @param cache The cache with the job
     202     * @param key The tile key
     203     * @param value The {@link ImageCache} to remove and cancel
     204     */
     205    private static void cancelImageFuture(CacheAccess<TileZXY, ImageCache> cache, TileZXY key, ImageCache value) {
     206        if (value.imageFuture() != null) {
     207            value.imageFuture().cancel();
     208            if (value.image() == null) {
     209                cache.remove(key);
     210            } else {
     211                cache.put(key, new ImageCache(value.image(), null, value.isDirty()));
     212            }
     213        }
     214    }
     215
     216    /**
     217     * Generate tile images
     218     * @param data The data to generate tiles from
     219     * @param tiles The collection of tiles to generate (note: there is currently a bug with multiple tiles)
     220     * @param tileSize The size of the tile image
     221     * @return The image for the tiles passed in
     222     */
     223    private BufferedImage generateTiles(OsmData<?, ?, ?, ?> data, Collection<TileZXY> tiles, int tileSize) {
     224        if (tiles.isEmpty()) {
     225            throw new IllegalArgumentException("tiles cannot be empty");
     226        }
     227        // We need to know how large of an area we are rendering; we get the min x/y and max x/y in order to get the
     228        // number of tiles in the x/y directions we are rendering.
     229        final IntSummaryStatistics xStats = tiles.stream().mapToInt(TileZXY::x).distinct().summaryStatistics();
     230        final IntSummaryStatistics yStats = tiles.stream().mapToInt(TileZXY::y).distinct().summaryStatistics();
     231        final int xCount = xStats.getMax() - xStats.getMin() + 1; // inclusive
     232        final int yCount = yStats.getMax() - yStats.getMin() + 1; // inclusive
     233        final int width = tileSize * (2 * BUFFER_TILES + xCount);
     234        final int height = tileSize * (2 * BUFFER_TILES + yCount);
     235        // getWidth and getHeight are called in the constructor; Java 22 will let us call super after we set variables.
     236        final NavigatableComponent temporaryView = new NavigatableComponent() {
     237            @Override
     238            public int getWidth() {
     239                return width;
     240            }
     241
     242            @Override
     243            public int getHeight() {
     244                return height;
     245            }
     246        };
     247        // These bounds are used to set the render area; it includes the buffer area.
     248        final Bounds bounds = generateRenderArea(tiles);
     249
     250        temporaryView.zoomTo(bounds.getCenter().getEastNorth(ProjectionRegistry.getProjection()), mapState.getScale());
     251        BufferedImage bufferedImage = Optional.ofNullable(nc.getGraphicsConfiguration())
     252                .map(gc -> gc.createCompatibleImage(tileSize * xCount + xCount, tileSize * yCount + xCount, Transparency.TRANSLUCENT))
     253                .orElseGet(() -> new BufferedImage(tileSize * xCount + xCount, tileSize * yCount + xCount, BufferedImage.TYPE_INT_ARGB));
     254        Graphics2D g2d = bufferedImage.createGraphics();
     255        try {
     256            g2d.setTransform(AffineTransform.getTranslateInstance(-BUFFER_TILES * (double) tileSize, -BUFFER_TILES * (double) tileSize));
     257            final AbstractMapRenderer tilePainter = MapRendererFactory.getInstance().createActiveRenderer(g2d, temporaryView, false);
     258            tilePainter.render(data, true, bounds);
     259        } finally {
     260            g2d.dispose();
     261        }
     262        return bufferedImage;
     263    }
     264
     265    /**
     266     * Generate the area for rendering
     267     * @param tiles The tiles that we want to render
     268     * @return The generated render area with {@link #BUFFER_TILES} on all sides.
     269     */
     270    private static Bounds generateRenderArea(Collection<TileZXY> tiles) {
     271        Bounds bounds = null;
     272        for (TileZXY tile : tiles) {
     273            if (bounds == null) {
     274                bounds = TileZXY.tileToBounds(tile);
     275            }
     276            bounds.extend(TileZXY.tileToBounds(new TileZXY(tile.zoom(), tile.x() - BUFFER_TILES, tile.y() - BUFFER_TILES)));
     277            bounds.extend(TileZXY.tileToBounds(new TileZXY(tile.zoom(), tile.x() + BUFFER_TILES, tile.y() + BUFFER_TILES)));
     278        }
     279        return Objects.requireNonNull(bounds);
     280    }
     281
     282    /**
     283     * A loader for tiles
     284     */
     285    class TileLoader implements Runnable {
     286        private final TileZXY tile;
     287        private final int tileSize;
     288        private final OsmData<?, ?, ?, ?> data;
     289        private boolean cancel;
     290        private final Collection<TileLoader> tileCollection;
     291        private boolean done;
     292
     293        /**
     294         * Create a new tile loader
     295         * @param data The data to use for painting
     296         * @param tile The tile this tile loader is for
     297         * @param tileSize The expected size of this tile
     298         * @param tileCollection The collection of tiles that this tile is being rendered with (for batching)
     299         */
     300        TileLoader(OsmData<?, ?, ?, ?> data, TileZXY tile, int tileSize, Collection<TileLoader> tileCollection) {
     301            this.data = data;
     302            this.tile = tile;
     303            this.tileSize = tileSize;
     304            this.tileCollection = tileCollection;
     305            this.tileCollection.add(this);
     306        }
     307
     308        @Override
     309        public void run() {
     310            if (!cancel) {
     311                synchronized (tileCollection) {
     312                    if (!done) {
     313                        final BufferedImage tImage = generateTiles(data,
     314                                tileCollection.stream().map(t -> t.tile).collect(Collectors.toList()), tileSize);
     315                        final int minX = tileCollection.stream().map(t -> t.tile).mapToInt(TileZXY::x).min().orElse(this.tile.x());
     316                        final int minY = tileCollection.stream().map(t -> t.tile).mapToInt(TileZXY::y).min().orElse(this.tile.y());
     317                        for (TileLoader loader : tileCollection) {
     318                            final TileZXY txy = loader.tile;
     319                            final int x = (txy.x() - minX) * (tileSize - BUFFER_PIXELS) + BUFFER_PIXELS / 2;
     320                            final int y = (txy.y() - minY) * (tileSize - BUFFER_PIXELS) + BUFFER_PIXELS / 2;
     321                            final int wh = tileSize - BUFFER_PIXELS / 2;
     322
     323                            final BufferedImage tileImage = tImage.getSubimage(x, y, wh, wh);
     324                            loader.cacheTile(tileImage);
     325                        }
     326                    }
     327                }
     328            }
     329        }
     330
     331        /**
     332         * Finish a tile generation job
     333         * @param tImage The tile image for this job
     334         */
     335        private synchronized void cacheTile(BufferedImage tImage) {
     336            cache.put(tile, new ImageCache(tImage, null, false));
     337            done = true;
     338            notifier.accept(tile);
     339        }
     340
     341        /**
     342         * Cancel this job without causing a {@link java.util.concurrent.CancellationException}
     343         */
     344        void cancel() {
     345            this.cancel = true;
     346        }
     347    }
     348}
  • 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;
     
    9195import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
    9296import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
    9397import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer;
     98import org.openstreetmap.josm.data.osm.visitor.paint.ImageCache;
    9499import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory;
     100import org.openstreetmap.josm.data.osm.visitor.paint.StyledTiledMapRenderer;
     101import org.openstreetmap.josm.data.osm.visitor.paint.TileZXY;
    95102import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
    96103import org.openstreetmap.josm.data.preferences.BooleanProperty;
    97104import org.openstreetmap.josm.data.preferences.IntegerProperty;
     
    104111import org.openstreetmap.josm.gui.MapFrame;
    105112import org.openstreetmap.josm.gui.MapView;
    106113import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
     114import org.openstreetmap.josm.gui.NavigatableComponent;
     115import org.openstreetmap.josm.gui.PrimitiveHoverListener;
    107116import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
    108117import org.openstreetmap.josm.gui.datatransfer.data.OsmLayerTransferData;
    109118import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
     
    144153 * @author imi
    145154 * @since 17
    146155 */
    147 public class OsmDataLayer extends AbstractOsmDataLayer implements Listener, DataSelectionListener, HighlightUpdateListener {
     156public class OsmDataLayer extends AbstractOsmDataLayer
     157        implements Listener, DataSelectionListener, HighlightUpdateListener, PrimitiveHoverListener {
     158    private static final int MAX_ZOOM = 30;
     159    private static final int OVER_ZOOM = 2;
    148160    private static final int HATCHED_SIZE = 15;
    149161    // U+2205 EMPTY SET
    150162    private static final String IS_EMPTY_SYMBOL = "\u2205";
     
    155167    private boolean requiresUploadToServer;
    156168    /** Flag used to know if the layer is being uploaded */
    157169    private final AtomicBoolean isUploadInProgress = new AtomicBoolean(false);
     170    /**
     171     * A cache used for painting
     172     */
     173    private final CacheAccess<TileZXY, ImageCache> cache = JCSCacheManager.getCache("osmDataLayer:" + System.identityHashCode(this));
     174    /** The map paint index that was painted (used to invalidate {@link #cache}) */
     175    private int lastDataIdx;
     176    /** The last zoom level (we invalidate all tiles when switching layers) */
     177    private int lastZoom;
     178    private boolean hoverListenerAdded;
    158179
    159180    /**
    160181     * List of validation errors in this layer.
     
    497518     * Draw nodes last to overlap the ways they belong to.
    498519     */
    499520    @Override public void paint(final Graphics2D g, final MapView mv, Bounds box) {
     521        if (!hoverListenerAdded) {
     522            MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
     523            hoverListenerAdded = true;
     524        }
    500525        boolean active = mv.getLayerManager().getActiveLayer() == this;
    501526        boolean inactive = !active && Config.getPref().getBoolean("draw.data.inactive_color", true);
    502527        boolean virtual = !inactive && mv.isVirtualNodesEnabled();
     
    537562            }
    538563        }
    539564
     565        // Used to invalidate cache
     566        int zoom = getZoom(mv);
     567        if (zoom != lastZoom) {
     568            // We just mark the previous zoom as dirty before moving in.
     569            // It means we don't have to traverse up/down z-levels marking tiles as dirty (this can get *very* expensive).
     570            this.cache.getMatching("TileZXY\\{" + lastZoom + "/.*")
     571                    .forEach((tile, imageCache) -> this.cache.put(tile, imageCache.becomeDirty()));
     572        }
     573        lastZoom = zoom;
    540574        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);
     575        if (!(painter instanceof StyledTiledMapRenderer) || zoom - OVER_ZOOM > Config.getPref().getInt("mappaint.fast_render.zlevel", 16)) {
     576            painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress()
     577                    || !PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get());
     578        } else {
     579            StyledTiledMapRenderer renderer = (StyledTiledMapRenderer) painter;
     580            renderer.setCache(box, this.cache, zoom, (tile) -> {
     581                /* This causes "bouncing". I'm not certain why.
     582                if (oldState.equalsInWindow(mv.getState())) { (oldstate = mv.getState())
     583                    final Point upperLeft = mv.getPoint(tile);
     584                    final Point lowerRight = mv.getPoint(new TileZXY(tile.zoom(), tile.x() + 1, tile.y() + 1));
     585                    GuiHelper.runInEDT(() -> mv.repaint(0, upperLeft.x, upperLeft.y, lowerRight.x - upperLeft.x, lowerRight.y - upperLeft.y));
     586                }
     587                 */
     588                // Invalidate doesn't trigger an instant repaint, but putting this off lets us batch the repaints needed for multiple tiles
     589                MainApplication.worker.submit(this::invalidate);
     590            });
     591
     592            if (this.data.getMappaintCacheIndex() != this.lastDataIdx) {
     593                this.cache.clear();
     594                this.lastDataIdx = this.data.getMappaintCacheIndex();
     595                Logging.trace("OsmDataLayer {0} paint cache cleared", this.getName());
     596            }
     597        }
     598        painter.render(this.data, virtual, box);
    544599        MainApplication.getMap().conflictDialog.paintConflicts(g, mv);
    545600    }
    546601
     
    11471202        validationErrors.clear();
    11481203        removeClipboardDataFor(this);
    11491204        recentRelations.clear();
     1205        if (hoverListenerAdded) {
     1206            hoverListenerAdded = false;
     1207            MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
     1208        }
    11501209    }
    11511210
    11521211    protected static void removeClipboardDataFor(OsmDataLayer osm) {
     
    11651224
    11661225    @Override
    11671226    public void processDatasetEvent(AbstractDatasetChangedEvent event) {
     1227        resetTiles(event.getPrimitives());
    11681228        invalidate();
    11691229        setRequiresSaveToFile(true);
    11701230        setRequiresUploadToServer(event.getDataset().requiresUploadToServer());
     
    11721232
    11731233    @Override
    11741234    public void selectionChanged(SelectionChangeEvent event) {
     1235        Set<IPrimitive> primitives = new HashSet<>(event.getAdded());
     1236        primitives.addAll(event.getRemoved());
     1237        resetTiles(primitives);
    11751238        invalidate();
    11761239    }
    11771240
     1241    private void resetTiles(Collection<? extends IPrimitive> primitives) {
     1242        if (primitives.size() == this.data.allPrimitives().size()) {
     1243            this.data.getDataSourceBounds().forEach(b -> resetBounds(b.getMinLat(), b.getMinLon(), b.getMaxLat(), b.getMaxLon()));
     1244            return;
     1245        }
     1246        // Most of the time, a selection is going to be a big box.
     1247        // So we want to optimize for that case.
     1248        BBox box = null;
     1249        for (IPrimitive primitive : primitives) {
     1250            if (primitive == null || primitive.getDataSet() != this.getDataSet()) continue;
     1251            final Collection<? extends IPrimitive> referrers = primitive.getReferrers();
     1252            if (box == null) {
     1253                box = new BBox(primitive.getBBox());
     1254            } else {
     1255                box.addPrimitive(primitive, 0);
     1256            }
     1257            for (IPrimitive referrer : referrers) {
     1258                box.addPrimitive(referrer, 0);
     1259            }
     1260        }
     1261        if (box != null) {
     1262            resetBounds(box.getMinLat(), box.getMinLon(), box.getMaxLat(), box.getMaxLon());
     1263        }
     1264    }
     1265
     1266    private void resetBounds(double minLat, double minLon, double maxLat, double maxLon) {
     1267        // Get the current zoom. Hopefully we aren't painting with a different navigatable component
     1268        final int currentZoom = lastZoom;
     1269        TileZXY.boundsToTiles(minLat, minLon, maxLat, maxLon, currentZoom, 1).forEach(tile -> {
     1270            final ImageCache imageCache = this.cache.get(tile);
     1271            if (imageCache != null && !imageCache.isDirty()) {
     1272                this.cache.put(tile, imageCache.becomeDirty());
     1273            }
     1274        });
     1275    }
     1276
     1277    /**
     1278     * Get the zoom for a {@link NavigatableComponent}
     1279     * @param navigatableComponent The component to get the zoom from
     1280     * @return The zoom for the navigatable component
     1281     */
     1282    private static int getZoom(NavigatableComponent navigatableComponent) {
     1283        final double scale = navigatableComponent.getScale();
     1284        // We might have to fall back to the old method if user is reprojecting
     1285        // 256 is the "target" size, (TODO check HiDPI!)
     1286        final int targetSize = Config.getPref().getInt("mappaint.fast_render.tile_size", 256);
     1287        final double topResolution = 2 * Math.PI * OsmMercator.EARTH_RADIUS / targetSize;
     1288        int zoom;
     1289        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)
     1290            if (scale > topResolution / Math.pow(2, zoom)) {
     1291                zoom = zoom > 0 ? zoom - 1 : zoom;
     1292                break;
     1293            }
     1294        }
     1295        // We paint at a few levels higher, note that the tiles are appropriately sized (if 256 is the "target" size, the tiles should be
     1296        // 64px square).
     1297        zoom += OVER_ZOOM;
     1298        return zoom;
     1299    }
     1300
    11781301    @Override
    11791302    public void projectionChanged(Projection oldValue, Projection newValue) {
    11801303         // No reprojection required. The dataset itself is registered as projection
     
    13071430        invalidate();
    13081431    }
    13091432
     1433    @Override
     1434    public void primitiveHovered(PrimitiveHoverEvent e) {
     1435        resetTiles(Arrays.asList(e.getHoveredPrimitive(), e.getPreviousPrimitive()));
     1436        this.invalidate();
     1437    }
     1438
    13101439    @Override
    13111440    public void setName(String name) {
    13121441        if (data != null) {
  • src/org/openstreetmap/josm/gui/MainMenu.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/MainMenu.java b/src/org/openstreetmap/josm/gui/MainMenu.java
    a b  
    103103import org.openstreetmap.josm.actions.SimplifyWayAction;
    104104import org.openstreetmap.josm.actions.SplitWayAction;
    105105import org.openstreetmap.josm.actions.TaggingPresetSearchAction;
     106import org.openstreetmap.josm.actions.TiledRenderToggleAction;
    106107import org.openstreetmap.josm.actions.UnGlueAction;
    107108import org.openstreetmap.josm.actions.UnJoinNodeWayAction;
    108109import org.openstreetmap.josm.actions.UndoAction;
     
    248249    /* View menu */
    249250    /** View / Wireframe View */
    250251    public final WireframeToggleAction wireFrameToggleAction = new WireframeToggleAction();
     252    /** View / Tiled Rendering */
     253    public final TiledRenderToggleAction tiledRenderToggleAction = new TiledRenderToggleAction();
    251254    /** View / Hatch area outside download */
    252255    public final DrawBoundariesOfDownloadedDataAction drawBoundariesOfDownloadedDataAction = new DrawBoundariesOfDownloadedDataAction();
    253256    /** View / Advanced info */
     
    799802        viewMenu.add(wireframe);
    800803        wireframe.setAccelerator(wireFrameToggleAction.getShortcut().getKeyStroke());
    801804        wireFrameToggleAction.addButtonModel(wireframe.getModel());
     805        // -- tiled render toggle action
     806        final JCheckBoxMenuItem tiledRender = new JCheckBoxMenuItem(tiledRenderToggleAction);
     807        viewMenu.add(tiledRender);
     808        tiledRenderToggleAction.addButtonModel(tiledRender.getModel());
     809        ExpertToggleAction.addVisibilitySwitcher(tiledRender);
     810        // -- hatch toggle action
    802811        final JCheckBoxMenuItem hatchAreaOutsideDownloadMenuItem = drawBoundariesOfDownloadedDataAction.getCheckbox();
    803812        viewMenu.add(hatchAreaOutsideDownloadMenuItem);
    804813        ExpertToggleAction.addVisibilitySwitcher(hatchAreaOutsideDownloadMenuItem);
  • 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  
    44import java.awt.AlphaComposite;
    55import java.awt.BasicStroke;
    66import java.awt.Color;
     7import java.awt.Component;
    78import java.awt.Dimension;
    89import java.awt.Graphics;
    910import java.awt.Graphics2D;
     11import java.awt.GraphicsEnvironment;
    1012import java.awt.Point;
    1113import java.awt.Rectangle;
    1214import java.awt.Shape;
    1315import java.awt.Stroke;
     16import java.awt.Transparency;
    1417import java.awt.event.ComponentAdapter;
    1518import java.awt.event.ComponentEvent;
    1619import java.awt.event.KeyEvent;
     
    328331        return Arrays.asList(zoomSlider, scaler);
    329332    }
    330333
     334    private static BufferedImage getAcceleratedImage(Component mv, int width, int height) {
     335        if (GraphicsEnvironment.isHeadless()) {
     336            return new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
     337        }
     338        return mv.getGraphicsConfiguration().createCompatibleImage(width, height, Transparency.OPAQUE);
     339    }
     340
    331341    // remebered geometry of the component
    332342    private Dimension oldSize;
    333343    private Point oldLoc;
     
    546556                && nonChangedLayers.equals(visibleLayers.subList(0, nonChangedLayers.size()));
    547557
    548558        if (null == offscreenBuffer || offscreenBuffer.getWidth() != width || offscreenBuffer.getHeight() != height) {
    549             offscreenBuffer = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
     559            offscreenBuffer = getAcceleratedImage(this, width, height);
    550560        }
    551561
    552562        if (!canUseBuffer || nonChangedLayersBuffer == null) {
    553563            if (null == nonChangedLayersBuffer
    554564                    || nonChangedLayersBuffer.getWidth() != width || nonChangedLayersBuffer.getHeight() != height) {
    555                 nonChangedLayersBuffer = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
     565                nonChangedLayersBuffer = getAcceleratedImage(this, width, height);
    556566            }
    557567            Graphics2D g2 = nonChangedLayersBuffer.createGraphics();
    558568            g2.setClip(scaledClip);