Ticket #21432: 21432.patch

File 21432.patch, 27.2 KB (added by taylor.smock, 3 years ago)
  • src/org/openstreetmap/josm/data/cache/BufferedImageCacheEntry.java

    diff --git a/src/org/openstreetmap/josm/data/cache/BufferedImageCacheEntry.java b/src/org/openstreetmap/josm/data/cache/BufferedImageCacheEntry.java
    index 2e637a12d1..41fb48b2a9 100644
    a b  
    22package org.openstreetmap.josm.data.cache;
    33
    44import java.awt.image.BufferedImage;
     5import java.awt.image.RenderedImage;
    56import java.io.ByteArrayInputStream;
    67import java.io.ByteArrayOutputStream;
    78import java.io.IOException;
    public class BufferedImageCacheEntry extends CacheEntry {  
    3738     * @return a cache entry for the PNG encoded image
    3839     * @throws UncheckedIOException if an I/O error occurs
    3940     */
    40     public static BufferedImageCacheEntry pngEncoded(BufferedImage img) {
     41    public static BufferedImageCacheEntry pngEncoded(RenderedImage img) {
    4142        try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
    4243            ImageIO.write(img, "png", output);
    4344            return new BufferedImageCacheEntry(output.toByteArray());
  • src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java
    index 418fcfd520..73d7e42daa 100644
    a b import java.awt.image.BufferedImage;  
    2121import java.io.IOException;
    2222import java.util.Objects;
    2323import java.util.concurrent.Future;
     24import java.util.concurrent.atomic.AtomicInteger;
    2425
    2526import javax.swing.JComponent;
    2627import javax.swing.SwingUtilities;
    import org.openstreetmap.josm.gui.MainApplication;  
    3435import org.openstreetmap.josm.gui.layer.AbstractMapViewPaintable;
    3536import org.openstreetmap.josm.gui.layer.geoimage.viewers.projections.IImageViewer;
    3637import org.openstreetmap.josm.gui.layer.geoimage.viewers.projections.ImageProjectionRegistry;
     38import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.IImageTiling;
    3739import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
    3840import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChangeListener;
    3941import org.openstreetmap.josm.gui.util.GuiHelper;
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    8789
    8890    private final ImgDisplayMouseListener imgMouseListener = new ImgDisplayMouseListener();
    8991
     92    private final AtomicInteger zoom = new AtomicInteger(12);
     93
    9094    private String emptyText;
    9195    private String osdText;
    9296
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    364368            if (rotation > 0) {
    365369                currentVisibleRect.width = (int) (currentVisibleRect.width * ZOOM_STEP.get());
    366370                currentVisibleRect.height = (int) (currentVisibleRect.height * ZOOM_STEP.get());
     371                ImageDisplay.this.zoom.decrementAndGet();
    367372            } else {
    368373                currentVisibleRect.width = (int) (currentVisibleRect.width / ZOOM_STEP.get());
    369374                currentVisibleRect.height = (int) (currentVisibleRect.height / ZOOM_STEP.get());
     375                ImageDisplay.this.zoom.incrementAndGet();
    370376            }
     377            // FIXME Remove logging
     378            Logging.error("Current zoom: {0}", ImageDisplay.this.zoom.get());
    371379
    372380            // Check that the zoom doesn't exceed MAX_ZOOM:1
    373381            ensureMaxZoom(currentVisibleRect);
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    719727            Rectangle r = new Rectangle(currentVisibleRect);
    720728            Rectangle target = calculateDrawImageRectangle(currentVisibleRect, size);
    721729
    722             currentImageViewer.paintImage(g, currentImage, target, r);
     730            if (currentEntry instanceof IImageTiling && ((IImageTiling) currentEntry).isTilingEnabled()) {
     731                currentImageViewer.paintTiledImage(g, (IImageTiling) currentEntry, target, r, zoom.get());
     732            } else {
     733                currentImageViewer.paintImage(g, currentImage, target, r);
     734            }
    723735            paintSelectedRect(g, target, currentVisibleRect, size);
    724736            if (currentErrorLoading && currentEntry != null) {
    725737                String loadingStr = tr("Error on file {0}", currentEntry.getDisplayName());
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    10171029        } else {
    10181030            rectangle.height = wFact / getSize().width;
    10191031        }
     1032
     1033        final IImageEntry<?> currentEntry;
     1034        synchronized (this) {
     1035            currentEntry = this.entry;
     1036        }
     1037        if (currentEntry instanceof IImageTiling) {
     1038            IImageTiling imageTiling = (IImageTiling) currentEntry;
     1039            if (this.zoom.get() > imageTiling.getMaxZoom()) {
     1040                this.zoom.set(imageTiling.getMaxZoom());
     1041            } else if (this.zoom.get() < imageTiling.getMinZoom()) {
     1042                this.zoom.set(imageTiling.getMinZoom());
     1043            }
     1044        }
    10201045    }
    10211046
    10221047    /**
  • src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java
    index d5a94886b1..888611fbd2 100644
    a b import static org.openstreetmap.josm.tools.I18n.tr;  
    66import java.awt.Dimension;
    77import java.awt.Graphics2D;
    88import java.awt.Image;
     9import java.awt.Rectangle;
    910import java.awt.geom.AffineTransform;
    1011import java.awt.image.BufferedImage;
    1112import java.io.File;
    import java.net.MalformedURLException;  
    1516import java.net.URL;
    1617import java.util.Collections;
    1718import java.util.Objects;
     19
    1820import javax.imageio.IIOParam;
    1921import javax.imageio.ImageReadParam;
    2022import javax.imageio.ImageReader;
    import javax.imageio.ImageReader;  
    2224import org.openstreetmap.josm.data.ImageData;
    2325import org.openstreetmap.josm.data.gpx.GpxImageEntry;
    2426import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
     27import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.IImageTiling;
    2528import org.openstreetmap.josm.tools.ExifReader;
    2629import org.openstreetmap.josm.tools.ImageProvider;
    2730import org.openstreetmap.josm.tools.Logging;
    import org.openstreetmap.josm.tools.Logging;  
    3033 * Stores info about each image, with an optional thumbnail
    3134 * @since 2662
    3235 */
    33 public class ImageEntry extends GpxImageEntry implements IImageEntry<ImageEntry> {
     36public class ImageEntry extends GpxImageEntry implements IImageEntry<ImageEntry>, IImageTiling {
    3437
    3538    private Image thumbnail;
    3639    private ImageData dataSet;
    public class ImageEntry extends GpxImageEntry implements IImageEntry<ImageEntry>  
    212215        return applyExifRotation(image);
    213216    }
    214217
     218    @Override
     219    public Image getTileImage(int zoom, int tileSize, int column, int row) {
     220        if (column < 0 || row < 0 || zoom > this.getMaxZoom() || this.getWidth(zoom) < column * tileSize || this.getHeight(zoom) < row * tileSize) {
     221            return null;
     222        }
     223        final URL imageUrl;
     224        final BufferedImage image;
     225        try {
     226            imageUrl = getImageUrl();
     227            Logging.info(tr("Loading {0} at {1}/{2}/{3} with size {4}", imageUrl, zoom, column, row, tileSize));
     228            image = ImageProvider.read(imageUrl, true, false,
     229                    r -> this.withSubsampling(r, tileSize, zoom, column, row));
     230        } catch (IOException e) {
     231            Logging.error(e);
     232            return null;
     233        }
     234
     235        if (image == null) {
     236            Logging.warn("Unable to load {0}", imageUrl);
     237        }
     238        // applyExifRotation not used here since it will not work with tiled images
     239        // Instead, we will have to rotate the column/row, and then apply rotation here.
     240        return image;
     241    }
     242
    215243    protected URL getImageUrl() throws MalformedURLException {
    216244        return getFile().toURI().toURL();
    217245    }
    218246
     247    private ImageReadParam withSubsampling(ImageReader reader, int tileSize, int zoom, int column, int row) {
     248        Rectangle tile = IImageTiling.super.getTileDimension(zoom, column, row, tileSize);
     249        ImageReadParam param = reader.getDefaultReadParam();
     250        param.setSourceRegion(tile);
     251        int subsampling = (int) Math.floor(Math.max(Math.pow(IImageTiling.super.getScale(zoom), -1), 1));
     252        param.setSourceSubsampling(subsampling, subsampling, 0, 0);
     253        return param;
     254    }
     255
    219256    private ImageReadParam withSubsampling(ImageReader reader, Dimension target) {
    220257        try {
    221258            ImageReadParam param = reader.getDefaultReadParam();
  • src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/IImageViewer.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/IImageViewer.java b/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/IImageViewer.java
    index 3c1d41e534..583bd53f97 100644
    a b import java.util.Set;  
    1212
    1313import org.openstreetmap.josm.data.imagery.street_level.Projections;
    1414import org.openstreetmap.josm.gui.layer.geoimage.ImageDisplay;
     15import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.IImageTiling;
    1516import org.openstreetmap.josm.gui.util.imagery.Vector3D;
    1617
    1718/**
    public interface IImageViewer extends ComponentListener {  
    3435     */
    3536    void paintImage(Graphics g, BufferedImage image, Rectangle target, Rectangle visibleRect);
    3637
     38
     39    /**
     40     * Paint the image tile
     41     * @param g The graphics to paint on
     42     * @param entry The image entry (specifically, with the tile size)
     43     * @param tile The tile to paint (x, y, z)
     44     * @param image The image to paint
     45     */
     46    default void paintImageTile(Graphics g, Rectangle target, Rectangle visibleRect, IImageTiling entry, IImageTiling.ImageTile tile, Image image) {
     47        final Rectangle toUse = visibleRect;
     48        g.drawImage(image, -toUse.x + entry.getTileSize() * tile.getXIndex(), -toUse.y + entry.getTileSize() * tile.getYIndex(), null);
     49    }
     50
     51    /**
     52     * Paint the image
     53     * @param g The graphics to paint on
     54     * @param imageEntry The image to paint
     55     * @param target The target area
     56     * @param visibleRect The visible rectangle
     57     * @param zoom The zoom level
     58     */
     59    default void paintTiledImage(Graphics g, IImageTiling imageEntry, Rectangle target, Rectangle visibleRect, int zoom) {
     60        imageEntry.getTiles(zoom, visibleRect).forEach(pair -> this.paintImageTile(g, target, visibleRect, imageEntry, pair.a, pair.b));
     61    }
     62
    3763    /**
    3864     * Get the default visible rectangle for the projection
    3965     * @param component The component the image will be displayed in
  • new file src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/IImageTiling.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/IImageTiling.java b/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/IImageTiling.java
    new file mode 100644
    index 0000000000..7413517fbc
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling;
     3
     4import java.awt.Image;
     5import java.awt.Rectangle;
     6import java.awt.image.RenderedImage;
     7import java.io.IOException;
     8import java.text.MessageFormat;
     9import java.util.function.IntUnaryOperator;
     10import java.util.stream.IntStream;
     11import java.util.stream.Stream;
     12
     13import org.apache.commons.jcs3.access.CacheAccess;
     14import org.openstreetmap.gui.jmapviewer.TileXY;
     15import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
     16import org.openstreetmap.josm.data.cache.JCSCacheManager;
     17import org.openstreetmap.josm.spi.preferences.Config;
     18import org.openstreetmap.josm.tools.Logging;
     19import org.openstreetmap.josm.tools.Pair;
     20
     21/**
     22 * An interface for tiled images. Primarily used to reduce memory usage in large images.
     23 * @author Taylor Smock
     24 * @since xxx
     25 */
     26public interface IImageTiling {
     27    /**
     28     * Just a class to hold tile information
     29     */
     30    class ImageTile extends TileXY {
     31        final int zoom;
     32        /**
     33         * Returns an instance of an image tile.
     34         * @param x number of the tile
     35         * @param y number of the tile
     36         * @param z The zoom level
     37         */
     38        public ImageTile(int x, int y, int z) {
     39            super(x, y);
     40            this.zoom = z;
     41        }
     42    }
     43    /**
     44     * The default tile size for the image tiles -- each tile takes 1024 px * 1024 px * 4 bytes = 4 MiB max
     45     * A 4k image (4160x3120) has (Math.ceil(4160/1024) * Math.ceil(3120/1024) = 20 tiles). Some tiles are almost empty.
     46     * This gives a reasonable number of tiles for most image sizes.
     47     */
     48    int DEFAULT_TILE_SIZE = 1024;
     49
     50    /** A good default minimum zoom (the image size is {@link #DEFAULT_TILE_SIZE} max, at 1024 it is 5) */
     51    int DEFAULT_MIN_ZOOM = (int) Math.round(Math.log(Math.sqrt(DEFAULT_TILE_SIZE))/Math.log(2));
     52
     53    /** A cache for images */
     54    CacheAccess<String, BufferedImageCacheEntry> IMAGE_CACHE = JCSCacheManager.getCache("iimagetiling", 100, 1_000, Config.getDirs().getCacheDirectory(true).getAbsolutePath());
     55
     56    /**
     57     * Get the size of the image at a specified zoom level
     58     * @param zoom The zoom level. Zoom 0 == 1 px for the image. Zoom 1 == 4 px for the image.
     59     * @return The number of pixels (max, for a square image)
     60     */
     61    static long getSizeAtZoom(final int zoom) {
     62        final long dimension = 1L << zoom;
     63        return dimension * dimension;
     64    }
     65
     66    /**
     67     * Get the default tile size.
     68     * @return The tile size to use
     69     */
     70    default int getDefaultTileSize() {
     71        return DEFAULT_TILE_SIZE;
     72    }
     73
     74    /**
     75     * Get the tile size.
     76     * @return The tile size to use
     77     */
     78    default int getTileSize() {
     79        return this.getDefaultTileSize();
     80    }
     81
     82    /**
     83     * Get the maximum zoom that the image supports
     84     * Feel free to override and cache the result for performance reasons.
     85     *
     86     * @return The maximum zoom of the image
     87     */
     88    default int getMaxZoom() {
     89        final int maxSize = Math.max(this.getWidth(), this.getHeight());
     90        return (int) Math.round(Math.ceil(Math.log(maxSize) / Math.log(2)));
     91    }
     92
     93    /**
     94     * Get the minimum zoom that the image supports or makes sense
     95     * @return The minimum zoom that makes sense
     96     */
     97    default int getMinZoom() {
     98        final IntUnaryOperator minZoom = input -> Math.toIntExact(Math.round(Math.floor(this.getMaxZoom() + Math.log((double) this.getTileSize() / input) / Math.log(2))));
     99        return Math.min(minZoom.applyAsInt(this.getWidth()), minZoom.applyAsInt(this.getHeight()));
     100    }
     101
     102    /**
     103     * Get the current scale of the image
     104     * @param zoom The zoom level
     105     * @return The scaling of the image at the specified level
     106     */
     107    default double getScale(final int zoom) {
     108        return Math.pow(2, (double) zoom - this.getMaxZoom());
     109    }
     110
     111    /**
     112     * Get the width of the image
     113     * @return The width of the image
     114     */
     115    int getWidth();
     116
     117    /**
     118     * Get the width of the image at a specified scale
     119     * @param zoom The zoom to use
     120     * @return The width at the specified scale
     121     */
     122    default int getWidth(final int zoom) {
     123        return Math.toIntExact(Math.round(this.getScale(zoom) * this.getWidth()));
     124    }
     125
     126    /**
     127     * Get the height of the image
     128     * @return The height of the image
     129     */
     130    int getHeight();
     131
     132    /**
     133     * Get the height of the image at a specified scale
     134     * @param zoom The zoom to use
     135     * @return The height at the specified scale
     136     */
     137    default int getHeight(final int zoom) {
     138        return Math.toIntExact(Math.round(this.getScale(zoom) * this.getHeight()));
     139    }
     140
     141    /**
     142     * Get the number of rows at a specified zoom level
     143     * @param zoom The zoom level
     144     * @return The number of rows
     145     */
     146    default int getRows(final int zoom) {
     147        return this.getRows(zoom, this.getTileSize());
     148    }
     149
     150    /**
     151     * Get the number of rows at a specified zoom level
     152     * @param zoom The zoom level
     153     * @param tileSize The tile size
     154     * @return The number of rows
     155     */
     156    default int getRows(final int zoom, final int tileSize) {
     157        final int height = this.getHeight(zoom);
     158        return Math.toIntExact(Math.round(Math.ceil(height / (double) tileSize)));
     159    }
     160
     161    /**
     162     * Get the number of columns at a specified zoom level
     163     * @param zoom The zoom level
     164     * @return The number of columns
     165     */
     166    default int getColumns(final int zoom) {
     167        return this.getColumns(zoom, this.getTileSize());
     168    }
     169
     170    /**
     171     * Get the number of columns at a specified zoom level
     172     * @param zoom The zoom level
     173     * @param tileSize The tile size
     174     * @return The number of columns
     175     */
     176    default int getColumns(final int zoom, final int tileSize) {
     177        final int width = this.getWidth(zoom);
     178        return Math.toIntExact(Math.round(Math.ceil(width / (double) tileSize)));
     179    }
     180
     181    /**
     182     * Get the image to show for a specific tile location. This should be cached by the implementation in most cases.
     183     * Top-left corner is 0,0
     184     * @param zoom The zoom to use
     185     * @param tileSize The tile size to use
     186     * @param column The column to get (x)
     187     * @param row The row to get (y)
     188     * @return The image to display (not padded). May be {@code null}.
     189     */
     190    Image getTileImage(int zoom, int tileSize, int column, int row);
     191
     192    /**
     193     * Get the image to show for a specific tile location with the default tile size
     194     * Top-left corner is 0,0
     195     * @param zoom The zoom to use
     196     * @param column The column to get (x)
     197     * @param row The row to get (y)
     198     * @return The image to display (not padded). May be {@code null}.
     199     */
     200    default Image getTileImage(final int zoom, final int column, final int row) {
     201        final String storage = MessageFormat.format("{0}: {1}/{2}/{3}", this, zoom, column, row);
     202        BufferedImageCacheEntry image = IMAGE_CACHE.get(storage);
     203        if (image == null) {
     204            Image newImage = this.getTileImage(zoom, this.getTileSize(), column, row);
     205            if (newImage instanceof RenderedImage) {
     206                IMAGE_CACHE.put(storage, BufferedImageCacheEntry.pngEncoded((RenderedImage) newImage));
     207            }
     208            return newImage;
     209        }
     210        try {
     211            return image.getImage();
     212        } catch (IOException e) {
     213            Logging.error(e);
     214        }
     215        return null;
     216    }
     217
     218    /**
     219     * Get the subsection of the image to show
     220     * Top-left corner is 0,0
     221     * @param zoom The zoom to use
     222     * @param column The column to get (x)
     223     * @param row The row to get (y)
     224     * @return The subsection of the image to get
     225     */
     226    default Rectangle getTileDimension(final int zoom, final int column, final int row) {
     227        return this.getTileDimension(zoom, column, row, this.getTileSize());
     228    }
     229
     230    /**
     231     * Get the subsection of the image to show
     232     * Top-left corner is 0,0
     233     * @param zoom The zoom to use
     234     * @param column The column to get (x)
     235     * @param row The row to get (y)
     236     * @param tileSize the tile size to use
     237     * @return The subsection of the image to get
     238     */
     239    default Rectangle getTileDimension(final int zoom, final int column, final int row, final int tileSize) {
     240        final double scale = this.getScale(zoom); // e.g., 1, 1/2, 1/4, etc.
     241        final int x = Math.toIntExact(Math.round(Math.floor(column * tileSize / scale)));
     242        final int y = Math.toIntExact(Math.round(Math.floor(row * tileSize / scale)));
     243        return new Rectangle(x, y, (int) (tileSize / scale), (int) (tileSize / scale));
     244    }
     245
     246    /**
     247     * Get the tiles for a zoom level given a visible rectangle
     248     * @param zoom The zoom to get
     249     * @param visibleRect The rectangle to get
     250     * @return A stream of tiles to images
     251     */
     252    default Stream<Pair<ImageTile, Image>> getTiles(int zoom, Rectangle visibleRect) {
     253        final double scale = this.getScale(zoom);
     254        final int startX = Math.toIntExact(Math.round(Math.floor(visibleRect.getMinX() * scale / this.getTileSize())));
     255        final int startY = Math.toIntExact(Math.round(Math.floor(visibleRect.getMinY() * scale / this.getTileSize())));
     256        final int endX = Math.toIntExact(Math.round(Math.ceil(visibleRect.getMaxX() * scale / this.getTileSize())));
     257        final int endY = Math.toIntExact(Math.round(Math.ceil(visibleRect.getMaxY() * scale / this.getTileSize())));
     258        Logging.error("x [{0} - {1}], y[{2} - {3}]", startX, endX, startY, endY);
     259        return IntStream.rangeClosed(startX, endX).mapToObj(x -> IntStream.rangeClosed(startY, endY).mapToObj(y -> new ImageTile(x, y, zoom)))
     260                .flatMap(stream -> stream).map(tile -> new Pair<>(tile, this.getTileImage(tile.zoom, tile.getXIndex(), tile.getYIndex())));
     261    }
     262
     263    /**
     264     * Check if tiling is enabled for this object.
     265     *
     266     * @return {@code true} if tiling should be u sed
     267     */
     268    default boolean isTilingEnabled() {
     269        return true;
     270    }
     271}
  • new file test/unit/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/IImageTilingTest.java

    diff --git a/test/unit/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/IImageTilingTest.java b/test/unit/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/IImageTilingTest.java
    new file mode 100644
    index 0000000000..0ad1096efc
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling;
     3
     4import static org.junit.jupiter.api.Assertions.assertAll;
     5import static org.junit.jupiter.api.Assertions.assertEquals;
     6import static org.junit.jupiter.api.Assertions.assertNotEquals;
     7import static org.junit.jupiter.api.Assertions.assertNotNull;
     8import static org.junit.jupiter.api.Assertions.assertTrue;
     9
     10import java.awt.Image;
     11import java.awt.image.BufferedImage;
     12import java.util.concurrent.atomic.AtomicInteger;
     13import java.util.stream.Stream;
     14
     15import org.junit.jupiter.params.ParameterizedTest;
     16import org.junit.jupiter.params.provider.Arguments;
     17import org.junit.jupiter.params.provider.MethodSource;
     18import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
     19
     20/**
     21 * Test class for {@link IImageTiling}
     22 * @author Taylor Smock
     23 */
     24@BasicPreferences
     25class IImageTilingTest {
     26    static Stream<Arguments> testSizeAtZoom() {
     27        return Stream.of(Arguments.of(0, 1L), Arguments.of(1, 4L));
     28    }
     29
     30    @ParameterizedTest
     31    @MethodSource
     32    void testSizeAtZoom(int zoom, long expected) {
     33        assertEquals(expected, IImageTiling.getSizeAtZoom(zoom));
     34    }
     35
     36    static Stream<Arguments> getImageTilingSamples() {
     37        return Stream.of(
     38                Arguments.of(new ImageTiling(new BufferedImage(5000, 2500, BufferedImage.TYPE_INT_ARGB)), 13)
     39        );
     40    }
     41
     42    @ParameterizedTest
     43    @MethodSource("getImageTilingSamples")
     44    void testGetTileSizes(final ImageTiling imageTiling) {
     45        // The fake class uses default methods
     46        assertEquals(imageTiling.getTileSize(), imageTiling.getDefaultTileSize());
     47    }
     48
     49    @ParameterizedTest
     50    @MethodSource("getImageTilingSamples")
     51    void testGetMaxZoom(final ImageTiling imageTiling, final int maxZoom) {
     52        assertEquals(maxZoom, imageTiling.getMaxZoom());
     53    }
     54
     55    @ParameterizedTest
     56    @MethodSource("getImageTilingSamples")
     57    void testGetScale(final ImageTiling imageTiling, final int maxZoom) {
     58        assertEquals(1, imageTiling.getScale(maxZoom));
     59        assertEquals(0.5, imageTiling.getScale(maxZoom - 1));
     60        assertEquals(0.25, imageTiling.getScale(maxZoom - 2));
     61    }
     62
     63    @ParameterizedTest
     64    @MethodSource("getImageTilingSamples")
     65    void testGetWidth(final IImageTiling imageTiling, final int maxZoom) {
     66        assertEquals(imageTiling.getWidth(), imageTiling.getWidth(maxZoom));
     67        assertEquals(imageTiling.getWidth() / 2, imageTiling.getWidth(maxZoom - 1));
     68        assertEquals(imageTiling.getWidth() / 4, imageTiling.getWidth(maxZoom - 2));
     69    }
     70
     71    @ParameterizedTest
     72    @MethodSource("getImageTilingSamples")
     73    void testGetHeight(final IImageTiling imageTiling, final int maxZoom) {
     74        assertEquals(imageTiling.getHeight(), imageTiling.getHeight(maxZoom));
     75        assertEquals(imageTiling.getHeight() / 2, imageTiling.getHeight(maxZoom - 1));
     76        assertEquals(imageTiling.getHeight() / 4, imageTiling.getHeight(maxZoom - 2));
     77    }
     78
     79    @ParameterizedTest
     80    @MethodSource("getImageTilingSamples")
     81    void testGetRows(final IImageTiling imageTiling, final int maxZoom) {
     82        assertEquals(3, imageTiling.getRows(maxZoom));
     83        assertEquals(2, imageTiling.getRows(maxZoom - 1));
     84    }
     85
     86    @ParameterizedTest
     87    @MethodSource("getImageTilingSamples")
     88    void testGetColumns(final IImageTiling imageTiling, final int maxZoom) {
     89        assertEquals(5, imageTiling.getColumns(maxZoom));
     90        assertEquals(3, imageTiling.getColumns(maxZoom - 1));
     91    }
     92
     93    @ParameterizedTest
     94    @MethodSource("getImageTilingSamples")
     95    void testGetTileImage(final IImageTiling imageTiling, final int maxZoom) {
     96        assertNotNull(imageTiling.getTileImage(maxZoom, 0, 0));
     97        final Image cornerImage = imageTiling.getTileImage(maxZoom, imageTiling.getColumns(maxZoom) - 1, imageTiling.getRows(maxZoom) - 1);
     98        assertAll(() -> assertNotEquals(-1, cornerImage.getWidth(null)),
     99                () -> assertNotEquals(-1, cornerImage.getHeight(null)),
     100                () -> assertTrue(imageTiling.getTileSize() > cornerImage.getWidth(null)),
     101                () -> assertTrue(imageTiling.getTileSize() > cornerImage.getHeight(null)));
     102    }
     103
     104    @ParameterizedTest
     105    @MethodSource("getImageTilingSamples")
     106    void testGetTileDimension(final IImageTiling imageTiling) {
     107        imageTiling.getTileDimension(0, 0, 0);
     108    }
     109
     110    private static class ImageTiling implements IImageTiling {
     111        private final int width;
     112        private final int height;
     113        private final Image image;
     114        final AtomicInteger counter = new AtomicInteger(0);
     115        ImageTiling(final Image image) {
     116            this.image = image;
     117            this.width = image.getWidth(null);
     118            this.height = image.getHeight(null);
     119        }
     120
     121        @Override
     122        public int getWidth() {
     123            return this.width;
     124        }
     125
     126        @Override
     127        public int getHeight() {
     128            return this.height;
     129        }
     130
     131        @Override
     132        public Image getTileImage(int zoom, int tileSize, int column, int row) {
     133            this.counter.incrementAndGet();
     134            if (image instanceof BufferedImage) {
     135                final BufferedImage bufferedImage = (BufferedImage) image;
     136                return bufferedImage.getSubimage(column * tileSize, row * tileSize,
     137                        Math.min(tileSize, bufferedImage.getWidth() - column * tileSize - 1),
     138                        Math.min(tileSize, bufferedImage.getHeight() - row * tileSize - 1));
     139            }
     140            throw new UnsupportedOperationException("The test ImageTiling class only supports BufferedImages");
     141        }
     142    }
     143}