Ticket #21432: 21432.3.patch

File 21432.3.patch, 87.1 KB (added by taylor.smock, 4 years ago)

Load tiles in non-EDT thread. Still kind of jerky, but a lot better. Zoom still broken.

  • 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/data/cache/JCSCachedTileLoaderJob.java

    diff --git a/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
    index f8603239a9..7512641aa9 100644
    a b public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements  
    9999    private boolean force;
    100100    private final long minimumExpiryTime;
    101101
     102    /**
     103     * Get the deduplication string for this job
     104     * @return The string used for deduplication
     105     * @throws IOException See {@link #getUrl()}
     106     */
     107    private String getDeduplicationString() throws IOException {
     108        // getCacheKey is useful for where the same url might return different items
     109        return this.getUrl().toString() + '/' + getCacheKey();
     110    }
     111
    102112    /**
    103113     * @param cache cache instance that we will work on
    104114     * @param options options of the request
    public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements  
    150160        String deduplicationKey = null;
    151161        if (url != null) {
    152162            // url might be null, for example when Bing Attribution is not loaded yet
    153             deduplicationKey = url.toString();
     163            deduplicationKey = this.getDeduplicationString();
    154164        }
    155165        if (deduplicationKey == null) {
    156166            Logging.warn("No url returned for: {0}, skipping", getCacheKey());
    public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements  
    252262    private void finishLoading(LoadResult result) {
    253263        Set<ICachedLoaderListener> listeners;
    254264        try {
    255             listeners = inProgress.remove(getUrl().toString());
     265            listeners = inProgress.remove(this.getDeduplicationString());
    256266        } catch (IOException e) {
    257267            listeners = null;
    258268            Logging.trace(e);
    public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements  
    319329        if (!file.exists()) {
    320330            file = new File(fileName.substring("file://".length() - 1));
    321331        }
    322         try (InputStream fileInputStream = Files.newInputStream(file.toPath())) {
    323             cacheData = createCacheEntry(Utils.readBytesFromStream(fileInputStream));
     332        try {
     333            cacheData = createCacheEntry(this.loadObjectBytes(file));
    324334            cache.put(getCacheKey(), cacheData, attributes);
    325335            return true;
    326336        } catch (IOException e) {
    public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements  
    331341        return false;
    332342    }
    333343
     344    /**
     345     * Load bytes from a file. This is overridable to allow for sparse loading of bytes
     346     * @param file The file to load
     347     * @return The file bytes
     348     * @throws IOException If there is an issue reading the file
     349     */
     350    protected byte[] loadObjectBytes(File file) throws IOException {
     351        try (InputStream fileInputStream = Files.newInputStream(file.toPath())) {
     352            return Utils.readBytesFromStream(fileInputStream);
     353        }
     354    }
     355
    334356    /**
    335357     * @return true if object was successfully downloaded via http, false, if there was a loading failure
    336358     */
  • 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..1e2c8c9d1c 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  
    213217        }
    214218
    215219        public void checkRectPos() {
     220            this.checkRectPos(null, 0);
     221        }
     222
     223        /**
     224         * Ensure that the rectangle is within bounds
     225         * @param imageEntry The current image entry -- if it is a tiling entry, different constraints are needed
     226         * @param zoom The current zoom level (only used if tiling)
     227         */
     228        public void checkRectPos(final IImageEntry<?> imageEntry, final int zoom) {
     229            final int useWidth;
     230            final int useHeight;
     231            if (imageEntry instanceof IImageTiling) {
     232                useHeight = ((IImageTiling<?>) imageEntry).getHeight(zoom);
     233                useWidth = ((IImageTiling<?>) imageEntry).getWidth(zoom);
     234            } else {
     235                useWidth = init.width;
     236                useHeight = init.height;
     237            }
    216238            if (x < 0) {
    217239                x = 0;
    218240            }
    219241            if (y < 0) {
    220242                y = 0;
    221243            }
    222             if (x + width > init.width) {
    223                 x = init.width - width;
     244            if (width > useWidth) {
     245                width = useWidth;
     246            }
     247            if (height > useHeight) {
     248                height = useHeight;
    224249            }
    225             if (y + height > init.height) {
    226                 y = init.height - height;
     250            if (x + width > useWidth) {
     251                x = useWidth - width;
     252            }
     253            if (y + height > useHeight) {
     254                y = useHeight - height;
    227255            }
    228256        }
    229257
    230258        public void checkRectSize() {
    231             if (width > init.width) {
    232                 width = init.width;
     259            this.checkRectSize(null, 0);
     260        }
     261
     262        /**
     263         * Ensure that the rectangle is the appropriate size
     264         * @param imageEntry The current image entry -- if it is a tiling entry, different constraints are needed
     265         * @param zoom The current zoom level (only used if tiling)
     266         */
     267        public void checkRectSize(final IImageEntry<?> imageEntry, final int zoom) {
     268            final int useWidth;
     269            final int useHeight;
     270            if (imageEntry instanceof IImageTiling) {
     271                useWidth = ((IImageTiling<?>) imageEntry).getWidth(zoom);
     272                useHeight = ((IImageTiling<?>) imageEntry).getHeight(zoom);
     273            } else {
     274                useWidth = init.width;
     275                useHeight = init.height;
     276            }
     277            if (width > useWidth) {
     278                width = useWidth;
    233279            }
    234             if (height > init.height) {
    235                 height = init.height;
     280            if (height > useHeight) {
     281                height = useHeight;
    236282            }
    237283        }
    238284
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    289335                    }
    290336                }
    291337
    292                 int width = img.getWidth();
    293                 int height = img.getHeight();
    294                 entry.setWidth(width);
    295                 entry.setHeight(height);
     338                // Only set width/height if the entry is not something that can be tiled
     339                // Tiling *requires* knowledge of the actual width/height of the image.
     340                if (!(entry instanceof IImageTiling)) {
     341                    int width = img.getWidth();
     342                    int height = img.getHeight();
     343                    entry.setWidth(width);
     344                    entry.setHeight(height);
     345                }
    296346
    297347                synchronized (ImageDisplay.this) {
    298348                    if (this.entry != ImageDisplay.this.entry) {
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    301351                    }
    302352
    303353                    ImageDisplay.this.image = img;
    304                     updateProcessedImage();
    305354                    // This will clear the loading info box
    306355                    ImageDisplay.this.oldEntry = ImageDisplay.this.entry;
    307                     visibleRect = getIImageViewer(entry).getDefaultVisibleRectangle(ImageDisplay.this, image);
     356                    visibleRect = getIImageViewer(entry).getDefaultVisibleRectangle(ImageDisplay.this, image, this.entry);
     357                    // Update the visible rectangle
     358                    ImageDisplay.this.updateVisibleRectangle();
     359                    // Update the processed image *after* updating the visible rect -- otherwise, we may try to load a large image fully (tiled)
     360                    ImageDisplay.this.updateProcessedImage();
    308361
    309362                    selectedRect = null;
    310363                    errorLoading = false;
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    358411
    359412            // Calculate the mouse cursor position in image coordinates to center the zoom.
    360413            if (refreshMousePointInImg)
    361                 mousePointInImg = comp2imgCoord(currentVisibleRect, x, y, getSize());
     414                mousePointInImg = comp2imgCoord(currentEntry, currentVisibleRect, x, y, getSize(), zoom.get());
    362415
    363416            // Apply the zoom to the visible rectangle in image coordinates
     417            final int zoom;
    364418            if (rotation > 0) {
    365419                currentVisibleRect.width = (int) (currentVisibleRect.width * ZOOM_STEP.get());
    366420                currentVisibleRect.height = (int) (currentVisibleRect.height * ZOOM_STEP.get());
     421                zoom = ImageDisplay.this.zoom.decrementAndGet();
    367422            } else {
    368423                currentVisibleRect.width = (int) (currentVisibleRect.width / ZOOM_STEP.get());
    369424                currentVisibleRect.height = (int) (currentVisibleRect.height / ZOOM_STEP.get());
     425                zoom = ImageDisplay.this.zoom.incrementAndGet();
    370426            }
    371427
    372428            // Check that the zoom doesn't exceed MAX_ZOOM:1
    373429            ensureMaxZoom(currentVisibleRect);
    374430
    375431            // The size of the visible rectangle is limited by the image size or the viewer implementation.
     432            // It can also be influenced by whether or not the current entry allows tiling. Tiling image implementations
     433            // don't care what the size of the image buffer is. In fact, the image buffer can be the size of the window.
     434            // So the image buffer really only defines the scale.
    376435            if (imageViewer != null) {
    377                 imageViewer.checkAndModifyVisibleRectSize(currentImage, currentVisibleRect);
     436                imageViewer.checkAndModifyVisibleRectSize(currentImage, currentEntry, currentVisibleRect);
    378437            } else {
    379                 currentVisibleRect.checkRectSize();
     438                currentVisibleRect.checkRectSize(currentEntry, zoom);
    380439            }
    381440
    382441            // Set the position of the visible rectangle, so that the mouse cursor doesn't move on the image.
    383             Rectangle drawRect = calculateDrawImageRectangle(currentVisibleRect, getSize());
     442            final Rectangle drawRect;
     443            if (currentEntry instanceof IImageTiling) {
     444                final byte multiplyBy = rotation > 0 ? (byte) -2 : (byte) 2;
     445                drawRect = new VisRect(currentVisibleRect.x * multiplyBy, currentVisibleRect.y * multiplyBy,
     446                        currentVisibleRect.width * multiplyBy, currentVisibleRect.height * multiplyBy);
     447            } else {
     448                drawRect = calculateDrawImageRectangle(currentVisibleRect, getSize());
     449            }
    384450            currentVisibleRect.x = mousePointInImg.x + ((drawRect.x - x) * currentVisibleRect.width) / drawRect.width;
    385451            currentVisibleRect.y = mousePointInImg.y + ((drawRect.y - y) * currentVisibleRect.height) / drawRect.height;
    386452
    387453            // The position is also limited by the image size
    388             currentVisibleRect.checkRectPos();
     454            currentVisibleRect.checkRectPos(currentEntry, zoom);
    389455
    390456            synchronized (ImageDisplay.this) {
    391457                if (ImageDisplay.this.entry == currentEntry) {
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    447513            }
    448514
    449515            // Calculate the translation to set the clicked point the center of the view.
    450             Point click = comp2imgCoord(currentVisibleRect, e.getX(), e.getY(), getSize());
     516            Point click = comp2imgCoord(currentEntry, currentVisibleRect, e.getX(), e.getY(), getSize(), zoom.get());
    451517            Point center = getCenterImgCoord(currentVisibleRect);
    452518
    453519            currentVisibleRect.x += click.x - center.x;
    454520            currentVisibleRect.y += click.y - center.y;
    455521
    456             currentVisibleRect.checkRectPos();
     522            currentVisibleRect.checkRectPos(currentEntry, ImageDisplay.this.zoom.get());
    457523
    458524            synchronized (ImageDisplay.this) {
    459525                if (ImageDisplay.this.entry == currentEntry) {
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    467533         * a picture part) */
    468534        @Override
    469535        public void mousePressed(MouseEvent e) {
    470             Image currentImage;
    471             VisRect currentVisibleRect;
     536            final Image currentImage;
     537            final VisRect currentVisibleRect;
     538            final IImageEntry<?> imageEntry;
    472539
    473540            synchronized (ImageDisplay.this) {
    474541                currentImage = ImageDisplay.this.image;
    475542                currentVisibleRect = ImageDisplay.this.visibleRect;
     543                imageEntry = ImageDisplay.this.entry;
    476544            }
    477545
    478546            if (currentImage == null)
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    481549            selectedRect = null;
    482550
    483551            if (mouseIsDragging(e) || mouseIsZoomSelecting(e))
    484                 mousePointInImg = comp2imgCoord(currentVisibleRect, e.getX(), e.getY(), getSize());
     552                mousePointInImg = comp2imgCoord(imageEntry, currentVisibleRect, e.getX(), e.getY(), getSize(), zoom.get());
    485553        }
    486554
    487555        @Override
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    503571                return;
    504572
    505573            if (mouseIsDragging(e) && mousePointInImg != null) {
    506                 Point p = comp2imgCoord(currentVisibleRect, e.getX(), e.getY(), getSize());
     574                Point p = comp2imgCoord(imageEntry, currentVisibleRect, e.getX(), e.getY(), getSize(), zoom.get());
    507575                getIImageViewer(entry).mouseDragged(this.mousePointInImg, p, currentVisibleRect);
    508                 currentVisibleRect.checkRectPos();
     576                currentVisibleRect.checkRectPos(imageEntry, ImageDisplay.this.zoom.get());
    509577                synchronized (ImageDisplay.this) {
    510578                    if (ImageDisplay.this.entry == imageEntry) {
    511579                        ImageDisplay.this.visibleRect = currentVisibleRect;
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    514582                // We have to update the mousePointInImg for 360 image panning, as otherwise the panning never stops.
    515583                // This does not work well with the perspective viewer at this time (2021-08-26).
    516584                boolean is360panning = entry != null && Projections.EQUIRECTANGULAR == entry.getProjectionType();
    517                 if (is360panning) {
     585                if (is360panning || entry instanceof IImageTiling) {
    518586                    this.mousePointInImg = p;
    519587                }
    520588                ImageDisplay.this.repaint();
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    525593            }
    526594
    527595            if (mouseIsZoomSelecting(e) && mousePointInImg != null) {
    528                 Point p = comp2imgCoord(currentVisibleRect, e.getX(), e.getY(), getSize());
     596                Point p = comp2imgCoord(imageEntry, currentVisibleRect, e.getX(), e.getY(), getSize(), zoom.get());
    529597                currentVisibleRect.checkPointInside(p);
    530598                VisRect selectedRectTemp = new VisRect(
    531599                        Math.min(p.x, mousePointInImg.x),
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    533601                        p.x < mousePointInImg.x ? mousePointInImg.x - p.x : p.x - mousePointInImg.x,
    534602                        p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y,
    535603                        currentVisibleRect);
    536                 selectedRectTemp.checkRectSize();
    537                 selectedRectTemp.checkRectPos();
     604                selectedRectTemp.checkRectSize(imageEntry, zoom.get());
     605                selectedRectTemp.checkRectPos(imageEntry, zoom.get());
    538606                ImageDisplay.this.selectedRect = selectedRectTemp;
    539607                ImageDisplay.this.repaint();
    540608            }
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    574642                    selectedRect.y -= (selectedRect.height - oldHeight) / 2;
    575643                }
    576644
    577                 selectedRect.checkRectSize();
    578                 selectedRect.checkRectPos();
     645                selectedRect.checkRectSize(currentEntry, zoom.get());
     646                selectedRect.checkRectPos(currentEntry, zoom.get());
    579647            }
    580648
    581649            synchronized (ImageDisplay.this) {
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    642710        synchronized (this) {
    643711            this.oldEntry = this.entry;
    644712            this.entry = entry;
     713            if (entry instanceof IImageTiling) {
     714                this.zoom.set(((IImageTiling<?>) entry).getMinZoom() + 1);
     715            }
    645716            if (entry == null) {
    646717                image = null;
    647718                updateProcessedImage();
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    719790            Rectangle r = new Rectangle(currentVisibleRect);
    720791            Rectangle target = calculateDrawImageRectangle(currentVisibleRect, size);
    721792
    722             currentImageViewer.paintImage(g, currentImage, target, r);
     793            if (currentEntry instanceof IImageTiling && ((IImageTiling<?>) currentEntry).isTilingEnabled()) {
     794                currentImageViewer.paintTiledImage(g, (IImageTiling<?>) currentEntry, target, r, zoom.get(), this);
     795            } else {
     796                currentImageViewer.paintImage(g, currentImage, target, r);
     797            }
    723798            paintSelectedRect(g, target, currentVisibleRect, size);
    724799            if (currentErrorLoading && currentEntry != null) {
    725800                String loadingStr = tr("Error on file {0}", currentEntry.getDisplayName());
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    826901     */
    827902    private void paintSelectedRect(Graphics g, Rectangle target, VisRect visibleRectTemp, Dimension size) {
    828903        if (selectedRect != null) {
    829             Point topLeft = img2compCoord(visibleRectTemp, selectedRect.x, selectedRect.y, size);
    830             Point bottomRight = img2compCoord(visibleRectTemp,
     904            Point topLeft = img2compCoord(entry, visibleRectTemp, selectedRect.x, selectedRect.y, size, zoom.get());
     905            Point bottomRight = img2compCoord(entry, visibleRectTemp,
    831906                    selectedRect.x + selectedRect.width,
    832                     selectedRect.y + selectedRect.height, size);
     907                    selectedRect.y + selectedRect.height, size, zoom.get());
    833908            g.setColor(new Color(128, 128, 128, 180));
    834909            g.fillRect(target.x, target.y, target.width, topLeft.y - target.y);
    835910            g.fillRect(target.x, target.y, topLeft.x - target.x, target.height);
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    840915        }
    841916    }
    842917
    843     static Point img2compCoord(VisRect visibleRect, int xImg, int yImg, Dimension compSize) {
    844         Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize);
     918    /**
     919     * Convert an image coordinate to a component coordinate
     920     * @param imageEntry The image entry -- only used if tiling
     921     * @param visibleRect The visible rectangle
     922     * @param xImg The x position in the component
     923     * @param yImg The y position in the component
     924     * @param compSize The component size
     925     * @param zoom The current zoom level
     926     * @return The point in the image
     927     */
     928    static Point img2compCoord(IImageEntry<?> imageEntry, VisRect visibleRect, int xImg, int yImg, Dimension compSize, int zoom) {
     929        final Rectangle drawRect;
     930        if (imageEntry instanceof IImageTiling) {
     931            drawRect = visibleRect;
     932        } else {
     933            drawRect = calculateDrawImageRectangle(visibleRect, compSize);
     934        }
    845935        return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) / visibleRect.width,
    846936                drawRect.y + ((yImg - visibleRect.y) * drawRect.height) / visibleRect.height);
    847937    }
    848938
    849     static Point comp2imgCoord(VisRect visibleRect, int xComp, int yComp, Dimension compSize) {
    850         Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize);
     939    /**
     940     * Convert a component coordinate to an image coordinate
     941     * @param imageEntry The image entry -- only used if tiling
     942     * @param visibleRect The visible rectangle
     943     * @param xComp The x position in the component
     944     * @param yComp The y position in the component
     945     * @param compSize The component size
     946     * @param zoom The current zoom level
     947     * @return The point in the image
     948     */
     949    static Point comp2imgCoord(IImageEntry<?> imageEntry, VisRect visibleRect, int xComp, int yComp, Dimension compSize, int zoom) {
     950        final Rectangle drawRect;
     951        if (imageEntry instanceof IImageTiling) {
     952            drawRect = visibleRect;
     953        } else {
     954            drawRect = calculateDrawImageRectangle(visibleRect, compSize);
     955        }
    851956        Point p = new Point(
    852957                        ((xComp - drawRect.x) * visibleRect.width),
    853958                        ((yComp - drawRect.y) * visibleRect.height));
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    9441049            Point center = getCenterImgCoord(currentVisibleRect);
    9451050            currentVisibleRect.setBounds(center.x - getWidth() / 2, center.y - getHeight() / 2,
    9461051                    getWidth(), getHeight());
    947             currentVisibleRect.checkRectSize();
    948             currentVisibleRect.checkRectPos();
     1052            currentVisibleRect.checkRectSize(currentEntry, this.zoom.get());
     1053            currentVisibleRect.checkRectPos(currentEntry, this.zoom.get());
    9491054        }
    9501055
    9511056        synchronized (this) {
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    10091114            rectangle.height = (int) (getSize().height / MAX_ZOOM.get());
    10101115        }
    10111116
    1012         // Set the same ratio for the visible rectangle and the display area
    1013         int hFact = rectangle.height * getSize().width;
    1014         int wFact = rectangle.width * getSize().height;
    1015         if (hFact > wFact) {
    1016             rectangle.width = hFact / getSize().height;
     1117
     1118        final IImageEntry<?> currentEntry;
     1119        synchronized (this) {
     1120            currentEntry = this.entry;
     1121        }
     1122        if (currentEntry instanceof IImageTiling) {
     1123            IImageTiling<?> imageTiling = (IImageTiling<?>) currentEntry;
     1124            if (this.zoom.get() > imageTiling.getMaxZoom()) {
     1125                this.zoom.set(imageTiling.getMaxZoom());
     1126            } else if (this.zoom.get() < imageTiling.getMinZoom()) {
     1127                this.zoom.set(imageTiling.getMinZoom());
     1128            }
    10171129        } else {
    1018             rectangle.height = wFact / getSize().width;
     1130            // Set the same ratio for the visible rectangle and the display area
     1131            int hFact = rectangle.height * getSize().width;
     1132            int wFact = rectangle.width * getSize().height;
     1133            if (hFact > wFact) {
     1134                rectangle.width = hFact / getSize().height;
     1135            } else {
     1136                rectangle.height = wFact / getSize().width;
     1137            }
    10191138        }
    10201139    }
    10211140
    public class ImageDisplay extends JComponent implements Destroyable, PreferenceC  
    10271146    public void updateVisibleRectangle() {
    10281147        final VisRect currentVisibleRect;
    10291148        final Image mouseImage;
    1030         final IImageViewer iImageViewer;
     1149        final IImageViewer currentImageViewer;
     1150        final IImageEntry<?> imageEntry;
    10311151        synchronized (this) {
    10321152            currentVisibleRect = this.visibleRect;
    10331153            mouseImage = this.image;
    1034             iImageViewer = this.getIImageViewer(this.entry);
     1154            imageEntry = this.entry;
     1155            currentImageViewer = this.getIImageViewer(imageEntry);
    10351156        }
    1036         if (mouseImage != null && currentVisibleRect != null && iImageViewer != null) {
    1037             final Image maxImageSize = iImageViewer.getMaxImageSize(this, mouseImage);
     1157        if (mouseImage != null && currentVisibleRect != null && currentImageViewer != null) {
     1158            final Image maxImageSize = currentImageViewer.getMaxImageSize(this, mouseImage);
    10381159            final VisRect maxVisibleRect = new VisRect(0, 0, maxImageSize.getWidth(null), maxImageSize.getHeight(null));
    10391160            maxVisibleRect.setRect(currentVisibleRect);
    10401161            ensureMaxZoom(maxVisibleRect);
    10411162
    1042             maxVisibleRect.checkRectSize();
     1163            maxVisibleRect.checkRectSize(imageEntry, this.zoom.get());
    10431164            synchronized (this) {
    10441165                this.visibleRect = maxVisibleRect;
    10451166            }
  • 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 7ea6371f62..4170d07a04 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.DeepTileSet;
     28import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.IImageTiling;
    2529import org.openstreetmap.josm.tools.ExifReader;
    2630import org.openstreetmap.josm.tools.ImageProvider;
    2731import org.openstreetmap.josm.tools.Logging;
    import org.openstreetmap.josm.tools.Utils;  
    3135 * Stores info about each image, with an optional thumbnail
    3236 * @since 2662
    3337 */
    34 public class ImageEntry extends GpxImageEntry implements IImageEntry<ImageEntry> {
     38public class ImageEntry extends GpxImageEntry implements IImageEntry<ImageEntry>, IImageTiling<BufferedImage> {
    3539
    3640    private Image thumbnail;
    3741    private ImageData dataSet;
     42    private DeepTileSet deepTileSet;
    3843
    3944    /**
    4045     * Constructs a new {@code ImageEntry}.
    public class ImageEntry extends GpxImageEntry implements IImageEntry<ImageEntry>  
    5156        super(other);
    5257        thumbnail = other.thumbnail;
    5358        dataSet = other.dataSet;
     59        this.deepTileSet = other.deepTileSet;
    5460    }
    5561
    5662    /**
    public class ImageEntry extends GpxImageEntry implements IImageEntry<ImageEntry>  
    223229        return applyExifRotation(image);
    224230    }
    225231
     232    @Override
     233    public BufferedImage getTileImage(int zoom, int tileSize, int column, int row) {
     234        final Rectangle tile = IImageTiling.super.getTileDimension(zoom, column, row, tileSize);
     235        if (column < 0 || row < 0 || zoom > this.getMaxZoom() || tile.getWidth() <= 0 || tile.getHeight() <= 0) {
     236            return null;
     237        }
     238        final URL imageUrl;
     239        final BufferedImage image;
     240        try {
     241            imageUrl = getImageUrl();
     242            Logging.info(tr("Loading {0} at {1}/{2}/{3} with size {4}", imageUrl, zoom, column, row, tileSize));
     243            image = ImageProvider.read(imageUrl, true, false,
     244                    r -> this.withSubsampling(r, tile, zoom));
     245        } catch (IOException e) {
     246            Logging.error(e);
     247            return null;
     248        }
     249
     250        if (image == null) {
     251            Logging.warn("Unable to load {0}", imageUrl);
     252        }
     253        // applyExifRotation not used here since it will not work with tiled images
     254        // Instead, we will have to rotate the column/row, and then apply rotation here.
     255        return image;
     256    }
     257
    226258    protected URL getImageUrl() throws MalformedURLException {
    227259        return getFile().toURI().toURL();
    228260    }
    229261
     262    private ImageReadParam withSubsampling(ImageReader reader, final Rectangle tile, int zoom) {
     263        ImageReadParam param = reader.getDefaultReadParam();
     264        param.setSourceRegion(tile);
     265        int subsampling = (int) Math.floor(Math.max(Math.pow(IImageTiling.super.getScale(zoom), -1), 1));
     266        param.setSourceSubsampling(subsampling, subsampling, 0, 0);
     267        return param;
     268    }
     269
    230270    private ImageReadParam withSubsampling(ImageReader reader, Dimension target) {
    231271        try {
    232272            ImageReadParam param = reader.getDefaultReadParam();
    public class ImageEntry extends GpxImageEntry implements IImageEntry<ImageEntry>  
    258298        g.dispose();
    259299        return rotated;
    260300    }
     301
     302    @Override
     303    public DeepTileSet getDeepTileSet() {
     304        if (this.deepTileSet == null) {
     305            this.deepTileSet = new DeepTileSet(this.getMinZoom(), this.getMaxZoom(), this);
     306        }
     307        return this.deepTileSet;
     308    }
     309
     310    @Override
     311    public boolean isTilingEnabled() {
     312        // Flipped images are going to need more work (the column/rows will need to be translated)
     313        return IImageTiling.super.isTilingEnabled() && !ExifReader.orientationNeedsCorrection(getExifOrientation());
     314    }
    261315}
  • src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/Equirectangular.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/Equirectangular.java b/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/Equirectangular.java
    index f76b252344..0a5467342b 100644
    a b import java.awt.image.BufferedImage;  
    1212import java.util.Collections;
    1313import java.util.Set;
    1414
     15import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
    1516import org.openstreetmap.josm.data.imagery.street_level.Projections;
    1617import org.openstreetmap.josm.gui.layer.geoimage.ImageDisplay;
     18import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.GeoImageTileLoader;
     19import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.IImageTiling;
    1720import org.openstreetmap.josm.gui.util.GuiHelper;
    1821import org.openstreetmap.josm.gui.util.imagery.CameraPlane;
    1922import org.openstreetmap.josm.gui.util.imagery.Vector3D;
    import org.openstreetmap.josm.gui.util.imagery.Vector3D;  
    2427 * @since 18246
    2528 */
    2629public class Equirectangular extends ComponentAdapter implements IImageViewer {
     30    private final GeoImageTileLoader tileLoader = new GeoImageTileLoader(null, IImageTiling.IMAGE_CACHE);
    2731    private volatile CameraPlane cameraPlane;
    2832    private volatile BufferedImage offscreenImage;
    2933
    public class Equirectangular extends ComponentAdapter implements IImageViewer {  
    108112    public Image getMaxImageSize(ImageDisplay imageDisplay, Image image) {
    109113        return this.offscreenImage;
    110114    }
     115
     116    @Override
     117    public TileLoader getTileLoader() {
     118        return this.tileLoader;
     119    }
    111120}
  • 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..c9edb55163 100644
    a b import java.awt.Point;  
    88import java.awt.Rectangle;
    99import java.awt.event.ComponentListener;
    1010import java.awt.image.BufferedImage;
     11import java.util.List;
    1112import java.util.Set;
     13import java.util.Timer;
     14import java.util.TimerTask;
     15import java.util.function.Predicate;
     16import java.util.stream.Collectors;
    1217
     18import org.openstreetmap.gui.jmapviewer.Tile;
     19import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
     20import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     21import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
    1322import org.openstreetmap.josm.data.imagery.street_level.Projections;
    1423import org.openstreetmap.josm.gui.layer.geoimage.ImageDisplay;
     24import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.GeoImageTileLoader;
     25import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.IImageTiling;
     26import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.TileSet;
    1527import org.openstreetmap.josm.gui.util.imagery.Vector3D;
    1628
    1729/**
    import org.openstreetmap.josm.gui.util.imagery.Vector3D;  
    1931 * @since 18246
    2032 */
    2133public interface IImageViewer extends ComponentListener {
     34    /**
     35     * A class for {@link IImageViewer}. Probably shouldn't be used elsewhere.
     36     */
     37    class ImageTimerTask {
     38        /** A timer so that we aren't doing many repaints in a short time frame */
     39        static final Timer REPAINT_TIMER = new Timer("IImageViewerTimer");
     40        /** The current timer task */
     41        static TimerTask timerTask;
     42        private ImageTimerTask() {
     43            // Hide constructor
     44        }
     45    }
     46
    2247    /**
    2348     * Get the supported projections for the image viewer
    2449     * @return The projections supported. Typically, only one.
    public interface IImageViewer extends ComponentListener {  
    3459     */
    3560    void paintImage(Graphics g, BufferedImage image, Rectangle target, Rectangle visibleRect);
    3661
     62    /**
     63     * Paint the image tile
     64     * @param g The graphics to paint on
     65     * @param visibleRect The visible area
     66     * @param entry The image entry (specifically, with the tile size)
     67     * @param tile The tile to paint (x, y, z)
     68     * @param zoom The current zoom
     69     */
     70    default void paintImageTile(final Graphics g, final Rectangle visibleRect, final IImageTiling<?> entry, final Tile tile, final int zoom) {
     71        final int tileSize = entry.getTileSize();
     72        final Image image;
     73        int xPositionInImage = tileSize * tile.getXtile();
     74        int yPositionInImage = tileSize * tile.getYtile();
     75        if (zoom == tile.getZoom()) {
     76            image = tile.getImage();
     77        } else {
     78            // When the zooms are not the same, we have to scale the image appropriately
     79            final double scalingFactor = Math.pow(2, (double) zoom - tile.getZoom());
     80            yPositionInImage *= scalingFactor;
     81            xPositionInImage *= scalingFactor;
     82            final Image tileImage = tile.getImage();
     83            image = tileImage.getScaledInstance((int) (tileImage.getWidth(null) * scalingFactor),
     84                    (int) (tileImage.getHeight(null) * scalingFactor), Image.SCALE_DEFAULT);
     85        }
     86        final int x = xPositionInImage - visibleRect.x;
     87        final int y = yPositionInImage - visibleRect.y;
     88        g.drawImage(image, x, y, null);
     89    }
     90
     91    /**
     92     * Paint the image
     93     * @param g The graphics to paint on
     94     * @param imageEntry The image to paint
     95     * @param target The target area
     96     * @param visibleRect The visible rectangle
     97     * @param zoom The zoom level
     98     * @param component The component to repaint when tiles are loaded
     99     */
     100    default void paintTiledImage(Graphics g, IImageTiling<?> imageEntry, Rectangle target, Rectangle visibleRect,
     101            int zoom, Component component) {
     102        if (this.getTileLoader() instanceof GeoImageTileLoader) {
     103            ((GeoImageTileLoader) this.getTileLoader()).listener = (tile, success) -> updateRepaintTimer(component);
     104        }
     105        final Predicate<Tile> paintableTile = tile -> tile.isLoaded() && !tile.hasError();
     106        final Predicate<Tile> missingTile = tile -> !tile.isLoaded() && !tile.isLoading();
     107        final List<Tile> tiles = imageEntry.getTiles(zoom, visibleRect).collect(Collectors.toList());
     108        final List<Tile> missed = tiles.stream().filter(missingTile).collect(Collectors.toList());
     109        // Attempt to paint tiles at a lower zoom that are already loaded.
     110        if (zoom > imageEntry.getMinZoom()) {
     111            final List<Tile> superToPaint = missed.stream().map(tile -> imageEntry.getCoveringTileRange(tile, zoom - 1))
     112                    .distinct().flatMap(TileSet::allTiles).distinct().collect(Collectors.toList());
     113            // Paint lower resolution filler first
     114            superToPaint.forEach(tile -> tiles.add(0, tile));
     115            // Add any unloaded lower resolution tiles to the missed tiles. We are probably going to use them as the
     116            // user pans around.
     117            superToPaint.stream().filter(missingTile).forEach(missed::add);
     118        }
     119        // Attempt to paint tiles at a higher zoom that are already loaded
     120        if (zoom < imageEntry.getMaxZoom()) {
     121            final List<Tile> subsetToPaint = missed.stream().map(tile -> imageEntry.getCoveringTileRange(tile, zoom + 1))
     122                    .distinct().flatMap(TileSet::allTiles).distinct().collect(Collectors.toList());
     123            // Paint higher resolution filler last
     124            tiles.addAll(subsetToPaint);
     125        }
     126        // Paint the tiles that are loaded on this layer
     127        tiles.stream().filter(paintableTile).forEach(tile -> this.paintImageTile(g, visibleRect, imageEntry, tile, zoom));
     128        // Start loading tiles that have yet to be loaded.
     129        missed.stream().map(this.getTileLoader()::createTileLoaderJob).forEach(TileJob::submit);
     130    }
     131
     132    /**
     133     * Update the common repaint timer
     134     * @param component The component to update
     135     */
     136    static void updateRepaintTimer(Component component) {
     137        synchronized (ImageTimerTask.REPAINT_TIMER) {
     138            if (ImageTimerTask.timerTask != null) {
     139                ImageTimerTask.timerTask.cancel();
     140            }
     141            ImageTimerTask.timerTask = new TimerTask() {
     142                @Override
     143                public void run() {
     144                    component.repaint();
     145                }
     146            };
     147            ImageTimerTask.REPAINT_TIMER.schedule(ImageTimerTask.timerTask, 100);
     148        }
     149    }
     150
     151    /**
     152     * Get the tile loader for this image
     153     * @return The tile loader.
     154     */
     155    TileLoader getTileLoader();
     156
    37157    /**
    38158     * Get the default visible rectangle for the projection
    39159     * @param component The component the image will be displayed in
    public interface IImageViewer extends ComponentListener {  
    42162     */
    43163    ImageDisplay.VisRect getDefaultVisibleRectangle(Component component, Image image);
    44164
     165    /**
     166     * Get the default visible rectangle for the projection and entry
     167     * @param component The component the image will be displayed in
     168     * @param image The image that will be shown
     169     * @param entry The entry that will be used
     170     * @return The default visible rectangle
     171     */
     172    default ImageDisplay.VisRect getDefaultVisibleRectangle(Component component, Image image, IImageEntry<?> entry) {
     173        return this.getDefaultVisibleRectangle(component, image);
     174    }
     175
    45176    /**
    46177     * Get the current rotation in the image viewer
    47178     * @return The rotation
    public interface IImageViewer extends ComponentListener {  
    77208        }
    78209    }
    79210
     211    /**
     212     * Check and modify the visible rect size to appropriate dimensions
     213     * @param visibleRect the visible rectangle to update
     214     * @param entry the entry to use for checking
     215     * @param image The image to use for checking
     216     */
     217    default void checkAndModifyVisibleRectSize(Image image, IImageEntry<?> entry, ImageDisplay.VisRect visibleRect) {
     218        if (entry instanceof IImageTiling) {
     219            final IImageTiling<?> tiling = (IImageTiling<?>) entry;
     220            if (visibleRect.width > tiling.getWidth()) {
     221                visibleRect.width = tiling.getWidth();
     222            }
     223            if (visibleRect.height > tiling.getHeight()) {
     224                visibleRect.height = tiling.getHeight();
     225            }
     226            if (visibleRect.x + visibleRect.width > tiling.getWidth()) {
     227                visibleRect.x = tiling.getWidth() - visibleRect.width;
     228            }
     229            if (visibleRect.y + visibleRect.height > tiling.getHeight()) {
     230                visibleRect.y = tiling.getHeight() - visibleRect.height;
     231            }
     232            if (visibleRect.x < 0) {
     233                visibleRect.x = 0;
     234            }
     235            if (visibleRect.y < 0) {
     236                visibleRect.y = 0;
     237            }
     238        } else {
     239            this.checkAndModifyVisibleRectSize(image, visibleRect);
     240        }
     241    }
     242
    80243    /**
    81244     * Get the maximum image size that can be displayed
    82245     * @param imageDisplay The image display
  • src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/Perspective.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/Perspective.java b/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/Perspective.java
    index d570c53dde..b2a721dae1 100644
    a b import java.awt.image.BufferedImage;  
    1010import java.util.EnumSet;
    1111import java.util.Set;
    1212
     13import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     14import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
    1315import org.openstreetmap.josm.data.imagery.street_level.Projections;
    1416import org.openstreetmap.josm.gui.layer.geoimage.ImageDisplay;
     17import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.GeoImageTileLoader;
     18import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.IImageTiling;
    1519
    1620/**
    1721 * The default perspective image viewer class.
    import org.openstreetmap.josm.gui.layer.geoimage.ImageDisplay;  
    1923 * @since 18246
    2024 */
    2125public class Perspective extends ComponentAdapter implements IImageViewer {
    22 
     26    private final GeoImageTileLoader tileLoader = new GeoImageTileLoader(null, IImageTiling.IMAGE_CACHE);
    2327    @Override
    2428    public Set<Projections> getSupportedProjections() {
    2529        return EnumSet.of(Projections.PERSPECTIVE);
    public class Perspective extends ComponentAdapter implements IImageViewer {  
    3236                r.x, r.y, r.x + r.width, r.y + r.height, null);
    3337    }
    3438
     39    @Override
     40    public TileLoader getTileLoader() {
     41        return this.tileLoader;
     42    }
     43
    3544    @Override
    3645    public ImageDisplay.VisRect getDefaultVisibleRectangle(Component component, Image image) {
    3746        return new ImageDisplay.VisRect(0, 0, image.getWidth(null), image.getHeight(null));
    3847    }
     48
     49    @Override
     50    public ImageDisplay.VisRect getDefaultVisibleRectangle(Component component, Image image, IImageEntry<?> entry) {
     51        if (entry instanceof IImageTiling) {
     52            return new ImageDisplay.VisRect(0, 0, ((IImageTiling) entry).getWidth(), ((IImageTiling) entry).getHeight());
     53        }
     54        return IImageViewer.super.getDefaultVisibleRectangle(component, image, entry);
     55    }
    3956}
  • new file src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/DeepTileSet.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/DeepTileSet.java b/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/DeepTileSet.java
    new file mode 100644
    index 0000000000..8532f936a3
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling;
     3
     4import java.util.Arrays;
     5import java.util.Collection;
     6import java.util.Iterator;
     7import java.util.List;
     8import java.util.Objects;
     9import java.util.Set;
     10import java.util.stream.Collectors;
     11import java.util.stream.Stream;
     12
     13import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
     14import org.openstreetmap.gui.jmapviewer.TileXY;
     15import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
     16
     17/**
     18 * A collection of caching tile sets
     19 * @author Taylor Smock
     20 * @since xxx
     21 */
     22public class DeepTileSet implements Set<TileSet> {
     23    private final TileCache memoryTileCache = new MemoryTileCache();
     24
     25    private final int minZoom;
     26    private final int maxZoom;
     27    private final TileSet[] tileSets;
     28    private final TileSet nullTileSet = new TileSet();
     29    private final IImageTiling<?> imageTiling;
     30
     31    public DeepTileSet(final int minZoom, final int maxZoom, final IImageTiling<?> imageTiling) {
     32        this.minZoom = minZoom;
     33        this.maxZoom = maxZoom;
     34        if (minZoom > maxZoom) {
     35            throw new IllegalArgumentException(minZoom + " > " + maxZoom);
     36        }
     37        this.tileSets = new TileSet[maxZoom - minZoom + 1];
     38        this.imageTiling = imageTiling;
     39    }
     40
     41    public TileSet getTileSet(int zoom) {
     42        if (zoom < minZoom) {
     43            return nullTileSet;
     44        } else if (zoom > maxZoom) {
     45            zoom = maxZoom;
     46        }
     47        synchronized (tileSets) {
     48            TileSet ts = tileSets[zoom-minZoom];
     49            if (ts == null) {
     50                ts = new TileSet(new TileXY(0, 0),
     51                        new TileXY(this.imageTiling.getTileXMax(zoom), this.imageTiling.getTileYMax(zoom)),
     52                        zoom, this.memoryTileCache, this.imageTiling);
     53                ts.allTilesCreate();
     54                tileSets[zoom-minZoom] = ts;
     55            }
     56            return ts;
     57        }
     58    }
     59
     60    /**
     61     * Get the tile cache for this deep tile set
     62     * @return The tile cache
     63     */
     64    TileCache getTileCache() {
     65        return this.memoryTileCache;
     66    }
     67
     68    @Override
     69    public int size() {
     70        return Math.toIntExact(Stream.of(this.tileSets).filter(Objects::nonNull).count());
     71    }
     72
     73    @Override
     74    public boolean isEmpty() {
     75        return Stream.of(this.tileSets).allMatch(Objects::isNull);
     76    }
     77
     78    @Override
     79    public boolean contains(Object o) {
     80        Objects.requireNonNull(o);
     81        return Arrays.asList(this.tileSets).contains(o);
     82    }
     83
     84    @Override
     85    public Iterator<TileSet> iterator() {
     86        return Stream.of(this.tileSets).filter(Objects::nonNull).iterator();
     87    }
     88
     89    @Override
     90    public Object[] toArray() {
     91        return Stream.of(this.tileSets).filter(Objects::nonNull).toArray();
     92    }
     93
     94    @Override
     95    public <T> T[] toArray(T[] array) {
     96        return Arrays.asList(this.tileSets).toArray(array);
     97    }
     98
     99    @Override
     100    public boolean add(TileSet tileSet) {
     101        throw new UnsupportedOperationException(this.getClass().getSimpleName() + " does not support add");
     102    }
     103
     104    @Override
     105    public boolean remove(Object object) {
     106        Objects.requireNonNull(object);
     107        for (int i = 0; i < this.tileSets.length; i++) {
     108            if (object.equals(this.tileSets[i])) {
     109                this.tileSets[i] = null;
     110                return true;
     111            }
     112        }
     113        return false;
     114    }
     115
     116    @Override
     117    public boolean containsAll(Collection<?> collection) {
     118        Objects.requireNonNull(collection);
     119        List<TileSet> list = Arrays.asList(this.tileSets);
     120        return list.containsAll(collection);
     121    }
     122
     123    @Override
     124    public boolean addAll(Collection<? extends TileSet> collection) {
     125        throw new UnsupportedOperationException(this.getClass().getSimpleName() + " does not support addAll");
     126    }
     127
     128    @Override
     129    public boolean removeAll(Collection<?> collection) {
     130        Objects.requireNonNull(collection);
     131        final int count = this.size();
     132        collection.forEach(this::remove);
     133        return count != this.size();
     134    }
     135
     136    @Override
     137    public boolean retainAll(Collection<?> collection) {
     138        Objects.requireNonNull(collection);
     139        List<?> toRemove = Stream.of(this.tileSets).filter(Objects::nonNull)
     140                .filter(set -> !collection.contains(set)).collect(Collectors.toList());
     141        return this.removeAll(toRemove);
     142    }
     143
     144    @Override
     145    public void clear() {
     146        this.memoryTileCache.clear();
     147        Arrays.fill(this.tileSets, null);
     148    }
     149}
  • new file src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/GeoImageTileLoader.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/GeoImageTileLoader.java b/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/GeoImageTileLoader.java
    new file mode 100644
    index 0000000000..a96f462b4c
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling;
     3
     4import java.util.concurrent.ThreadPoolExecutor;
     5
     6import org.apache.commons.jcs3.access.behavior.ICacheAccess;
     7import org.apache.commons.jcs3.engine.behavior.ICache;
     8import org.openstreetmap.gui.jmapviewer.Tile;
     9import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
     10import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
     11import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     12import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     13import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     14import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
     15import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
     16import org.openstreetmap.josm.tools.CheckParameterUtil;
     17
     18/**
     19 * A tile loader for geo images
     20 * @author Taylor Smock
     21 * @since xxx
     22 */
     23public class GeoImageTileLoader implements TileLoader, CachedTileLoader {
     24
     25    protected final ICacheAccess<String, BufferedImageCacheEntry> cache;
     26    public TileLoaderListener listener;
     27    private static final ThreadPoolExecutor DEFAULT_DOWNLOAD_JOB_DISPATCHER = TMSCachedTileLoader.getNewThreadPoolExecutor("GeoImage-tiler-%d");
     28
     29    /**
     30     * Constructor for the GeoImageTileLoader
     31     * @param listener The listener to notify when tile loading finishes
     32     * @param cache The cache to use
     33     */
     34    public GeoImageTileLoader(final TileLoaderListener listener, final ICacheAccess<String, BufferedImageCacheEntry> cache) {
     35        CheckParameterUtil.ensureParameterNotNull(cache);
     36        this.cache = cache;
     37        this.listener = listener;
     38    }
     39
     40    @Override
     41    public void clearCache(TileSource source) {
     42        this.cache.remove(source.getName() + ICache.NAME_COMPONENT_DELIMITER);
     43    }
     44
     45    @Override
     46    public TileJob createTileLoaderJob(Tile tile) {
     47        return new GeoImageTileLoaderJob(this.listener, tile,
     48                this.cache, DEFAULT_DOWNLOAD_JOB_DISPATCHER
     49        );
     50    }
     51
     52    @Override
     53    public boolean hasOutstandingTasks() {
     54        return DEFAULT_DOWNLOAD_JOB_DISPATCHER.getTaskCount() > DEFAULT_DOWNLOAD_JOB_DISPATCHER.getCompletedTaskCount();
     55    }
     56
     57    @Override
     58    public void cancelOutstandingTasks() {
     59        for (Runnable runnable : DEFAULT_DOWNLOAD_JOB_DISPATCHER.getQueue()) {
     60            DEFAULT_DOWNLOAD_JOB_DISPATCHER.remove(runnable);
     61        }
     62    }
     63}
  • new file src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/GeoImageTileLoaderJob.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/GeoImageTileLoaderJob.java b/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/GeoImageTileLoaderJob.java
    new file mode 100644
    index 0000000000..79945abe13
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling;
     3
     4import java.awt.image.RenderedImage;
     5import java.io.ByteArrayInputStream;
     6import java.io.ByteArrayOutputStream;
     7import java.io.File;
     8import java.io.IOException;
     9import java.net.URL;
     10import java.time.Duration;
     11import java.util.Collections;
     12import java.util.concurrent.ThreadPoolExecutor;
     13
     14import javax.imageio.ImageIO;
     15
     16import org.apache.commons.jcs3.access.behavior.ICacheAccess;
     17import org.openstreetmap.gui.jmapviewer.Tile;
     18import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
     19import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     20import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
     21import org.openstreetmap.josm.data.cache.CacheEntry;
     22import org.openstreetmap.josm.data.cache.CacheEntryAttributes;
     23import org.openstreetmap.josm.data.cache.ICachedLoaderListener;
     24import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob;
     25import org.openstreetmap.josm.data.imagery.TileJobOptions;
     26import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
     27import org.openstreetmap.josm.tools.Logging;
     28
     29/**
     30 * A job to load geoimage tiles
     31 * @author Taylor Smock
     32 * @since xxx
     33 */
     34public class GeoImageTileLoaderJob extends JCSCachedTileLoaderJob<String, BufferedImageCacheEntry> implements TileJob,
     35        ICachedLoaderListener {
     36    private static final TileJobOptions DEFAULT_OPTIONS = new TileJobOptions(0, 0, Collections.emptyMap(),
     37            Duration.ofHours(1).getSeconds());
     38    private final Tile tile;
     39    private final TileLoaderListener listener;
     40
     41    public GeoImageTileLoaderJob(TileLoaderListener listener, Tile tile,
     42            ICacheAccess<String, BufferedImageCacheEntry> cache, ThreadPoolExecutor geoimagetileloader) {
     43        super(cache, DEFAULT_OPTIONS, geoimagetileloader);
     44        this.listener = listener;
     45        this.tile = tile;
     46    }
     47
     48    @Override
     49    public void submit() {
     50        submit(false);
     51    }
     52
     53    @Override
     54    public void submit(boolean force) {
     55        try {
     56            super.submit(this, force);
     57
     58        } catch (IllegalArgumentException | IOException e) {
     59            Logging.warn(e);
     60        }
     61    }
     62
     63    @Override
     64    public String getCacheKey() {
     65        return tile.getKey();
     66    }
     67
     68    @Override
     69    public URL getUrl() throws IOException {
     70        if (this.tile.getTileSource() instanceof IImageEntry) {
     71            return ((IImageEntry<?>) this.tile.getTileSource()).getFile().toURI().toURL();
     72        }
     73        return null;
     74    }
     75
     76    @Override
     77    public void loadingFinished(CacheEntry data, CacheEntryAttributes attributes, LoadResult result) {
     78        try {
     79            if (data instanceof BufferedImageCacheEntry) {
     80                this.tile.setImage(((BufferedImageCacheEntry) data).getImage());
     81            } else if (data != null) {
     82                this.tile.loadImage(new ByteArrayInputStream(data.getContent()));
     83            }
     84            this.tile.finishLoading();
     85        } catch (IOException e) {
     86            this.tile.setError(e);
     87        }
     88        this.listener.tileLoadingFinished(this.tile, LoadResult.SUCCESS == result);
     89    }
     90
     91    @Override
     92    protected BufferedImageCacheEntry createCacheEntry(byte[] content) {
     93        return new BufferedImageCacheEntry(content);
     94    }
     95
     96    @Override
     97    protected byte[] loadObjectBytes(File file) throws IOException {
     98        if (this.tile.getTileSource() instanceof IImageTiling) {
     99            final IImageTiling<?> tileSource = (IImageTiling<?>) this.tile.getTileSource();
     100            final RenderedImage image = tileSource.getTileImage(this.tile.getZoom(), this.tile.getXtile(), this.tile.getYtile());
     101            if (image == null) {
     102                throw new IOException("No image loaded for " + file.toString());
     103            }
     104            final ByteArrayOutputStream output = new ByteArrayOutputStream(image.getHeight() * image.getWidth());
     105            ImageIO.write(image, "jpg", output);
     106            return output.toByteArray();
     107        }
     108        return super.loadObjectBytes(file);
     109    }
     110}
  • 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..eb43fb09b3
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling;
     3
     4import java.awt.Dimension;
     5import java.awt.Image;
     6import java.awt.Point;
     7import java.awt.Rectangle;
     8import java.awt.image.RenderedImage;
     9import java.io.IOException;
     10import java.util.List;
     11import java.util.Map;
     12import java.util.Objects;
     13import java.util.function.DoubleToIntFunction;
     14import java.util.function.IntUnaryOperator;
     15import java.util.stream.IntStream;
     16import java.util.stream.Stream;
     17
     18import org.apache.commons.jcs3.access.CacheAccess;
     19import org.openstreetmap.gui.jmapviewer.Tile;
     20import org.openstreetmap.gui.jmapviewer.TileXY;
     21import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
     22import org.openstreetmap.gui.jmapviewer.interfaces.IProjected;
     23import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     24import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
     25import org.openstreetmap.josm.data.cache.JCSCacheManager;
     26import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
     27import org.openstreetmap.josm.gui.layer.imagery.TilePosition;
     28import org.openstreetmap.josm.spi.preferences.Config;
     29
     30/**
     31 * An interface for tiled images. Primarily used to reduce memory usage in large images.
     32 * @author Taylor Smock
     33 * @param <I> The image type returned
     34 * @since xxx
     35 */
     36public interface IImageTiling<I extends Image & RenderedImage> extends TileSource {
     37
     38    /**
     39     * The default tile size for the image tiles -- each tile takes 1024 px * 1024 px * 4 bytes = 4 MiB max
     40     * A 4k image (4160x3120) has (Math.ceil(4160/1024) * Math.ceil(3120/1024) = 20 tiles). Some tiles are almost empty.
     41     * This gives a reasonable number of tiles for most image sizes.
     42     */
     43    int DEFAULT_TILE_SIZE = 1024;
     44
     45    /** A good default minimum zoom (the image size is {@link #DEFAULT_TILE_SIZE} max, at 1024 it is 5) */
     46    int DEFAULT_MIN_ZOOM = (int) Math.round(Math.log(Math.sqrt(DEFAULT_TILE_SIZE))/Math.log(2));
     47
     48    /** A cache for images */
     49    CacheAccess<String, BufferedImageCacheEntry> IMAGE_CACHE =
     50            JCSCacheManager.getCache("iimagetiling", 100, 1_000,
     51                    Config.getDirs().getCacheDirectory(true).getAbsolutePath());
     52
     53    /**
     54     * Get the size of the image at a specified zoom level
     55     * @param zoom The zoom level. Zoom 0 == 1 px for the image. Zoom 1 == 4 px for the image.
     56     * @return The number of pixels (max, for a square image)
     57     */
     58    static long getSizeAtZoom(final int zoom) {
     59        final long dimension = 1L << zoom;
     60        return dimension * dimension;
     61    }
     62
     63    /**
     64     * Get the default tile size.
     65     * @return The tile size to use
     66     */
     67    default int getDefaultTileSize() {
     68        return DEFAULT_TILE_SIZE;
     69    }
     70
     71    /**
     72     * Get the tile size.
     73     * @return The tile size to use
     74     */
     75    default int getTileSize() {
     76        return this.getDefaultTileSize();
     77    }
     78
     79    /**
     80     * Get the maximum zoom that the image supports
     81     * Feel free to override and cache the result for performance reasons.
     82     *
     83     * @return The maximum zoom of the image
     84     */
     85    default int getMaxZoom() {
     86        final int maxSize = Math.max(this.getWidth(), this.getHeight());
     87        return (int) Math.round(Math.ceil(Math.log(maxSize) / Math.log(2)));
     88    }
     89
     90    /**
     91     * Get the minimum zoom that the image supports or makes sense
     92     * @return The minimum zoom that makes sense
     93     */
     94    default int getMinZoom() {
     95        final IntUnaryOperator minZoom = input -> Math.toIntExact(
     96                Math.round(Math.floor(this.getMaxZoom() + Math.log((double) this.getTileSize() / input) / Math.log(2))));
     97        return Math.min(minZoom.applyAsInt(this.getWidth()), minZoom.applyAsInt(this.getHeight()));
     98    }
     99
     100    /**
     101     * Get the current scale of the image
     102     * @param zoom The zoom level
     103     * @return The scaling of the image at the specified level
     104     */
     105    default double getScale(final int zoom) {
     106        return Math.pow(2, (double) zoom - this.getMaxZoom());
     107    }
     108
     109    /**
     110     * Get the width of the image
     111     * @return The width of the image
     112     */
     113    int getWidth();
     114
     115    /**
     116     * Get the width of the image at a specified scale
     117     * @param zoom The zoom to use
     118     * @return The width at the specified scale
     119     */
     120    default int getWidth(final int zoom) {
     121        return Math.toIntExact(Math.round(this.getScale(zoom) * this.getWidth()));
     122    }
     123
     124    /**
     125     * Get the height of the image
     126     * @return The height of the image
     127     */
     128    int getHeight();
     129
     130    /**
     131     * Get the height of the image at a specified scale
     132     * @param zoom The zoom to use
     133     * @return The height at the specified scale
     134     */
     135    default int getHeight(final int zoom) {
     136        return Math.toIntExact(Math.round(this.getScale(zoom) * this.getHeight()));
     137    }
     138
     139    /**
     140     * Get the size of the image
     141     * @return The image size at the zoom level
     142     */
     143    default Dimension getSize(int zoom) {
     144        return new Dimension(this.getWidth(zoom), this.getHeight(zoom));
     145    }
     146
     147    /**
     148     * Get the number of rows at a specified zoom level
     149     * @param zoom The zoom level
     150     * @return The number of rows
     151     */
     152    default int getRows(final int zoom) {
     153        return this.getRows(zoom, this.getTileSize());
     154    }
     155
     156    /**
     157     * Get the number of rows at a specified zoom level
     158     * @param zoom The zoom level
     159     * @param tileSize The tile size
     160     * @return The number of rows
     161     */
     162    default int getRows(final int zoom, final int tileSize) {
     163        final int height = this.getHeight(zoom);
     164        return Math.toIntExact(Math.round(Math.ceil(height / (double) tileSize)));
     165    }
     166
     167    /**
     168     * Get the number of columns at a specified zoom level
     169     * @param zoom The zoom level
     170     * @return The number of columns
     171     */
     172    default int getColumns(final int zoom) {
     173        return this.getColumns(zoom, this.getTileSize());
     174    }
     175
     176    /**
     177     * Get the number of columns at a specified zoom level
     178     * @param zoom The zoom level
     179     * @param tileSize The tile size
     180     * @return The number of columns
     181     */
     182    default int getColumns(final int zoom, final int tileSize) {
     183        final int width = this.getWidth(zoom);
     184        return Math.toIntExact(Math.round(Math.ceil(width / (double) tileSize)));
     185    }
     186
     187    /**
     188     * A DeepTileSet (to avoid creating and loading tiles over and over)
     189     * @return The deap tile set
     190     */
     191    DeepTileSet getDeepTileSet();
     192
     193    /**
     194     * Get the image to show for a specific tile location. This should be cached by the implementation in most cases.
     195     * Top-left corner is 0,0
     196     * @param zoom The zoom to use
     197     * @param tileSize The tile size to use
     198     * @param column The column to get (x)
     199     * @param row The row to get (y)
     200     * @return The image to display (not padded). May be {@code null}.
     201     */
     202    I getTileImage(int zoom, int tileSize, int column, int row);
     203
     204    /**
     205     * Get the image to show for a specific tile location with the default tile size
     206     * Top-left corner is 0,0
     207     * @param zoom The zoom to use
     208     * @param column The column to get (x)
     209     * @param row The row to get (y)
     210     * @return The image to display (not padded). May be {@code null}.
     211     */
     212    default I getTileImage(final int zoom, final int column, final int row) {
     213        return this.getTileImage(zoom, this.getTileSize(), column, row);
     214    }
     215
     216    /**
     217     * Get the subsection of the image to show
     218     * Top-left corner is 0,0
     219     * @param zoom The zoom to use
     220     * @param column The column to get (x)
     221     * @param row The row to get (y)
     222     * @return The subsection of the image to get
     223     */
     224    default Rectangle getTileDimension(final int zoom, final int column, final int row) {
     225        return this.getTileDimension(zoom, column, row, this.getTileSize());
     226    }
     227
     228    /**
     229     * Get the subsection of the image to show
     230     * Top-left corner is 0,0
     231     * @param zoom The zoom to use
     232     * @param column The column to get (x)
     233     * @param row The row to get (y)
     234     * @param tileSize the tile size to use
     235     * @return The subsection of the image to get
     236     */
     237    default Rectangle getTileDimension(final int zoom, final int column, final int row, final int tileSize) {
     238        final double scale = this.getScale(zoom); // e.g., 1, 1/2, 1/4, etc.
     239        final DoubleToIntFunction roundToInt = dbl -> Math.toIntExact(Math.round(Math.floor(dbl)));
     240        final int x = roundToInt.applyAsInt(column * tileSize / scale);
     241        final int y = roundToInt.applyAsInt(row * tileSize / scale);
     242        final int defaultDimension = roundToInt.applyAsInt(tileSize / scale);
     243        final int width = Math.min(defaultDimension, roundToInt.applyAsInt(this.getWidth() - column * tileSize / scale));
     244        final int height = Math.min(defaultDimension, roundToInt.applyAsInt(this.getHeight() - row * tileSize / scale));
     245        return new Rectangle(x, y, width, height);
     246    }
     247
     248    /**
     249     * Get the tiles for a zoom level given a visible rectangle
     250     * @param zoom The zoom to get
     251     * @param visibleRect The rectangle to get
     252     * @return A stream of tiles to images (may be parallel)
     253     */
     254    default Stream<Tile> getTiles(int zoom, Rectangle visibleRect) {
     255        // We very specifically want to "overscan" -- this fixes some issues where the image isn't fully loaded
     256        final int startX = Math.max(0, Math.toIntExact(Math.round(Math.floor(visibleRect.getMinX() / this.getTileSize()))) - 1);
     257        final int startY = Math.max(0, Math.toIntExact(Math.round(Math.floor(visibleRect.getMinY() / this.getTileSize()))) - 1);
     258        final int endX = Math.min(this.getColumns(zoom), Math.toIntExact(Math.round(Math.ceil(visibleRect.getMaxX() / this.getTileSize()))) + 1);
     259        final int endY = Math.min(this.getRows(zoom), Math.toIntExact(Math.round(Math.ceil(visibleRect.getMaxY() / this.getTileSize()))) + 1);
     260        final TileSet tileSet = this.getDeepTileSet().getTileSet(zoom);
     261        tileSet.allTilesCreate();
     262        return IntStream.range(startX, endX).mapToObj(x -> IntStream.range(startY, endY).mapToObj(y -> new TilePosition(x, y, zoom)))
     263                .flatMap(stream -> stream).map(tileSet::getTile).filter(Objects::nonNull);
     264    }
     265
     266    /**
     267     * Check if tiling is enabled for this object.
     268     *
     269     * @return {@code true} if tiling should be u sed
     270     */
     271    default boolean isTilingEnabled() {
     272        return true;
     273    }
     274
     275    /* ************** The following are filler methods ***************** */
     276
     277    @Override
     278    default String getName() {
     279        if (this instanceof IImageEntry) {
     280            return ((IImageEntry<?>) this).getDisplayName();
     281        }
     282        return this.getClass().getSimpleName();
     283    }
     284
     285    @Override
     286    default String getId() {
     287        return this.getClass().getName();
     288    }
     289
     290    @Override
     291    default String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
     292        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     293    }
     294
     295    @Override
     296    default String getTileId(int zoom, int tilex, int tiley) {
     297        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     298    }
     299
     300    @Override
     301    default double getDistance(double la1, double lo1, double la2, double lo2) {
     302        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     303    }
     304
     305    @Override
     306    default Point latLonToXY(double lat, double lon, int zoom) {
     307        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     308    }
     309
     310    @Override
     311    default Point latLonToXY(ICoordinate point, int zoom) {
     312        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     313    }
     314
     315    @Override
     316    default ICoordinate xyToLatLon(Point point, int zoom) {
     317        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     318    }
     319
     320    @Override
     321    default ICoordinate xyToLatLon(int x, int y, int zoom) {
     322        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     323    }
     324
     325    @Override
     326    default TileXY latLonToTileXY(double lat, double lon, int zoom) {
     327        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     328    }
     329
     330    @Override
     331    default TileXY latLonToTileXY(ICoordinate point, int zoom) {
     332        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     333    }
     334
     335    @Override
     336    default ICoordinate tileXYToLatLon(TileXY xy, int zoom) {
     337        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     338    }
     339
     340    @Override
     341    default ICoordinate tileXYToLatLon(Tile tile) {
     342        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     343    }
     344
     345    @Override
     346    default ICoordinate tileXYToLatLon(int x, int y, int zoom) {
     347        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     348    }
     349
     350    @Override
     351    default int getTileXMax(int zoom) {
     352        return this.getColumns(zoom);
     353    }
     354
     355    @Override
     356    default int getTileXMin(int zoom) {
     357        return 0;
     358    }
     359
     360    @Override
     361    default int getTileYMax(int zoom) {
     362        return this.getRows(zoom);
     363    }
     364
     365    @Override
     366    default int getTileYMin(int zoom) {
     367        return 0;
     368    }
     369
     370    @Override
     371    default boolean isNoTileAtZoom(Map<String, List<String>> headers, int statusCode, byte[] content) {
     372        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     373    }
     374
     375    @Override
     376    default Map<String, String> getMetadata(Map<String, List<String>> headers) {
     377        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     378    }
     379
     380    @Override
     381    default IProjected tileXYtoProjected(int x, int y, int zoom) {
     382        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     383    }
     384
     385    @Override
     386    default TileXY projectedToTileXY(IProjected p, int zoom) {
     387        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     388    }
     389
     390    @Override
     391    default boolean isInside(Tile inner, Tile outer) {
     392        // If the outer zoom is greater than inner zoom, then it the inner tile cannot be inside the outer tile
     393        final int zoomDifference = inner.getZoom() - outer.getZoom();
     394        // Example: outer(z13) > inner(z12), so outer covers a smaller "real" area than the inner.
     395        if (zoomDifference < 0) {
     396            return false;
     397        }
     398        // Each zoom level has 4x as many tiles, 2x in each direction
     399        final double tileScale = Math.pow(2, zoomDifference);
     400        final double minX = inner.getXtile() * tileScale;
     401        final double minY = inner.getXtile() * tileScale;
     402        final double maxX = minX + tileScale - 1;
     403        final double maxY = minY + tileScale - 1;
     404        return inner.getXtile() >= minX && inner.getXtile() <= maxX
     405                && inner.getYtile() >= minY && inner.getYtile() <= maxY;
     406    }
     407
     408    @Override
     409    default TileSet getCoveringTileRange(Tile tile, int newZoom) {
     410        final int tileZoom = tile.getZoom();
     411        final double tileScale = Math.pow(2, newZoom - tileZoom);
     412        final DoubleToIntFunction clampDouble = dbl -> Math.toIntExact(Math.round(dbl));
     413        if (tileScale < 1) {
     414            final TileXY superTile = new TileXY(clampDouble.applyAsInt(tile.getXtile() * tileScale),
     415                    clampDouble.applyAsInt(tile.getYtile() * tileScale));
     416            return new TileSet(superTile, superTile, newZoom, this.getDeepTileSet().getTileCache(), this);
     417        }
     418        final TileXY subTile1 = new TileXY(clampDouble.applyAsInt(tile.getXtile() * tileScale),
     419                clampDouble.applyAsInt(tile.getYtile() * tileScale));
     420        final TileXY subTile2 = new TileXY(subTile1.getXIndex() + clampDouble.applyAsInt(tileScale),
     421                subTile1.getY() + clampDouble.applyAsInt(tileScale));
     422        return new TileSet(subTile1, subTile2, newZoom, this.getDeepTileSet().getTileCache(), this);
     423    }
     424
     425    @Override
     426    default String getServerCRS() {
     427        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     428    }
     429
     430    @Override
     431    default boolean requiresAttribution() {
     432        return false;
     433    }
     434
     435    @Override
     436    default String getAttributionText(int zoom, ICoordinate topLeft, ICoordinate botRight) {
     437        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     438    }
     439
     440    @Override
     441    default String getAttributionLinkURL() {
     442        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     443    }
     444
     445    @Override
     446    default Image getAttributionImage() {
     447        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     448    }
     449
     450    @Override
     451    default String getAttributionImageURL() {
     452        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     453    }
     454
     455    @Override
     456    default String getTermsOfUseText() {
     457        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     458    }
     459
     460    @Override
     461    default String getTermsOfUseURL() {
     462        throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName());
     463    }
     464}
  • new file src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/TileSet.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/TileSet.java b/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/TileSet.java
    new file mode 100644
    index 0000000000..4572f73fe6
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling;
     3
     4import java.util.Collection;
     5import java.util.Iterator;
     6import java.util.List;
     7import java.util.Objects;
     8import java.util.Set;
     9import java.util.function.Function;
     10import java.util.stream.Collectors;
     11import java.util.stream.IntStream;
     12import java.util.stream.Stream;
     13
     14import org.openstreetmap.gui.jmapviewer.Tile;
     15import org.openstreetmap.gui.jmapviewer.TileRange;
     16import org.openstreetmap.gui.jmapviewer.TileXY;
     17import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
     18import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     19import org.openstreetmap.josm.gui.layer.imagery.TilePosition;
     20
     21/**
     22 * A set of tiles
     23 * @author Taylor Smock
     24 * @since xxx
     25 */
     26public class TileSet extends TileRange implements Set<Tile> {
     27    private final TileCache tileCache;
     28    private final TileSource tileSource;
     29
     30    /**
     31     * Constructs a new {@code TileRange}.
     32     * @param t1 first tile
     33     * @param t2 second tile
     34     * @param zoom zoom level
     35     */
     36    public TileSet(final TileXY t1, final TileXY t2, final int zoom, final TileCache tileCache, final TileSource tileSource) {
     37        super(t1, t2, zoom);
     38        this.tileCache = tileCache;
     39        this.tileSource = tileSource;
     40    }
     41
     42    TileSet() {
     43        this.tileCache = null;
     44        this.tileSource = null;
     45    }
     46
     47    /**
     48     * Gets a stream of all tile positions in this set
     49     * @return A stream of all positions
     50     */
     51    public Stream<TilePosition> tilePositions() {
     52        if (zoom == 0) {
     53            return Stream.empty(); // Tileset is empty
     54        } else {
     55            return IntStream.rangeClosed(minX, maxX).mapToObj(
     56                    x -> IntStream.rangeClosed(minY, maxY).mapToObj(y -> new TilePosition(x, y, zoom))
     57            ).flatMap(Function.identity());
     58        }
     59    }
     60
     61    /**
     62     * Get a tile at a position
     63     * @param tilePosition The position to get
     64     * @return The tile (may be null)
     65     */
     66    public Tile getTile(final TilePosition tilePosition) {
     67        return this.tileCache.getTile(this.tileSource, tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom());
     68    }
     69
     70    protected List<Tile> allTilesCreate() {
     71        return this.allTiles(this::createOrGetTiles).collect(Collectors.toList());
     72    }
     73
     74    private Tile createOrGetTiles(final TilePosition tilePosition) {
     75        Tile tile = this.tileCache.getTile(this.tileSource, tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom());
     76        if (tile != null) {
     77            return tile;
     78        }
     79        tile = new Tile(this.tileSource, tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom());
     80        this.tileCache.addTile(tile);
     81        return tile;
     82    }
     83
     84    /**
     85     * Get all tiles
     86     * @return All tiles in this set
     87     */
     88    public Stream<Tile> allTiles() {
     89        return this.allTiles(this::getTile);
     90    }
     91
     92    private Stream<Tile> allTiles(Function<TilePosition, Tile> mapper) {
     93        return tilePositions().map(mapper).filter(Objects::nonNull);
     94    }
     95
     96    @Override
     97    public boolean isEmpty() {
     98        return this.allTiles(tile -> this.tileCache.getTile(this.tileSource, tile.getX(), tile.getY(), tile.getZoom()))
     99                .anyMatch(Objects::nonNull);
     100    }
     101
     102    @Override
     103    public boolean contains(Object o) {
     104        if (o instanceof Tile) {
     105            Tile tile = (Tile) o;
     106            return this.getTile(new TilePosition(tile.getXtile(), tile.getYtile(), tile.getZoom())) != null;
     107        }
     108        return false;
     109    }
     110
     111    @Override
     112    public Iterator<Tile> iterator() {
     113        return allTiles().iterator();
     114    }
     115
     116    @Override
     117    public Object[] toArray() {
     118        return allTiles().toArray();
     119    }
     120
     121    @Override
     122    public <T> T[] toArray(T[] array) {
     123        return allTiles().collect(Collectors.toList()).toArray(array);
     124    }
     125
     126    @Override
     127    public boolean add(Tile tile) {
     128        throw new UnsupportedOperationException(this.getClass().getSimpleName() + " does not support add");
     129    }
     130
     131    @Override
     132    public boolean remove(Object o) {
     133        throw new UnsupportedOperationException(this.getClass().getSimpleName() + " does not support remove");
     134    }
     135
     136    @Override
     137    public boolean containsAll(Collection<?> c) {
     138        return c.stream().allMatch(this::contains);
     139    }
     140
     141    @Override
     142    public boolean addAll(Collection<? extends Tile> c) {
     143        throw new UnsupportedOperationException(this.getClass().getSimpleName() + " does not support add");
     144    }
     145
     146    @Override
     147    public boolean retainAll(Collection<?> c) {
     148        throw new UnsupportedOperationException(this.getClass().getSimpleName() + " does not support remove");
     149    }
     150
     151    @Override
     152    public boolean removeAll(Collection<?> c) {
     153        throw new UnsupportedOperationException(this.getClass().getSimpleName() + " does not support remove");
     154    }
     155
     156    @Override
     157    public void clear() {
     158        throw new UnsupportedOperationException(this.getClass().getSimpleName() + " does not support remove");
     159    }
     160}
  • 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..a5b8d11d6e
    - +  
     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 DeepTileSet getDeepTileSet() {
     133            return new DeepTileSet(this.getMinZoom(), this.getMaxZoom(), this);
     134        }
     135
     136        @Override
     137        public BufferedImage getTileImage(int zoom, int tileSize, int column, int row) {
     138            this.counter.incrementAndGet();
     139            if (image instanceof BufferedImage) {
     140                final BufferedImage bufferedImage = (BufferedImage) image;
     141                return bufferedImage.getSubimage(column * tileSize, row * tileSize,
     142                        Math.min(tileSize, bufferedImage.getWidth() - column * tileSize - 1),
     143                        Math.min(tileSize, bufferedImage.getHeight() - row * tileSize - 1));
     144            }
     145            throw new UnsupportedOperationException("The test ImageTiling class only supports BufferedImages");
     146        }
     147    }
     148}