Index: trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java	(revision 17869)
+++ trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java	(revision 17871)
@@ -10,5 +10,4 @@
 import java.awt.Graphics2D;
 import java.awt.Image;
-import java.awt.MediaTracker;
 import java.awt.Point;
 import java.awt.Rectangle;
@@ -22,10 +21,7 @@
 import java.awt.geom.Rectangle2D;
 import java.awt.image.BufferedImage;
-import java.awt.image.ImageObserver;
-import java.io.File;
 import java.io.IOException;
 import java.util.Objects;
 
-import javax.imageio.ImageIO;
 import javax.swing.JComponent;
 import javax.swing.SwingUtilities;
@@ -33,4 +29,6 @@
 import org.openstreetmap.josm.data.preferences.BooleanProperty;
 import org.openstreetmap.josm.data.preferences.DoubleProperty;
+import org.openstreetmap.josm.data.preferences.IntegerProperty;
+import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
 import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChangeListener;
@@ -40,7 +38,5 @@
 import org.openstreetmap.josm.tools.Destroyable;
 import org.openstreetmap.josm.tools.ExifReader;
-import org.openstreetmap.josm.tools.HiDPISupport;
 import org.openstreetmap.josm.tools.ImageProcessor;
-import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.Logging;
 
@@ -79,7 +75,4 @@
     private VisRect selectedRect;
 
-    /** The tracker to load the images */
-    private final MediaTracker tracker = new MediaTracker(this);
-
     private final ImgDisplayMouseListener imgMouseListener = new ImgDisplayMouseListener();
 
@@ -104,11 +97,7 @@
         new DoubleProperty("geoimage.maximum-zoom-scale", 2.0);
 
-    /** Use bilinear filtering **/
-    private static final BooleanProperty BILIN_DOWNSAMP =
-        new BooleanProperty("geoimage.bilinear-downsampling-progressive", true);
-    private static final BooleanProperty BILIN_UPSAMP =
-        new BooleanProperty("geoimage.bilinear-upsampling", false);
-    private static double bilinUpper;
-    private static double bilinLower;
+    /** Maximum width (in pixels) for loading images **/
+    private static final IntegerProperty MAX_WIDTH =
+        new IntegerProperty("geoimage.maximum-width", 6000);
 
     /** Show a background for the error text (may be hard on eyes) */
@@ -121,11 +110,4 @@
             dragButton = AGPIFO_STYLE.get() ? 1 : 3;
             zoomButton = dragButton == 1 ? 3 : 1;
-        }
-        if (e == null ||
-            e.getKey().equals(MAX_ZOOM.getKey()) ||
-            e.getKey().equals(BILIN_DOWNSAMP.getKey()) ||
-            e.getKey().equals(BILIN_UPSAMP.getKey())) {
-            bilinUpper = (BILIN_UPSAMP.get() ? 2*MAX_ZOOM.get() : (BILIN_DOWNSAMP.get() ? 0.5 : 0));
-            bilinLower = (BILIN_DOWNSAMP.get() ? 0 : 1);
         }
     }
@@ -237,123 +219,29 @@
 
     /** The thread that reads the images. */
