Ticket #15476: josm-13038-geoimage-zoom-regression-fix-and-minor-usability-enhancements.patch

File josm-13038-geoimage-zoom-regression-fix-and-minor-usability-enhancements.patch, 34.7 KB (added by cmuelle8, 15 months ago)

josm-13038-geoimage-zoom-regression-fix-and-minor-usability-enhancements.patch

  • src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java

     
    2525import java.io.File;
    2626
    2727import javax.swing.JComponent;
     28import javax.swing.SwingUtilities;
    2829
     30import org.openstreetmap.josm.data.preferences.BooleanProperty;
     31import org.openstreetmap.josm.data.preferences.DoubleProperty;
    2932import org.openstreetmap.josm.spi.preferences.Config;
     33import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
     34import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
    3035import org.openstreetmap.josm.tools.ExifReader;
    3136import org.openstreetmap.josm.tools.ImageProvider;
    3237import org.openstreetmap.josm.tools.Logging;
     
    3641 *
    3742 * Offers basic mouse interaction (zoom, drag) and on-screen text.
    3843 */
    39 public class ImageDisplay extends JComponent {
     44public class ImageDisplay extends JComponent implements PreferenceChangedListener {
    4045
    4146    /** The file that is currently displayed */
    4247    private File file;
     
    4954
    5055    /** The rectangle (in image coordinates) of the image that is visible. This rectangle is calculated
    5156     * each time the zoom is modified */
    52     private Rectangle visibleRect;
     57    private VisRect visibleRect;
    5358
    5459    /** When a selection is done, the rectangle of the selection (in image coordinates) */
    55     private Rectangle selectedRect;
     60    private VisRect selectedRect;
    5661
    5762    /** The tracker to load the images */
    5863    private final MediaTracker tracker = new MediaTracker(this);
    5964
    6065    private String osdText;
    6166
    62     private static final int DRAG_BUTTON = Config.getPref().getBoolean("geoimage.agpifo-style-drag-and-zoom", false) ? 1 : 3;
    63     private static final int ZOOM_BUTTON = DRAG_BUTTON == 1 ? 3 : 1;
     67    private static final BooleanProperty AGPIFO_STYLE2 =
     68        new BooleanProperty("geoimage.agpifo-style-drag-and-zoom", false);
     69    private static int DRAG_BUTTON;
     70    private static int ZOOM_BUTTON;
     71
     72    /** Alternative to mouse wheel zoom; esp. handy if no mouse wheel is present **/
     73    private static final BooleanProperty ZOOM_ON_CLICK =
     74        new BooleanProperty("geoimage.use-mouse-clicks-to-zoom", true);
     75
     76    /** Zoom factor when click or wheel zooming **/
     77    private static final DoubleProperty ZOOM_STEP =
     78        new DoubleProperty("geoimage.zoom-step-factor", 3 / 2.0);
     79
     80    /** Maximum zoom allowed **/
     81    private static final DoubleProperty MAX_ZOOM =
     82        new DoubleProperty("geoimage.maximum-zoom-scale", 2.0);
     83
     84    /** Use bilinear filtering **/
     85    private static final BooleanProperty BILIN_DOWNSAMP =
     86        new BooleanProperty("geoimage.bilinear-downsampling-progressive", true);
     87    private static final BooleanProperty BILIN_UPSAMP =
     88        new BooleanProperty("geoimage.bilinear-upsampling", false);
     89    private static double BILIN_UPPER;
     90    private static double BILIN_LOWER;
     91
     92    @Override
     93    public void preferenceChanged(PreferenceChangeEvent e) {
     94        if (e == null ||
     95            e.getKey().equals(AGPIFO_STYLE2.getKey()))
     96        {
     97            DRAG_BUTTON = AGPIFO_STYLE2.get() ? 1 : 3;
     98            ZOOM_BUTTON = DRAG_BUTTON == 1 ? 3 : 1;
     99        }
     100        if (e == null ||
     101            e.getKey().equals(MAX_ZOOM.getKey()) ||
     102            e.getKey().equals(BILIN_DOWNSAMP.getKey()) ||
     103            e.getKey().equals(BILIN_UPSAMP.getKey()))
     104        {
     105            BILIN_UPPER = (BILIN_UPSAMP.get() ? 2*MAX_ZOOM.get() : (BILIN_DOWNSAMP.get() ? 0.5 : 0));
     106            BILIN_LOWER = (BILIN_DOWNSAMP.get() ? 0 : 1);
     107        }
     108    }
     109
     110    /** Manage the visible rectangle of an image with full bounds stored in init. **/
     111    public static class VisRect extends Rectangle {
     112        private final Rectangle init;
     113
     114        public VisRect(int x, int y, int width, int height) {
     115            super(x, y, width, height);
     116            init = new Rectangle(this);
     117        }
     118
     119        public VisRect(int x, int y, int width, int height, VisRect peer) {
     120            super(x, y, width, height);
     121            init = peer.init;
     122        }
     123
     124        public VisRect(VisRect v) {
     125            super(v);
     126            init = v.init;
     127        }
     128
     129        public VisRect() {
     130            this(0, 0, 0, 0);
     131        }
     132
     133        public boolean isFullView() {
     134            return init.equals(this);
     135        }
     136
     137        public boolean isFullView1D() {
     138            return (init.x == x && init.width == width)
     139                || (init.y == y && init.height == height);
     140        }
     141
     142        public void reset() {
     143            setBounds(init);
     144        }
     145
     146        public void checkRectPos() {
     147            if (x < 0) {
     148                x = 0;
     149            }
     150            if (y < 0) {
     151                y = 0;
     152            }
     153            if (x + width > init.width) {
     154                x = init.width - width;
     155            }
     156            if (y + height > init.height) {
     157                y = init.height - height;
     158            }
     159        }
     160
     161        public void checkRectSize() {
     162            if (width > init.width) {
     163                width = init.width;
     164            }
     165            if (height > init.height) {
     166                height = init.height;
     167            }
     168        }
     169
     170        public void checkPointInside(Point p) {
     171            if (p.x < x) {
     172                p.x = x;
     173            }
     174            if (p.x > x + width) {
     175                p.x = x + width;
     176            }
     177            if (p.y < y) {
     178                p.y = y;
     179            }
     180            if (p.y > y + height) {
     181                p.y = y + height;
     182            }
     183        }
     184    }
    64185
    65186    /** The thread that reads the images. */
    66187    private class LoadImageRunnable implements Runnable {
     
    107228
    108229                if (!error) {
    109230                    ImageDisplay.this.image = img;
    110                     visibleRect = new Rectangle(0, 0, img.getWidth(null), img.getHeight(null));
     231                    visibleRect = new VisRect(0, 0, img.getWidth(null), img.getHeight(null));
    111232
    112233                    final int w = (int) visibleRect.getWidth();
    113234                    final int h = (int) visibleRect.getHeight();
     
    143264
    144265    private class ImgDisplayMouseListener implements MouseListener, MouseWheelListener, MouseMotionListener {
    145266
    146         private boolean mouseIsDragging;
    147         private long lastTimeForMousePoint;
     267        private MouseEvent lastMouseEvent;
    148268        private Point mousePointInImg;
    149269
    150         /** Zoom in and out, trying to preserve the point of the image that was under the mouse cursor
    151          * at the same place */
    152         @Override
    153         public void mouseWheelMoved(MouseWheelEvent e) {
     270        private boolean mouseIsDragging(MouseEvent e) {
     271            return (DRAG_BUTTON == 1 && SwingUtilities.isLeftMouseButton(e)) ||
     272                   (DRAG_BUTTON == 2 && SwingUtilities.isMiddleMouseButton(e)) ||
     273                   (DRAG_BUTTON == 3 && SwingUtilities.isRightMouseButton(e));
     274        }
     275
     276        private boolean mouseIsZoomSelecting(MouseEvent e) {
     277            return (ZOOM_BUTTON == 1 && SwingUtilities.isLeftMouseButton(e)) ||
     278                   (ZOOM_BUTTON == 2 && SwingUtilities.isMiddleMouseButton(e)) ||
     279                   (ZOOM_BUTTON == 3 && SwingUtilities.isRightMouseButton(e));
     280        }
     281
     282        private boolean isAtMaxZoom(Rectangle visibleRect) {
     283            return (visibleRect.width == (int) (getSize().width / MAX_ZOOM.get()) ||
     284                    visibleRect.height == (int) (getSize().height / MAX_ZOOM.get()));
     285        }
     286
     287        private void mouseWheelMovedImpl(int x, int y, int rotation, boolean refreshMousePointInImg) {
    154288            File file;
    155289            Image image;
    156             Rectangle visibleRect;
     290            VisRect visibleRect;
    157291
    158292            synchronized (ImageDisplay.this) {
    159293                file = ImageDisplay.this.file;
     
    161295                visibleRect = ImageDisplay.this.visibleRect;
    162296            }
    163297
    164             mouseIsDragging = false;
    165298            selectedRect = null;
    166299
    167300            if (image == null)
    168301                return;
    169302
    170             // Calculate the mouse cursor position in image coordinates, so that we can center the zoom
    171             // on that mouse position.
    172             // To avoid issues when the user tries to zoom in on the image borders, this point is not calculated
    173             // again if there was less than 1.5seconds since the last event.
    174             if (e.getWhen() - lastTimeForMousePoint > 1500 || mousePointInImg == null) {
    175                 lastTimeForMousePoint = e.getWhen();
    176                 mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
    177             }
     303            // Calculate the mouse cursor position in image coordinates to center the zoom.
     304            if (refreshMousePointInImg)
     305                mousePointInImg = comp2imgCoord(visibleRect, x, y, getSize());
    178306
    179             // Applicate the zoom to the visible rectangle in image coordinates
    180             if (e.getWheelRotation() > 0) {
    181                 visibleRect.width = visibleRect.width * 3 / 2;
    182                 visibleRect.height = visibleRect.height * 3 / 2;
     307            // Apply the zoom to the visible rectangle in image coordinates
     308            if (rotation > 0) {
     309                visibleRect.width = (int) (visibleRect.width * ZOOM_STEP.get());
     310                visibleRect.height = (int) (visibleRect.height * ZOOM_STEP.get());
    183311            } else {
    184                 visibleRect.width = visibleRect.width * 2 / 3;
    185                 visibleRect.height = visibleRect.height * 2 / 3;
     312                visibleRect.width = (int) (visibleRect.width / ZOOM_STEP.get());
     313                visibleRect.height = (int) (visibleRect.height / ZOOM_STEP.get());
    186314            }
    187315
    188             // Check that the zoom doesn't exceed 2:1
    189             if (visibleRect.width < getSize().width / 2) {
    190                 visibleRect.width = getSize().width / 2;
     316            // Check that the zoom doesn't exceed MAX_ZOOM:1
     317            if (visibleRect.width < getSize().width / MAX_ZOOM.get()) {
     318                visibleRect.width = (int) (getSize().width / MAX_ZOOM.get());
    191319            }
    192             if (visibleRect.height < getSize().height / 2) {
    193                 visibleRect.height = getSize().height / 2;
     320            if (visibleRect.height < getSize().height / MAX_ZOOM.get()) {
     321                visibleRect.height = (int) (getSize().height / MAX_ZOOM.get());
    194322            }
    195323
    196324            // Set the same ratio for the visible rectangle and the display area
     
    203331            }
    204332
    205333            // The size of the visible rectangle is limited by the image size.
    206             checkVisibleRectSize(image, visibleRect);
     334            visibleRect.checkRectSize();
    207335
    208336            // Set the position of the visible rectangle, so that the mouse cursor doesn't move on the image.
    209337            Rectangle drawRect = calculateDrawImageRectangle(visibleRect, getSize());
    210             visibleRect.x = mousePointInImg.x + ((drawRect.x - e.getX()) * visibleRect.width) / drawRect.width;
    211             visibleRect.y = mousePointInImg.y + ((drawRect.y - e.getY()) * visibleRect.height) / drawRect.height;
     338            visibleRect.x = mousePointInImg.x + ((drawRect.x - x) * visibleRect.width) / drawRect.width;
     339            visibleRect.y = mousePointInImg.y + ((drawRect.y - y) * visibleRect.height) / drawRect.height;
    212340
    213341            // The position is also limited by the image size
    214             checkVisibleRectPos(image, visibleRect);
     342            visibleRect.checkRectPos();
    215343
    216344            synchronized (ImageDisplay.this) {
    217345                if (ImageDisplay.this.file == file) {
     
    221349            ImageDisplay.this.repaint();
    222350        }
    223351
     352        /** Zoom in and out, trying to preserve the point of the image that was under the mouse cursor
     353         * at the same place */
     354        @Override
     355        public void mouseWheelMoved(MouseWheelEvent e) {
     356            boolean refreshMousePointInImg = false;
     357
     358            // To avoid issues when the user tries to zoom in on the image borders, this
     359            // point is not recalculated as long as e occurs at roughly the same position.
     360            if (lastMouseEvent == null || mousePointInImg == null ||
     361                ((lastMouseEvent.getX()-e.getX())*(lastMouseEvent.getX()-e.getX())
     362                +(lastMouseEvent.getY()-e.getY())*(lastMouseEvent.getY()-e.getY()) > 4*4))
     363            {
     364                lastMouseEvent = e;
     365                refreshMousePointInImg = true;
     366            }
     367
     368            mouseWheelMovedImpl(e.getX(), e.getY(), e.getWheelRotation(), refreshMousePointInImg);
     369        }
     370
    224371        /** Center the display on the point that has been clicked */
    225372        @Override
    226373        public void mouseClicked(MouseEvent e) {
    227374            // Move the center to the clicked point.
    228375            File file;
    229376            Image image;
    230             Rectangle visibleRect;
     377            VisRect visibleRect;
    231378
    232379            synchronized (ImageDisplay.this) {
    233380                file = ImageDisplay.this.file;
     
    238385            if (image == null)
    239386                return;
    240387
    241             if (e.getButton() != DRAG_BUTTON)
    242                 return;
     388            if (ZOOM_ON_CLICK.get()) {
     389                // click notions are less coherent than wheel, refresh mousePointInImg on each click
     390                lastMouseEvent = null;
     391
     392                if (mouseIsZoomSelecting(e) && !isAtMaxZoom(visibleRect)) {
     393                    // zoom in if clicked with the zoom button
     394                    mouseWheelMovedImpl(e.getX(), e.getY(), -1, true);
     395                    return;
     396                }
     397                if (mouseIsDragging(e)) {
     398                    // zoom out if clicked with the drag button
     399                    mouseWheelMovedImpl(e.getX(), e.getY(), 1, true);
     400                    return;
     401                }
     402            }
    243403
    244404            // Calculate the translation to set the clicked point the center of the view.
    245405            Point click = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
     
    248408            visibleRect.x += click.x - center.x;
    249409            visibleRect.y += click.y - center.y;
    250410
    251             checkVisibleRectPos(image, visibleRect);
     411            visibleRect.checkRectPos();
    252412
    253413            synchronized (ImageDisplay.this) {
    254414                if (ImageDisplay.this.file == file) {
     
    262422         * a picture part) */
    263423        @Override
    264424        public void mousePressed(MouseEvent e) {
    265             if (image == null) {
    266                 mouseIsDragging = false;
    267                 selectedRect = null;
    268                 return;
    269             }
    270 
    271425            Image image;
    272             Rectangle visibleRect;
     426            VisRect visibleRect;
    273427
    274428            synchronized (ImageDisplay.this) {
    275429                image = ImageDisplay.this.image;
     
    279433            if (image == null)
    280434                return;
    281435
    282             if (e.getButton() == DRAG_BUTTON) {
    283                 mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
    284                 mouseIsDragging = true;
    285                 selectedRect = null;
    286             } else if (e.getButton() == ZOOM_BUTTON) {
     436            selectedRect = null;
     437
     438            if (mouseIsDragging(e) || mouseIsZoomSelecting(e))
    287439                mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
    288                 checkPointInVisibleRect(mousePointInImg, visibleRect);
    289                 mouseIsDragging = false;
    290                 selectedRect = new Rectangle(mousePointInImg.x, mousePointInImg.y, 0, 0);
    291                 ImageDisplay.this.repaint();
    292             } else {
    293                 mouseIsDragging = false;
    294                 selectedRect = null;
    295             }
    296440        }
    297441
    298442        @Override
    299443        public void mouseDragged(MouseEvent e) {
    300             if (!mouseIsDragging && selectedRect == null)
     444            if (!mouseIsDragging(e) && !mouseIsZoomSelecting(e))
    301445                return;
    302446
    303447            File file;
    304448            Image image;
    305             Rectangle visibleRect;
     449            VisRect visibleRect;
    306450
    307451            synchronized (ImageDisplay.this) {
    308452                file = ImageDisplay.this.file;
     
    310454                visibleRect = ImageDisplay.this.visibleRect;
    311455            }
    312456
    313             if (image == null) {
    314                 mouseIsDragging = false;
    315                 selectedRect = null;
     457            if (image == null)
    316458                return;
    317             }
    318459
    319             if (mouseIsDragging) {
     460            if (mouseIsDragging(e)) {
    320461                Point p = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
    321462                visibleRect.x += mousePointInImg.x - p.x;
    322463                visibleRect.y += mousePointInImg.y - p.y;
    323                 checkVisibleRectPos(image, visibleRect);
     464                visibleRect.checkRectPos();
    324465                synchronized (ImageDisplay.this) {
    325466                    if (ImageDisplay.this.file == file) {
    326467                        ImageDisplay.this.visibleRect = visibleRect;
    327468                    }
    328469                }
    329470                ImageDisplay.this.repaint();
     471            }
    330472
    331             } else if (selectedRect != null) {
     473            if (mouseIsZoomSelecting(e)) {
     474                //if (selectedRect == null) {
     475                //    checkPointInVisibleRect(mousePointInImg, visibleRect);
     476                //    selectedRect = new Rectangle(mousePointInImg.x, mousePointInImg.y, 0, 0);
     477                //}
    332478                Point p = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
    333                 checkPointInVisibleRect(p, visibleRect);
    334                 Rectangle rect = new Rectangle(
     479                visibleRect.checkPointInside(p);
     480                VisRect selectedRect = new VisRect(
    335481                        p.x < mousePointInImg.x ? p.x : mousePointInImg.x,
    336482                        p.y < mousePointInImg.y ? p.y : mousePointInImg.y,
    337483                        p.x < mousePointInImg.x ? mousePointInImg.x - p.x : p.x - mousePointInImg.x,
    338                         p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y);
    339                 checkVisibleRectSize(image, rect);
    340                 checkVisibleRectPos(image, rect);
    341                 ImageDisplay.this.selectedRect = rect;
     484                        p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y,
     485                        visibleRect);
     486                selectedRect.checkRectSize();
     487                selectedRect.checkRectPos();
     488                ImageDisplay.this.selectedRect = selectedRect;
    342489                ImageDisplay.this.repaint();
    343490            }
    344491
     
    346493
    347494        @Override
    348495        public void mouseReleased(MouseEvent e) {
    349             if (!mouseIsDragging && selectedRect == null)
     496            if (!mouseIsZoomSelecting(e) || selectedRect == null)
    350497                return;
    351498
    352499            File file;
     
    358505            }
    359506
    360507            if (image == null) {
    361                 mouseIsDragging = false;
    362                 selectedRect = null;
    363508                return;
    364509            }
    365510
    366             if (mouseIsDragging) {
    367                 mouseIsDragging = false;
    368 
    369             } else if (selectedRect != null) {
     511            {
    370512                int oldWidth = selectedRect.width;
    371513                int oldHeight = selectedRect.height;
    372514
    373                 // Check that the zoom doesn't exceed 2:1
    374                 if (selectedRect.width < getSize().width / 2) {
    375                     selectedRect.width = getSize().width / 2;
     515                // Check that the zoom doesn't exceed MAX_ZOOM:1
     516                if (selectedRect.width < getSize().width / MAX_ZOOM.get()) {
     517                    selectedRect.width = (int) (getSize().width / MAX_ZOOM.get());
    376518                }
    377                 if (selectedRect.height < getSize().height / 2) {
    378                     selectedRect.height = getSize().height / 2;
     519                if (selectedRect.height < getSize().height / MAX_ZOOM.get()) {
     520                    selectedRect.height = (int) (getSize().height / MAX_ZOOM.get());
    379521                }
    380522
    381523                // Set the same ratio for the visible rectangle and the display area
     
    395537                    selectedRect.y -= (selectedRect.height - oldHeight) / 2;
    396538                }
    397539
    398                 checkVisibleRectSize(image, selectedRect);
    399                 checkVisibleRectPos(image, selectedRect);
     540                selectedRect.checkRectSize();
     541                selectedRect.checkRectPos();
    400542
    401543                synchronized (ImageDisplay.this) {
    402544                    if (file == ImageDisplay.this.file) {
    403                         ImageDisplay.this.visibleRect = selectedRect;
     545                        ImageDisplay.this.visibleRect.setBounds(selectedRect);
    404546                    }
    405547                }
    406548                selectedRect = null;
     
    422564        public void mouseMoved(MouseEvent e) {
    423565            // Do nothing
    424566        }
    425 
    426         private void checkPointInVisibleRect(Point p, Rectangle visibleRect) {
    427             if (p.x < visibleRect.x) {
    428                 p.x = visibleRect.x;
    429             }
    430             if (p.x > visibleRect.x + visibleRect.width) {
    431                 p.x = visibleRect.x + visibleRect.width;
    432             }
    433             if (p.y < visibleRect.y) {
    434                 p.y = visibleRect.y;
    435             }
    436             if (p.y > visibleRect.y + visibleRect.height) {
    437                 p.y = visibleRect.y + visibleRect.height;
    438             }
    439         }
    440567    }
    441568
    442569    /**
     
    447574        addMouseListener(mouseListener);
    448575        addMouseWheelListener(mouseListener);
    449576        addMouseMotionListener(mouseListener);
     577
     578        Config.getPref().addPreferenceChangeListener(this);
     579        preferenceChanged(null);
    450580    }
    451581
    452582    public void setImage(File file, Integer orientation) {
    453583        synchronized (this) {
    454584            this.file = file;
    455585            image = null;
    456             selectedRect = null;
    457586            errorLoading = false;
    458587        }
    459588        repaint();
     
    475604    public void paintComponent(Graphics g) {
    476605        Image image;
    477606        File file;
    478         Rectangle visibleRect;
     607        VisRect visibleRect;
    479608        boolean errorLoading;
    480609
    481610        synchronized (this) {
     
    510639                    (int) ((size.width - noImageSize.getWidth()) / 2),
    511640                    (int) ((size.height - noImageSize.getHeight()) / 2));
    512641        } else {
     642            Rectangle r = new Rectangle(visibleRect);
    513643            Rectangle target = calculateDrawImageRectangle(visibleRect, size);
    514             // See https://community.oracle.com/docs/DOC-983611 - The Perils of Image.getScaledInstance()
    515             // Pre-scale image when downscaling by more than two times to avoid aliasing from default algorithm
    516             if (selectedRect == null && (target.width < visibleRect.width/2 || target.height < visibleRect.height/2)) {
    517                 BufferedImage buffImage = ImageProvider.toBufferedImage(image);
    518                 g.drawImage(ImageProvider.createScaledImage(buffImage, target.width, target.height, RenderingHints.VALUE_INTERPOLATION_BILINEAR),
    519                         target.x, target.y, target.x + target.width, target.y + target.height,
    520                         visibleRect.x, visibleRect.y, visibleRect.x + target.width, visibleRect.y + target.height,
    521                         null);
     644            double scale = target.width / (double)r.width; // pixel ratio is 1:1
     645
     646            if (selectedRect == null && BILIN_LOWER < scale && scale < BILIN_UPPER) {
     647                BufferedImage bi = ImageProvider.toBufferedImage(image, r);
     648                r.x = r.y = 0;
     649
     650                // See https://community.oracle.com/docs/DOC-983611 - The Perils of Image.getScaledInstance()
     651                // Pre-scale image when downscaling by more than two times to avoid aliasing from default algorithm
     652                image = ImageProvider.createScaledImage(bi, target.width, target.height,
     653                            RenderingHints.VALUE_INTERPOLATION_BILINEAR);
     654                r.width = target.width;
     655                r.height = target.height;
    522656            } else {
    523                 g.drawImage(image,
    524                         target.x, target.y, target.x + target.width, target.y + target.height,
    525                         visibleRect.x, visibleRect.y, visibleRect.x + visibleRect.width, visibleRect.y + visibleRect.height,
    526                         null);
     657                // if target and r cause drawImage to scale image region to a tmp buffer exceeding
     658                // its bounds, it will silently fail; crop with r first in such cases
     659                // (might be impl. dependent, exhibited by openjdk 1.8.0_151)
     660                if (scale*(r.x+r.width) > Short.MAX_VALUE || scale*(r.y+r.height) > Short.MAX_VALUE) {
     661                    image = ImageProvider.toBufferedImage(image, r);
     662                    r.x = r.y = 0;
     663                }
    527664            }
     665
     666            g.drawImage(image,
     667                    target.x, target.y, target.x + target.width, target.y + target.height,
     668                    r.x, r.y, r.x + r.width, r.y + r.height, null);
     669
    528670            if (selectedRect != null) {
    529671                Point topLeft = img2compCoord(visibleRect, selectedRect.x, selectedRect.y, size);
    530672                Point bottomRight = img2compCoord(visibleRect,
     
    576718        }
    577719    }
    578720
    579     static Point img2compCoord(Rectangle visibleRect, int xImg, int yImg, Dimension compSize) {
     721    static Point img2compCoord(VisRect visibleRect, int xImg, int yImg, Dimension compSize) {
    580722        Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize);
    581723        return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) / visibleRect.width,
    582724                drawRect.y + ((yImg - visibleRect.y) * drawRect.height) / visibleRect.height);
    583725    }
    584726
    585     static Point comp2imgCoord(Rectangle visibleRect, int xComp, int yComp, Dimension compSize) {
     727    static Point comp2imgCoord(VisRect visibleRect, int xComp, int yComp, Dimension compSize) {
    586728        Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize);
    587         return new Point(visibleRect.x + ((xComp - drawRect.x) * visibleRect.width) / drawRect.width,
    588                 visibleRect.y + ((yComp - drawRect.y) * visibleRect.height) / drawRect.height);
     729        Point p = new Point(
     730                        ((xComp - drawRect.x) * visibleRect.width),
     731                        ((yComp - drawRect.y) * visibleRect.height));
     732        p.x += (((p.x % drawRect.width)<<1) >= drawRect.width) ? drawRect.width : 0;
     733        p.y += (((p.y % drawRect.height)<<1) >= drawRect.height) ? drawRect.height : 0;
     734        p.x = visibleRect.x + p.x / drawRect.width;
     735        p.y = visibleRect.y + p.y / drawRect.height;
     736        return p;
    589737    }
    590738
    591739    static Point getCenterImgCoord(Rectangle visibleRect) {
     
    593741                         visibleRect.y + visibleRect.height / 2);
    594742    }
    595743
    596     static Rectangle calculateDrawImageRectangle(Rectangle visibleRect, Dimension compSize) {
     744    static VisRect calculateDrawImageRectangle(VisRect visibleRect, Dimension compSize) {
    597745        return calculateDrawImageRectangle(visibleRect, new Rectangle(0, 0, compSize.width, compSize.height));
    598746    }
    599747
     
    604752     * @param compRect the part of the component where the image should be drawn (in component coordinates)
    605753     * @return the part of compRect with the same width/height ratio as the image
    606754     */
    607     static Rectangle calculateDrawImageRectangle(Rectangle imgRect, Rectangle compRect) {
     755    static VisRect calculateDrawImageRectangle(VisRect imgRect, Rectangle compRect) {
    608756        int x = 0;
    609757        int y = 0;
    610758        int w = compRect.width;
     
    621769                y = (compRect.height - h) / 2;
    622770            }
    623771        }
    624         return new Rectangle(x + compRect.x, y + compRect.y, w, h);
     772
     773        /* overscan to prevent empty edges when zooming in to zoom scales > 2:1 */
     774        if (w > imgRect.width && h > imgRect.height && !imgRect.isFullView1D()) {
     775            if (wFact != hFact) {
     776                if (wFact > hFact) {
     777                    w = compRect.width;
     778                    x = 0;
     779                    h = wFact / imgRect.width;
     780                    y = (compRect.height - h) / 2;
     781                } else {
     782                    h = compRect.height;
     783                    y = 0;
     784                    w = hFact / imgRect.height;
     785                    x = (compRect.width - w) / 2;
     786
     787                }
     788            }
     789        }
     790
     791        return new VisRect(x + compRect.x, y + compRect.y, w, h, imgRect);
    625792    }
    626793
    627794    public void zoomBestFitOrOne() {
    628795        File file;
    629796        Image image;
    630         Rectangle visibleRect;
     797        VisRect visibleRect;
    631798
    632799        synchronized (this) {
    633800            file = this.file;
     
    640807
    641808        if (visibleRect.width != image.getWidth(null) || visibleRect.height != image.getHeight(null)) {
    642809            // The display is not at best fit. => Zoom to best fit
    643             visibleRect = new Rectangle(0, 0, image.getWidth(null), image.getHeight(null));
    644 
     810            visibleRect.reset();
    645811        } else {
    646812            // The display is at best fit => zoom to 1:1
    647813            Point center = getCenterImgCoord(visibleRect);
    648             visibleRect = new Rectangle(center.x - getWidth() / 2, center.y - getHeight() / 2,
     814            visibleRect.setBounds(center.x - getWidth() / 2, center.y - getHeight() / 2,
    649815                    getWidth(), getHeight());
    650             checkVisibleRectPos(image, visibleRect);
     816            visibleRect.checkRectSize();
     817            visibleRect.checkRectPos();
    651818        }
    652819
    653820        synchronized (this) {
     
    657824        }
    658825        repaint();
    659826    }
    660 
    661     static void checkVisibleRectPos(Image image, Rectangle visibleRect) {
    662         if (visibleRect.x < 0) {
    663             visibleRect.x = 0;
    664         }
    665         if (visibleRect.y < 0) {
    666             visibleRect.y = 0;
    667         }
    668         if (visibleRect.x + visibleRect.width > image.getWidth(null)) {
    669             visibleRect.x = image.getWidth(null) - visibleRect.width;
    670         }
    671         if (visibleRect.y + visibleRect.height > image.getHeight(null)) {
    672             visibleRect.y = image.getHeight(null) - visibleRect.height;
    673         }
    674     }
    675 
    676     static void checkVisibleRectSize(Image image, Rectangle visibleRect) {
    677         if (visibleRect.width > image.getWidth(null)) {
    678             visibleRect.width = image.getWidth(null);
    679         }
    680         if (visibleRect.height > image.getHeight(null)) {
    681             visibleRect.height = image.getHeight(null);
    682         }
    683     }
    684827}
  • src/org/openstreetmap/josm/tools/ImageProvider.java

     
    1111import java.awt.GraphicsEnvironment;
    1212import java.awt.Image;
    1313import java.awt.Point;
     14import java.awt.Rectangle;
    1415import java.awt.RenderingHints;
    1516import java.awt.Toolkit;
    1617import java.awt.Transparency;
     
    14201421        do {
    14211422            if (w > targetWidth) {
    14221423                w /= 2;
    1423                 if (w < targetWidth) {
    1424                     w = targetWidth;
    1425                 }
     1424            }
     1425            if (w < targetWidth) {
     1426                w = targetWidth;
    14261427            }
    14271428            if (h > targetHeight) {
    14281429                h /= 2;
    1429                 if (h < targetHeight) {
    1430                     h = targetHeight;
    1431                 }
     1430            }
     1431            if (h < targetHeight) {
     1432                h = targetHeight;
    14321433            }
    14331434            BufferedImage tmp = new BufferedImage(w, h, type);
    14341435            Graphics2D g2 = tmp.createGraphics();
     
    19751976            return buffImage;
    19761977        }
    19771978    }
     1979
     1980    /**
     1981     * Converts an {@link Rectangle} area of {@link Image} to a {@link BufferedImage} instance.
     1982     * @param image image to convert
     1983     * @param crop_area rectangle to crop image with
     1984     * @return a {@code BufferedImage} instance for the cropped area of {@code Image}.
     1985     */
     1986    public static BufferedImage toBufferedImage(Image image, Rectangle crop_area) {
     1987        BufferedImage buffImage = null;
     1988
     1989        Rectangle r = new Rectangle(image.getWidth(null), image.getHeight(null));
     1990        if (r.intersection(crop_area).equals(crop_area)) {
     1991            buffImage = new BufferedImage(crop_area.width, crop_area.height, BufferedImage.TYPE_INT_ARGB);
     1992            Graphics2D g2 = buffImage.createGraphics();
     1993            g2.drawImage(image,
     1994                0, 0, crop_area.width, crop_area.height,
     1995                crop_area.x, crop_area.y,
     1996                crop_area.x + crop_area.width, crop_area.y + crop_area.height,
     1997                null);
     1998            g2.dispose();
     1999        }
     2000        return buffImage;
     2001    }
    19782002}
  • test/unit/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplayTest.java

     
    88
    99import org.junit.Rule;
    1010import org.junit.Test;
     11import org.openstreetmap.josm.gui.layer.geoimage.ImageDisplay.VisRect;
    1112import org.openstreetmap.josm.testutils.JOSMTestRules;
    1213
    1314import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     
    2930    @Test
    3031    public void testCalculateDrawImageRectangle() {
    3132        assertEquals(new Rectangle(),
    32                 ImageDisplay.calculateDrawImageRectangle(new Rectangle(), new Dimension()));
     33                ImageDisplay.calculateDrawImageRectangle(new VisRect(), new Dimension()));
    3334        assertEquals(new Rectangle(0, 0, 10, 5),
    34                 ImageDisplay.calculateDrawImageRectangle(new Rectangle(0, 0, 10, 5), new Dimension(10, 5)));
     35                ImageDisplay.calculateDrawImageRectangle(new VisRect(0, 0, 10, 5), new Dimension(10, 5)));
    3536        assertEquals(new Rectangle(0, 0, 10, 5),
    36                 ImageDisplay.calculateDrawImageRectangle(new Rectangle(0, 0, 20, 10), new Dimension(10, 5)));
     37                ImageDisplay.calculateDrawImageRectangle(new VisRect(0, 0, 20, 10), new Dimension(10, 5)));
    3738        assertEquals(new Rectangle(0, 0, 20, 10),
    38                 ImageDisplay.calculateDrawImageRectangle(new Rectangle(0, 0, 10, 5), new Dimension(20, 10)));
     39                ImageDisplay.calculateDrawImageRectangle(new VisRect(0, 0, 10, 5), new Dimension(20, 10)));
    3940        assertEquals(new Rectangle(5, 0, 24, 12),
    40                 ImageDisplay.calculateDrawImageRectangle(new Rectangle(0, 0, 10, 5), new Dimension(35, 12)));
     41                ImageDisplay.calculateDrawImageRectangle(new VisRect(0, 0, 10, 5), new Dimension(35, 12)));
    4142        assertEquals(new Rectangle(0, 1, 8, 4),
    42                 ImageDisplay.calculateDrawImageRectangle(new Rectangle(0, 0, 10, 5), new Dimension(8, 6)));
     43                ImageDisplay.calculateDrawImageRectangle(new VisRect(0, 0, 10, 5), new Dimension(8, 6)));
    4344    }
    4445}