-    protected class LoadImageRunnable implements Runnable, ImageObserver {
+    protected class LoadImageRunnable implements Runnable {
 
         private final ImageEntry entry;
-        private final File file;
 
         LoadImageRunnable(ImageEntry entry) {
             this.entry = entry;
-            this.file = entry.getFile();
-        }
-
-        @Override
-        public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
-            if (((infoflags & ImageObserver.WIDTH) == ImageObserver.WIDTH) &&
-                ((infoflags & ImageObserver.HEIGHT) == ImageObserver.HEIGHT)) {
-                synchronized (entry) {
-                    entry.setWidth(width);
-                    entry.setHeight(height);
-                    entry.notifyAll();
-                    return false;
-                }
-            }
-            return true;
-        }
-
-        private boolean updateImageEntry(Image img) {
-            if (img == null) {
-                synchronized (ImageDisplay.this) {
-                    errorLoading = true;
-                    ImageDisplay.this.repaint();
-                    return false;
-                }
-            }
-            if (!(entry.getWidth() > 0 && entry.getHeight() > 0)) {
-                synchronized (entry) {
-                    int width = img.getWidth(this);
-                    int height = img.getHeight(this);
-
-                    if (!(entry.getWidth() > 0 && entry.getHeight() > 0) && width > 0 && height > 0) {
-                        // dimensions not in metadata but already present in image, so observer won't be called
-                        entry.setWidth(width);
-                        entry.setHeight(height);
-                        entry.notifyAll();
-                    }
-
-                    long now = System.currentTimeMillis();
-                    while (!(entry.getWidth() > 0 && entry.getHeight() > 0)) {
-                        try {
-                            entry.wait(1000);
-                            if (this.entry != ImageDisplay.this.entry)
-                                return false;
-                            if (System.currentTimeMillis() - now > 10000)
-                                synchronized (ImageDisplay.this) {
-                                    errorLoading = true;
-                                    ImageDisplay.this.repaint();
-                                    return false;
-                                }
-                        } catch (InterruptedException e) {
-                            Logging.trace(e);
-                            Logging.warn("InterruptedException in {0} while getting properties of image {1}",
-                                    getClass().getSimpleName(), file.getPath());
-                            Thread.currentThread().interrupt();
-                        }
-                    }
-                }
-            }
-            return true;
-        }
-
-        private boolean mayFitMemory(long amountWanted) {
-            return amountWanted < (
-                   Runtime.getRuntime().maxMemory() -
-                   Runtime.getRuntime().totalMemory() +
-                   Runtime.getRuntime().freeMemory());
         }
 
         @Override
         public void run() {
-            BufferedImage img;
             try {
-                img = ImageIO.read(file);
-
-                if (!updateImageEntry(img))
-                    return;
-
-                int width = entry.getWidth();
-                int height = entry.getHeight();
-
-                if (mayFitMemory(((long) width)*height*4*2)) {
-                    Logging.info(tr("Loading {0}", file.getPath()));
-                    tracker.addImage(img, 1);
-
-                    // Wait for the end of loading
-                    while (!tracker.checkID(1, true)) {
-                        if (this.entry != ImageDisplay.this.entry) {
-                            // The file has changed
-                            tracker.removeImage(img);
-                            return;
-                        }
-                        try {
-                            Thread.sleep(5);
-                        } catch (InterruptedException e) {
-                            Logging.trace(e);
-                            Logging.warn("InterruptedException in {0} while loading image {1}",
-                                    getClass().getSimpleName(), file.getPath());
-                            Thread.currentThread().interrupt();
-                        }
+                Dimension target = new Dimension(MAX_WIDTH.get(), MAX_WIDTH.get());
+                BufferedImage img = entry.read(target);
+                if (img == null) {
+                    synchronized (ImageDisplay.this) {
+                        errorLoading = true;
+                        ImageDisplay.this.repaint();
+                        return;
                     }
-                    if (tracker.isErrorID(1)) {
-                        Logging.warn("Abort loading of {0} since tracker errored with 1", file);
-                        // the tracker catches OutOfMemory conditions
-                        tracker.removeImage(img);
-                        img = null;
-                    } else {
-                        tracker.removeImage(img);
-                    }
-                } else {
-                    Logging.warn("Abort loading of {0} since it might not fit into memory", file);
-                    img = null;
-                }
+                }
+
+                int width = img.getWidth();
+                int height = img.getHeight();
+                entry.setWidth(width);
+                entry.setHeight(height);
 
                 synchronized (ImageDisplay.this) {
@@ -363,35 +251,33 @@
                     }
 
-                    if (img != null) {
-                        boolean switchedDim = false;
-                        if (ExifReader.orientationNeedsCorrection(entry.getExifOrientation())) {
-                            if (ExifReader.orientationSwitchesDimensions(entry.getExifOrientation())) {
-                                width = img.getHeight(null);
-                                height = img.getWidth(null);
-                                switchedDim = true;
-                            }
-                            final BufferedImage rot = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
-                            final AffineTransform xform = ExifReader.getRestoreOrientationTransform(
-                                    entry.getExifOrientation(),
-                                    img.getWidth(null),
-                                    img.getHeight(null));
-                            final Graphics2D g = rot.createGraphics();
-                            g.drawImage(img, xform, null);
-                            g.dispose();
-                            img = rot;
+                    boolean switchedDim = false;
+                    if (ExifReader.orientationNeedsCorrection(entry.getExifOrientation())) {
+                        if (ExifReader.orientationSwitchesDimensions(entry.getExifOrientation())) {
+                            width = img.getHeight(null);
+                            height = img.getWidth(null);
+                            switchedDim = true;
                         }
-
-                        ImageDisplay.this.image = img;
-                        updateProcessedImage();
-                        // This will clear the loading info box
-                        ImageDisplay.this.oldEntry = ImageDisplay.this.entry;
-                        visibleRect = new VisRect(0, 0, width, height);
-
-                        Logging.debug("Loaded {0} with dimensions {1}x{2} memoryTaken={3}m exifOrientationSwitchedDimension={4}",
-                                file.getPath(), width, height, width*height*4/1024/1024, switchedDim);
+                        final BufferedImage rot = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+                        final AffineTransform xform = ExifReader.getRestoreOrientationTransform(
+                                entry.getExifOrientation(),
+                                img.getWidth(null),
+                                img.getHeight(null));
+                        final Graphics2D g = rot.createGraphics();
+                        g.drawImage(img, xform, null);
+                        g.dispose();
+                        img = rot;
                     }
 
+                    ImageDisplay.this.image = img;
+                    updateProcessedImage();
+                    // This will clear the loading info box
+                    ImageDisplay.this.oldEntry = ImageDisplay.this.entry;
+                    visibleRect = new VisRect(0, 0, width, height);
+
+                    Logging.debug("Loaded {0} with dimensions {1}x{2} memoryTaken={3}m exifOrientationSwitchedDimension={4}",
+                            entry.getFile().getPath(), width, height, width * height * 4 / 1024 / 1024, switchedDim);
+
                     selectedRect = null;
-                    errorLoading = (img == null);
+                    errorLoading = false;
                 }
                 ImageDisplay.this.repaint();
@@ -746,5 +632,5 @@
         LoadImageRunnable runnable = setImage0(entry);
         if (runnable != null) {
-            new Thread(runnable, LoadImageRunnable.class.getName()).start();
+            MainApplication.worker.execute(runnable);
         }
     }
@@ -821,37 +707,4 @@
             Rectangle r = new Rectangle(visibleRect);
             Rectangle target = calculateDrawImageRectangle(visibleRect, size);
-            double scale = target.width / (double) r.width; // pixel ratio is 1:1
-
-            if (selectedRect == null && !visibleRect.isDragUpdate &&
-                bilinLower < scale && scale < bilinUpper) {
-                try {
-                    BufferedImage bi = ImageProvider.toBufferedImage(image, r);
-                    if (bi != null) {
-                        r.x = r.y = 0;
-                        double hiDPIScale = HiDPISupport.getHiDPIScale();
-                        int width = (int) (target.width * hiDPIScale);
-                        int height = (int) (target.height * hiDPIScale);
-                        // See https://community.oracle.com/docs/DOC-983611 - The Perils of Image.getScaledInstance()
-                        // Pre-scale image when downscaling by more than two times to avoid aliasing from default algorithm
-                        bi = ImageProvider.createScaledImage(bi, width, height, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
-                        r.width = width;
-                        r.height = height;
-                        image = bi;
-                    }
-                } catch (OutOfMemoryError oom) {
-                    Logging.trace(oom);
-                    // fall-back to the non-bilinear scaler
-                    r.x = visibleRect.x;
-                    r.y = visibleRect.y;
-                }
-            } else {
-                // if target and r cause drawImage to scale image region to a tmp buffer exceeding
-                // its bounds, it will silently fail; crop with r first in such cases
-                // (might be impl. dependent, exhibited by openjdk 1.8.0_151)
-                if (scale*(r.x+r.width) > Short.MAX_VALUE || scale*(r.y+r.height) > Short.MAX_VALUE) {
-                    image = ImageProvider.toBufferedImage(image, r);
-                    r.x = r.y = 0;
-                }
-            }
 
             g.drawImage(image,
Index: trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java	(revision 17869)
+++ trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java	(revision 17871)
@@ -2,6 +2,10 @@
 package org.openstreetmap.josm.gui.layer.geoimage;
 
+import java.awt.Dimension;
 import java.awt.Image;
+import java.awt.image.BufferedImage;
 import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.util.Collections;
 import java.util.Objects;
@@ -9,4 +13,12 @@
 import org.openstreetmap.josm.data.ImageData;
 import org.openstreetmap.josm.data.gpx.GpxImageEntry;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.Logging;
+
+import javax.imageio.IIOParam;
+import javax.imageio.ImageReadParam;
+import javax.imageio.ImageReader;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
 
 /**
@@ -118,3 +130,31 @@
         return Objects.equals(thumbnail, other.thumbnail) && Objects.equals(dataSet, other.dataSet);
     }
+
+    /**
+     * Reads the image represented by this entry in the given target dimension.
+     * @param target the desired dimension used for {@linkplain IIOParam#setSourceSubsampling subsampling} or {@code null}
+     * @return the read image
+     * @throws IOException if any I/O error occurs
+     */
+    public BufferedImage read(Dimension target) throws IOException {
+        Logging.info(tr("Loading {0}", getFile().getPath()));
+        return ImageProvider.read(getFile(), false, false,
+                r -> target == null ? r.getDefaultReadParam() : withSubsampling(r, target));
+    }
+
+    private ImageReadParam withSubsampling(ImageReader reader, Dimension target) {
+        try {
+            ImageReadParam param = reader.getDefaultReadParam();
+            Dimension source = new Dimension(reader.getWidth(0), reader.getHeight(0));
+            if (source.getWidth() > target.getWidth() || source.getHeight() > target.getHeight()) {
+                int subsampling = (int) Math.floor(Math.max(
+                        source.getWidth() / target.getWidth(),
+                        source.getHeight() / target.getHeight()));
+                param.setSourceSubsampling(subsampling, subsampling, 0, 0);
+            }
+            return param;
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
 }
Index: trunk/src/org/openstreetmap/josm/tools/ImageProvider.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/ImageProvider.java	(revision 17869)
+++ trunk/src/org/openstreetmap/josm/tools/ImageProvider.java	(revision 17871)
@@ -48,4 +48,5 @@
 import java.util.concurrent.Executors;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.function.UnaryOperator;
 import java.util.regex.Matcher;
@@ -1548,4 +1549,47 @@
      */
     public static BufferedImage read(File input, boolean readMetadata, boolean enforceTransparency) throws IOException {
+        return read(input, readMetadata, enforceTransparency, ImageReader::getDefaultReadParam);
+    }
+
+    /**
+     * Returns a <code>BufferedImage</code> as the result of decoding
+     * a supplied <code>File</code> with an <code>ImageReader</code>
+     * chosen automatically from among those currently registered.
+     * The <code>File</code> is wrapped in an
+     * <code>ImageInputStream</code>.  If no registered
+     * <code>ImageReader</code> claims to be able to read the
+     * resulting stream, <code>null</code> is returned.
+     *
+     * <p> The current cache settings from <code>getUseCache</code>and
+     * <code>getCacheDirectory</code> will be used to control caching in the
+     * <code>ImageInputStream</code> that is created.
+     *
+     * <p> Note that there is no <code>read</code> method that takes a
+     * filename as a <code>String</code>; use this method instead after
+     * creating a <code>File</code> from the filename.
+     *
+     * <p> This method does not attempt to locate
+     * <code>ImageReader</code>s that can read directly from a
+     * <code>File</code>; that may be accomplished using
+     * <code>IIORegistry</code> and <code>ImageReaderSpi</code>.
+     *
+     * @param input a <code>File</code> to read from.
+     * @param readMetadata if {@code true}, makes sure to read image metadata to detect transparency color, if any.
+     * In that case the color can be retrieved later through {@link #PROP_TRANSPARENCY_COLOR}.
+     * Always considered {@code true} if {@code enforceTransparency} is also {@code true}
+     * @param enforceTransparency if {@code true}, makes sure to read image metadata and, if the image does not
+     * provide an alpha channel but defines a {@code TransparentColor} metadata node, that the resulting image
+     * has a transparency set to {@code TRANSLUCENT} and uses the correct transparent color.
+     * @param readParamFunction a function to compute the read parameters from the image reader
+     *
+     * @return a <code>BufferedImage</code> containing the decoded contents of the input, or <code>null</code>.
+     *
+     * @throws IllegalArgumentException if <code>input</code> is <code>null</code>.
+     * @throws IOException if an error occurs during reading.
+     * @see BufferedImage#getProperty
+     * @since xxx
+     */
+    public static BufferedImage read(File input, boolean readMetadata, boolean enforceTransparency,
+                                     Function<ImageReader, ImageReadParam> readParamFunction) throws IOException {
         CheckParameterUtil.ensureParameterNotNull(input, "input");
         if (!input.canRead()) {
@@ -1557,5 +1601,5 @@
             throw new IIOException("Can't create an ImageInputStream!");
         }
-        BufferedImage bi = read(stream, readMetadata, enforceTransparency);
+        BufferedImage bi = read(stream, readMetadata, enforceTransparency, readParamFunction);
         if (bi == null) {
             stream.close();
@@ -1687,4 +1731,9 @@
      */
     public static BufferedImage read(ImageInputStream stream, boolean readMetadata, boolean enforceTransparency) throws IOException {
+        return read(stream, readMetadata, enforceTransparency, ImageReader::getDefaultReadParam);
+    }
+
+    private static BufferedImage read(ImageInputStream stream, boolean readMetadata, boolean enforceTransparency,
+                                      Function<ImageReader, ImageReadParam> readParamFunction) throws IOException {
         CheckParameterUtil.ensureParameterNotNull(stream, "stream");
 
@@ -1695,6 +1744,6 @@
 
         ImageReader reader = iter.next();
-        ImageReadParam param = reader.getDefaultReadParam();
         reader.setInput(stream, true, !readMetadata && !enforceTransparency);
+        ImageReadParam param = readParamFunction.apply(reader);
         BufferedImage bi = null;
         try { // NOPMD
