source: josm/trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java@ 13129

Last change on this file since 13129 was 13129, checked in by Don-vip, 6 years ago

see #15476 - fix checkstyle/pmd warnings, add some javadoc

  • Property svn:eol-style set to native
File size: 31.2 KB
RevLine 
[8378]1// License: GPL. For details, see LICENSE file.
[2566]2package org.openstreetmap.josm.gui.layer.geoimage;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Color;
7import java.awt.Dimension;
8import java.awt.FontMetrics;
9import java.awt.Graphics;
[4241]10import java.awt.Graphics2D;
[2566]11import java.awt.Image;
12import java.awt.MediaTracker;
13import java.awt.Point;
14import java.awt.Rectangle;
[13038]15import java.awt.RenderingHints;
[2566]16import java.awt.Toolkit;
17import java.awt.event.MouseEvent;
18import java.awt.event.MouseListener;
19import java.awt.event.MouseMotionListener;
20import java.awt.event.MouseWheelEvent;
21import java.awt.event.MouseWheelListener;
[4241]22import java.awt.geom.AffineTransform;
[2566]23import java.awt.geom.Rectangle2D;
[4241]24import java.awt.image.BufferedImage;
[2566]25import java.io.File;
26
27import javax.swing.JComponent;
[13127]28import javax.swing.SwingUtilities;
[2566]29
[13127]30import org.openstreetmap.josm.data.preferences.BooleanProperty;
31import org.openstreetmap.josm.data.preferences.DoubleProperty;
[12846]32import org.openstreetmap.josm.spi.preferences.Config;
[13127]33import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
34import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
[7956]35import org.openstreetmap.josm.tools.ExifReader;
[13038]36import org.openstreetmap.josm.tools.ImageProvider;
[12620]37import org.openstreetmap.josm.tools.Logging;
[2566]38
[12460]39/**
40 * GUI component to display an image (photograph).
41 *
42 * Offers basic mouse interaction (zoom, drag) and on-screen text.
43 */
[13127]44public class ImageDisplay extends JComponent implements PreferenceChangedListener {
[2566]45
46 /** The file that is currently displayed */
[8840]47 private File file;
[2566]48
49 /** The image currently displayed */
[8840]50 private transient Image image;
[6070]51
[2566]52 /** The image currently displayed */
[8840]53 private boolean errorLoading;
[2566]54
55 /** The rectangle (in image coordinates) of the image that is visible. This rectangle is calculated
56 * each time the zoom is modified */
[13127]57 private VisRect visibleRect;
[2566]58
59 /** When a selection is done, the rectangle of the selection (in image coordinates) */
[13127]60 private VisRect selectedRect;
[2566]61
62 /** The tracker to load the images */
[9078]63 private final MediaTracker tracker = new MediaTracker(this);
[2566]64
[8840]65 private String osdText;
[2711]66
[13127]67 private static final BooleanProperty AGPIFO_STYLE2 =
68 new BooleanProperty("geoimage.agpifo-style-drag-and-zoom", false);
[13129]69 private static int dragButton;
70 private static int zoomButton;
[2566]71
[13127]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);
[13129]89 private static double bilinUpper;
90 private static double bilinLower;
[13127]91
92 @Override
93 public void preferenceChanged(PreferenceChangeEvent e) {
94 if (e == null ||
[13129]95 e.getKey().equals(AGPIFO_STYLE2.getKey())) {
96 dragButton = AGPIFO_STYLE2.get() ? 1 : 3;
97 zoomButton = dragButton == 1 ? 3 : 1;
[13127]98 }
99 if (e == null ||
100 e.getKey().equals(MAX_ZOOM.getKey()) ||
101 e.getKey().equals(BILIN_DOWNSAMP.getKey()) ||
[13129]102 e.getKey().equals(BILIN_UPSAMP.getKey())) {
103 bilinUpper = (BILIN_UPSAMP.get() ? 2*MAX_ZOOM.get() : (BILIN_DOWNSAMP.get() ? 0.5 : 0));
104 bilinLower = (BILIN_DOWNSAMP.get() ? 0 : 1);
[13127]105 }
106 }
107
[13129]108 /**
109 * Manage the visible rectangle of an image with full bounds stored in init.
110 * @since 13127
111 */
[13127]112 public static class VisRect extends Rectangle {
113 private final Rectangle init;
114
[13129]115 /**
116 * Constructs a new {@code VisRect}.
117 * @param x the specified X coordinate
118 * @param y the specified Y coordinate
119 * @param width the width of the rectangle
120 * @param height the height of the rectangle
121 */
[13127]122 public VisRect(int x, int y, int width, int height) {
123 super(x, y, width, height);
124 init = new Rectangle(this);
125 }
126
127 public VisRect(int x, int y, int width, int height, VisRect peer) {
128 super(x, y, width, height);
129 init = peer.init;
130 }
131
[13129]132 /**
133 * Constructs a new {@code VisRect} from another one.
134 * @param v rectangle to copy
135 */
[13127]136 public VisRect(VisRect v) {
137 super(v);
138 init = v.init;
139 }
140
[13129]141 /**
142 * Constructs a new empty {@code VisRect}.
143 */
[13127]144 public VisRect() {
145 this(0, 0, 0, 0);
146 }
147
148 public boolean isFullView() {
149 return init.equals(this);
150 }
151
152 public boolean isFullView1D() {
153 return (init.x == x && init.width == width)
154 || (init.y == y && init.height == height);
155 }
156
157 public void reset() {
158 setBounds(init);
159 }
160
161 public void checkRectPos() {
162 if (x < 0) {
163 x = 0;
164 }
165 if (y < 0) {
166 y = 0;
167 }
168 if (x + width > init.width) {
169 x = init.width - width;
170 }
171 if (y + height > init.height) {
172 y = init.height - height;
173 }
174 }
175
176 public void checkRectSize() {
177 if (width > init.width) {
178 width = init.width;
179 }
180 if (height > init.height) {
181 height = init.height;
182 }
183 }
184
185 public void checkPointInside(Point p) {
186 if (p.x < x) {
187 p.x = x;
188 }
189 if (p.x > x + width) {
190 p.x = x + width;
191 }
192 if (p.y < y) {
193 p.y = y;
194 }
195 if (p.y > y + height) {
196 p.y = y + height;
197 }
198 }
199 }
200
[2566]201 /** The thread that reads the images. */
202 private class LoadImageRunnable implements Runnable {
203
[9078]204 private final File file;
205 private final int orientation;
[2566]206
[8836]207 LoadImageRunnable(File file, Integer orientation) {
[2566]208 this.file = file;
[4241]209 this.orientation = orientation == null ? -1 : orientation;
[2566]210 }
211
[6084]212 @Override
[2566]213 public void run() {
214 Image img = Toolkit.getDefaultToolkit().createImage(file.getPath());
215 tracker.addImage(img, 1);
216
217 // Wait for the end of loading
[8444]218 while (!tracker.checkID(1, true)) {
[2566]219 if (this.file != ImageDisplay.this.file) {
220 // The file has changed
221 tracker.removeImage(img);
222 return;
223 }
224 try {
225 Thread.sleep(5);
226 } catch (InterruptedException e) {
[12620]227 Logging.warn("InterruptedException in "+getClass().getSimpleName()+" while loading image "+file.getPath());
[11535]228 Thread.currentThread().interrupt();
[2566]229 }
230 }
231
232 boolean error = tracker.isErrorID(1);
[4698]233 if (img.getWidth(null) < 0 || img.getHeight(null) < 0) {
[2566]234 error = true;
235 }
236
[8510]237 synchronized (ImageDisplay.this) {
[2566]238 if (this.file != ImageDisplay.this.file) {
239 // The file has changed
240 tracker.removeImage(img);
241 return;
242 }
[4241]243
[4698]244 if (!error) {
245 ImageDisplay.this.image = img;
[13127]246 visibleRect = new VisRect(0, 0, img.getWidth(null), img.getHeight(null));
[6070]247
[4698]248 final int w = (int) visibleRect.getWidth();
249 final int h = (int) visibleRect.getHeight();
[6070]250
[7956]251 if (ExifReader.orientationNeedsCorrection(orientation)) {
252 final int hh, ww;
253 if (ExifReader.orientationSwitchesDimensions(orientation)) {
[4698]254 ww = h;
255 hh = w;
[7956]256 } else {
[4698]257 ww = w;
258 hh = h;
259 }
260 final BufferedImage rot = new BufferedImage(ww, hh, BufferedImage.TYPE_INT_RGB);
[7956]261 final AffineTransform xform = ExifReader.getRestoreOrientationTransform(orientation, w, h);
[4698]262 final Graphics2D g = rot.createGraphics();
263 g.drawImage(image, xform, null);
264 g.dispose();
[6070]265
[4698]266 visibleRect.setSize(ww, hh);
267 image.flush();
268 ImageDisplay.this.image = rot;
[4241]269 }
270 }
271
[2566]272 selectedRect = null;
273 errorLoading = error;
274 }
275 tracker.removeImage(img);
276 ImageDisplay.this.repaint();
277 }
278 }
279
280 private class ImgDisplayMouseListener implements MouseListener, MouseWheelListener, MouseMotionListener {
281
[13127]282 private MouseEvent lastMouseEvent;
[8840]283 private Point mousePointInImg;
[2566]284
[13127]285 private boolean mouseIsDragging(MouseEvent e) {
[13129]286 return (dragButton == 1 && SwingUtilities.isLeftMouseButton(e)) ||
287 (dragButton == 2 && SwingUtilities.isMiddleMouseButton(e)) ||
288 (dragButton == 3 && SwingUtilities.isRightMouseButton(e));
[13127]289 }
290
291 private boolean mouseIsZoomSelecting(MouseEvent e) {
[13129]292 return (zoomButton == 1 && SwingUtilities.isLeftMouseButton(e)) ||
293 (zoomButton == 2 && SwingUtilities.isMiddleMouseButton(e)) ||
294 (zoomButton == 3 && SwingUtilities.isRightMouseButton(e));
[13127]295 }
296
297 private boolean isAtMaxZoom(Rectangle visibleRect) {
298 return (visibleRect.width == (int) (getSize().width / MAX_ZOOM.get()) ||
299 visibleRect.height == (int) (getSize().height / MAX_ZOOM.get()));
300 }
301
302 private void mouseWheelMovedImpl(int x, int y, int rotation, boolean refreshMousePointInImg) {
[2566]303 File file;
304 Image image;
[13127]305 VisRect visibleRect;
[2566]306
307 synchronized (ImageDisplay.this) {
308 file = ImageDisplay.this.file;
309 image = ImageDisplay.this.image;
310 visibleRect = ImageDisplay.this.visibleRect;
311 }
312
313 selectedRect = null;
314
[2986]315 if (image == null)
[2566]316 return;
317
[13127]318 // Calculate the mouse cursor position in image coordinates to center the zoom.
319 if (refreshMousePointInImg)
320 mousePointInImg = comp2imgCoord(visibleRect, x, y, getSize());
[2566]321
[13127]322 // Apply the zoom to the visible rectangle in image coordinates
323 if (rotation > 0) {
324 visibleRect.width = (int) (visibleRect.width * ZOOM_STEP.get());
325 visibleRect.height = (int) (visibleRect.height * ZOOM_STEP.get());
[2566]326 } else {
[13127]327 visibleRect.width = (int) (visibleRect.width / ZOOM_STEP.get());
328 visibleRect.height = (int) (visibleRect.height / ZOOM_STEP.get());
[2566]329 }
330
[13127]331 // Check that the zoom doesn't exceed MAX_ZOOM:1
332 if (visibleRect.width < getSize().width / MAX_ZOOM.get()) {
333 visibleRect.width = (int) (getSize().width / MAX_ZOOM.get());
[2566]334 }
[13127]335 if (visibleRect.height < getSize().height / MAX_ZOOM.get()) {
336 visibleRect.height = (int) (getSize().height / MAX_ZOOM.get());
[2566]337 }
338
339 // Set the same ratio for the visible rectangle and the display area
340 int hFact = visibleRect.height * getSize().width;
341 int wFact = visibleRect.width * getSize().height;
342 if (hFact > wFact) {
343 visibleRect.width = hFact / getSize().height;
344 } else {
345 visibleRect.height = wFact / getSize().width;
346 }
347
348 // The size of the visible rectangle is limited by the image size.
[13127]349 visibleRect.checkRectSize();
[2566]350
351 // Set the position of the visible rectangle, so that the mouse cursor doesn't move on the image.
[11539]352 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, getSize());
[13127]353 visibleRect.x = mousePointInImg.x + ((drawRect.x - x) * visibleRect.width) / drawRect.width;
354 visibleRect.y = mousePointInImg.y + ((drawRect.y - y) * visibleRect.height) / drawRect.height;
[2566]355
356 // The position is also limited by the image size
[13127]357 visibleRect.checkRectPos();
[2566]358
[8510]359 synchronized (ImageDisplay.this) {
[2566]360 if (ImageDisplay.this.file == file) {
361 ImageDisplay.this.visibleRect = visibleRect;
362 }
363 }
364 ImageDisplay.this.repaint();
365 }
366
[13127]367 /** Zoom in and out, trying to preserve the point of the image that was under the mouse cursor
368 * at the same place */
369 @Override
370 public void mouseWheelMoved(MouseWheelEvent e) {
371 boolean refreshMousePointInImg = false;
372
373 // To avoid issues when the user tries to zoom in on the image borders, this
374 // point is not recalculated as long as e occurs at roughly the same position.
375 if (lastMouseEvent == null || mousePointInImg == null ||
376 ((lastMouseEvent.getX()-e.getX())*(lastMouseEvent.getX()-e.getX())
377 +(lastMouseEvent.getY()-e.getY())*(lastMouseEvent.getY()-e.getY()) > 4*4)) {
378 lastMouseEvent = e;
379 refreshMousePointInImg = true;
380 }
381
382 mouseWheelMovedImpl(e.getX(), e.getY(), e.getWheelRotation(), refreshMousePointInImg);
383 }
384
[2566]385 /** Center the display on the point that has been clicked */
[6084]386 @Override
[2566]387 public void mouseClicked(MouseEvent e) {
388 // Move the center to the clicked point.
389 File file;
390 Image image;
[13127]391 VisRect visibleRect;
[2566]392
393 synchronized (ImageDisplay.this) {
394 file = ImageDisplay.this.file;
395 image = ImageDisplay.this.image;
396 visibleRect = ImageDisplay.this.visibleRect;
397 }
398
[2986]399 if (image == null)
[2566]400 return;
401
[13127]402 if (ZOOM_ON_CLICK.get()) {
403 // click notions are less coherent than wheel, refresh mousePointInImg on each click
404 lastMouseEvent = null;
[2566]405
[13127]406 if (mouseIsZoomSelecting(e) && !isAtMaxZoom(visibleRect)) {
407 // zoom in if clicked with the zoom button
408 mouseWheelMovedImpl(e.getX(), e.getY(), -1, true);
409 return;
410 }
411 if (mouseIsDragging(e)) {
412 // zoom out if clicked with the drag button
413 mouseWheelMovedImpl(e.getX(), e.getY(), 1, true);
414 return;
415 }
416 }
417
[2566]418 // Calculate the translation to set the clicked point the center of the view.
[11539]419 Point click = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
[2566]420 Point center = getCenterImgCoord(visibleRect);
421
422 visibleRect.x += click.x - center.x;
423 visibleRect.y += click.y - center.y;
424
[13127]425 visibleRect.checkRectPos();
[2566]426
[8510]427 synchronized (ImageDisplay.this) {
[2566]428 if (ImageDisplay.this.file == file) {
429 ImageDisplay.this.visibleRect = visibleRect;
430 }
431 }
432 ImageDisplay.this.repaint();
433 }
434
435 /** Initialize the dragging, either with button 1 (simple dragging) or button 3 (selection of
436 * a picture part) */
[6084]437 @Override
[2566]438 public void mousePressed(MouseEvent e) {
439 Image image;
[13127]440 VisRect visibleRect;
[2566]441
442 synchronized (ImageDisplay.this) {
443 image = ImageDisplay.this.image;
444 visibleRect = ImageDisplay.this.visibleRect;
445 }
446
[2986]447 if (image == null)
[2566]448 return;
449
[13127]450 selectedRect = null;
451
452 if (mouseIsDragging(e) || mouseIsZoomSelecting(e))
[11539]453 mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
[2566]454 }
455
[6084]456 @Override
[2566]457 public void mouseDragged(MouseEvent e) {
[13127]458 if (!mouseIsDragging(e) && !mouseIsZoomSelecting(e))
[2566]459 return;
460
461 File file;
462 Image image;
[13127]463 VisRect visibleRect;
[2566]464
465 synchronized (ImageDisplay.this) {
466 file = ImageDisplay.this.file;
467 image = ImageDisplay.this.image;
468 visibleRect = ImageDisplay.this.visibleRect;
469 }
470
[13127]471 if (image == null)
[2566]472 return;
473
[13127]474 if (mouseIsDragging(e)) {
[11539]475 Point p = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
[2566]476 visibleRect.x += mousePointInImg.x - p.x;
477 visibleRect.y += mousePointInImg.y - p.y;
[13127]478 visibleRect.checkRectPos();
[8510]479 synchronized (ImageDisplay.this) {
[2566]480 if (ImageDisplay.this.file == file) {
481 ImageDisplay.this.visibleRect = visibleRect;
482 }
483 }
484 ImageDisplay.this.repaint();
[13127]485 }
[2566]486
[13127]487 if (mouseIsZoomSelecting(e)) {
[11539]488 Point p = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
[13127]489 visibleRect.checkPointInside(p);
490 VisRect selectedRect = new VisRect(
[8345]491 p.x < mousePointInImg.x ? p.x : mousePointInImg.x,
492 p.y < mousePointInImg.y ? p.y : mousePointInImg.y,
493 p.x < mousePointInImg.x ? mousePointInImg.x - p.x : p.x - mousePointInImg.x,
[13127]494 p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y,
495 visibleRect);
496 selectedRect.checkRectSize();
497 selectedRect.checkRectPos();
498 ImageDisplay.this.selectedRect = selectedRect;
[2566]499 ImageDisplay.this.repaint();
500 }
501
502 }
503
[6084]504 @Override
[2566]505 public void mouseReleased(MouseEvent e) {
[13127]506 if (!mouseIsZoomSelecting(e) || selectedRect == null)
[2566]507 return;
508
509 File file;
510 Image image;
511
512 synchronized (ImageDisplay.this) {
513 file = ImageDisplay.this.file;
514 image = ImageDisplay.this.image;
515 }
516
517 if (image == null) {
518 return;
519 }
520
[13127]521 int oldWidth = selectedRect.width;
522 int oldHeight = selectedRect.height;
[2566]523
[13127]524 // Check that the zoom doesn't exceed MAX_ZOOM:1
525 if (selectedRect.width < getSize().width / MAX_ZOOM.get()) {
526 selectedRect.width = (int) (getSize().width / MAX_ZOOM.get());
527 }
528 if (selectedRect.height < getSize().height / MAX_ZOOM.get()) {
529 selectedRect.height = (int) (getSize().height / MAX_ZOOM.get());
530 }
[2566]531
[13127]532 // Set the same ratio for the visible rectangle and the display area
533 int hFact = selectedRect.height * getSize().width;
534 int wFact = selectedRect.width * getSize().height;
535 if (hFact > wFact) {
536 selectedRect.width = hFact / getSize().height;
537 } else {
538 selectedRect.height = wFact / getSize().width;
539 }
[2566]540
[13127]541 // Keep the center of the selection
542 if (selectedRect.width != oldWidth) {
543 selectedRect.x -= (selectedRect.width - oldWidth) / 2;
544 }
545 if (selectedRect.height != oldHeight) {
546 selectedRect.y -= (selectedRect.height - oldHeight) / 2;
547 }
[2566]548
[13127]549 selectedRect.checkRectSize();
550 selectedRect.checkRectPos();
[2566]551
[13127]552 synchronized (ImageDisplay.this) {
553 if (file == ImageDisplay.this.file) {
554 ImageDisplay.this.visibleRect.setBounds(selectedRect);
[2566]555 }
556 }
[13127]557 selectedRect = null;
558 ImageDisplay.this.repaint();
[2566]559 }
560
[6084]561 @Override
[2566]562 public void mouseEntered(MouseEvent e) {
[10173]563 // Do nothing
[2566]564 }
565
[6084]566 @Override
[2566]567 public void mouseExited(MouseEvent e) {
[10173]568 // Do nothing
[2566]569 }
570
[6084]571 @Override
[2566]572 public void mouseMoved(MouseEvent e) {
[10173]573 // Do nothing
[2566]574 }
575 }
576
[11539]577 /**
578 * Constructs a new {@code ImageDisplay}.
579 */
[2566]580 public ImageDisplay() {
581 ImgDisplayMouseListener mouseListener = new ImgDisplayMouseListener();
582 addMouseListener(mouseListener);
583 addMouseWheelListener(mouseListener);
584 addMouseMotionListener(mouseListener);
[13127]585 Config.getPref().addPreferenceChangeListener(this);
586 preferenceChanged(null);
[2566]587 }
588
[4241]589 public void setImage(File file, Integer orientation) {
[8510]590 synchronized (this) {
[2566]591 this.file = file;
592 image = null;
593 errorLoading = false;
594 }
595 repaint();
596 if (file != null) {
[8736]597 new Thread(new LoadImageRunnable(file, orientation), LoadImageRunnable.class.getName()).start();
[2566]598 }
599 }
600
[13038]601 /**
602 * Sets the On-Screen-Display text.
603 * @param text text to display on top of the image
604 */
[2566]605 public void setOsdText(String text) {
606 this.osdText = text;
[7912]607 repaint();
[2566]608 }
609
[2986]610 @Override
[2566]611 public void paintComponent(Graphics g) {
612 Image image;
613 File file;
[13127]614 VisRect visibleRect;
[2566]615 boolean errorLoading;
616
[8510]617 synchronized (this) {
[2566]618 image = this.image;
619 file = this.file;
620 visibleRect = this.visibleRect;
621 errorLoading = this.errorLoading;
622 }
623
[13038]624 if (g instanceof Graphics2D) {
625 ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
626 }
627
[11539]628 Dimension size = getSize();
[2566]629 if (file == null) {
630 g.setColor(Color.black);
631 String noImageStr = tr("No image");
632 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(noImageStr, g);
633 g.drawString(noImageStr,
[2986]634 (int) ((size.width - noImageSize.getWidth()) / 2),
635 (int) ((size.height - noImageSize.getHeight()) / 2));
[2566]636 } else if (image == null) {
637 g.setColor(Color.black);
638 String loadingStr;
[8444]639 if (!errorLoading) {
[2990]640 loadingStr = tr("Loading {0}", file.getName());
[2566]641 } else {
642 loadingStr = tr("Error on file {0}", file.getName());
643 }
644 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g);
645 g.drawString(loadingStr,
[2986]646 (int) ((size.width - noImageSize.getWidth()) / 2),
647 (int) ((size.height - noImageSize.getHeight()) / 2));
[2566]648 } else {
[13127]649 Rectangle r = new Rectangle(visibleRect);
[11539]650 Rectangle target = calculateDrawImageRectangle(visibleRect, size);
[13127]651 double scale = target.width / (double) r.width; // pixel ratio is 1:1
652
[13129]653 if (selectedRect == null && bilinLower < scale && scale < bilinUpper) {
[13127]654 BufferedImage bi = ImageProvider.toBufferedImage(image, r);
655 r.x = r.y = 0;
656
657 // See https://community.oracle.com/docs/DOC-983611 - The Perils of Image.getScaledInstance()
658 // Pre-scale image when downscaling by more than two times to avoid aliasing from default algorithm
659 image = ImageProvider.createScaledImage(bi, target.width, target.height,
660 RenderingHints.VALUE_INTERPOLATION_BILINEAR);
661 r.width = target.width;
662 r.height = target.height;
[13038]663 } else {
[13127]664 // if target and r cause drawImage to scale image region to a tmp buffer exceeding
665 // its bounds, it will silently fail; crop with r first in such cases
666 // (might be impl. dependent, exhibited by openjdk 1.8.0_151)
667 if (scale*(r.x+r.width) > Short.MAX_VALUE || scale*(r.y+r.height) > Short.MAX_VALUE) {
668 image = ImageProvider.toBufferedImage(image, r);
669 r.x = r.y = 0;
670 }
[13038]671 }
[13127]672
673 g.drawImage(image,
674 target.x, target.y, target.x + target.width, target.y + target.height,
675 r.x, r.y, r.x + r.width, r.y + r.height, null);
676
[2566]677 if (selectedRect != null) {
[11539]678 Point topLeft = img2compCoord(visibleRect, selectedRect.x, selectedRect.y, size);
[2566]679 Point bottomRight = img2compCoord(visibleRect,
[2986]680 selectedRect.x + selectedRect.width,
[11539]681 selectedRect.y + selectedRect.height, size);
[2566]682 g.setColor(new Color(128, 128, 128, 180));
683 g.fillRect(target.x, target.y, target.width, topLeft.y - target.y);
684 g.fillRect(target.x, target.y, topLeft.x - target.x, target.height);
685 g.fillRect(bottomRight.x, target.y, target.x + target.width - bottomRight.x, target.height);
686 g.fillRect(target.x, bottomRight.y, target.width, target.y + target.height - bottomRight.y);
687 g.setColor(Color.black);
688 g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y);
689 }
690 if (errorLoading) {
691 String loadingStr = tr("Error on file {0}", file.getName());
692 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g);
693 g.drawString(loadingStr,
[2986]694 (int) ((size.width - noImageSize.getWidth()) / 2),
695 (int) ((size.height - noImageSize.getHeight()) / 2));
[2566]696 }
697 if (osdText != null) {
698 FontMetrics metrics = g.getFontMetrics(g.getFont());
699 int ascent = metrics.getAscent();
700 Color bkground = new Color(255, 255, 255, 128);
701 int lastPos = 0;
[6083]702 int pos = osdText.indexOf('\n');
[2566]703 int x = 3;
704 int y = 3;
705 String line;
706 while (pos > 0) {
707 line = osdText.substring(lastPos, pos);
708 Rectangle2D lineSize = metrics.getStringBounds(line, g);
709 g.setColor(bkground);
710 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight());
711 g.setColor(Color.black);
712 g.drawString(line, x, y + ascent);
713 y += (int) lineSize.getHeight();
714 lastPos = pos + 1;
[6083]715 pos = osdText.indexOf('\n', lastPos);
[2566]716 }
717
718 line = osdText.substring(lastPos);
719 Rectangle2D lineSize = g.getFontMetrics(g.getFont()).getStringBounds(line, g);
720 g.setColor(bkground);
721 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight());
722 g.setColor(Color.black);
723 g.drawString(line, x, y + ascent);
724 }
725 }
726 }
727
[13127]728 static Point img2compCoord(VisRect visibleRect, int xImg, int yImg, Dimension compSize) {
[11539]729 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize);
[2566]730 return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) / visibleRect.width,
[2986]731 drawRect.y + ((yImg - visibleRect.y) * drawRect.height) / visibleRect.height);
[2566]732 }
733
[13127]734 static Point comp2imgCoord(VisRect visibleRect, int xComp, int yComp, Dimension compSize) {
[11539]735 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize);
[13127]736 Point p = new Point(
737 ((xComp - drawRect.x) * visibleRect.width),
738 ((yComp - drawRect.y) * visibleRect.height));
739 p.x += (((p.x % drawRect.width) << 1) >= drawRect.width) ? drawRect.width : 0;
740 p.y += (((p.y % drawRect.height) << 1) >= drawRect.height) ? drawRect.height : 0;
741 p.x = visibleRect.x + p.x / drawRect.width;
742 p.y = visibleRect.y + p.y / drawRect.height;
743 return p;
[2566]744 }
745
[11539]746 static Point getCenterImgCoord(Rectangle visibleRect) {
[2566]747 return new Point(visibleRect.x + visibleRect.width / 2,
[11539]748 visibleRect.y + visibleRect.height / 2);
[2566]749 }
750
[13127]751 static VisRect calculateDrawImageRectangle(VisRect visibleRect, Dimension compSize) {
[11539]752 return calculateDrawImageRectangle(visibleRect, new Rectangle(0, 0, compSize.width, compSize.height));
[2602]753 }
[2711]754
[2602]755 /**
756 * calculateDrawImageRectangle
757 *
758 * @param imgRect the part of the image that should be drawn (in image coordinates)
759 * @param compRect the part of the component where the image should be drawn (in component coordinates)
760 * @return the part of compRect with the same width/height ratio as the image
761 */
[13127]762 static VisRect calculateDrawImageRectangle(VisRect imgRect, Rectangle compRect) {
[11539]763 int x = 0;
764 int y = 0;
765 int w = compRect.width;
766 int h = compRect.height;
[2566]767
[2602]768 int wFact = w * imgRect.height;
769 int hFact = h * imgRect.width;
[2566]770 if (wFact != hFact) {
771 if (wFact > hFact) {
[2602]772 w = hFact / imgRect.height;
773 x = (compRect.width - w) / 2;
[2566]774 } else {
[2602]775 h = wFact / imgRect.width;
776 y = (compRect.height - h) / 2;
[2566]777 }
778 }
[13127]779
780 // overscan to prevent empty edges when zooming in to zoom scales > 2:1
781 if (w > imgRect.width && h > imgRect.height && !imgRect.isFullView1D()) {
782 if (wFact != hFact) {
783 if (wFact > hFact) {
784 w = compRect.width;
785 x = 0;
786 h = wFact / imgRect.width;
787 y = (compRect.height - h) / 2;
788 } else {
789 h = compRect.height;
790 y = 0;
791 w = hFact / imgRect.height;
792 x = (compRect.width - w) / 2;
793 }
794 }
795 }
796
797 return new VisRect(x + compRect.x, y + compRect.y, w, h, imgRect);
[2566]798 }
799
800 public void zoomBestFitOrOne() {
801 File file;
802 Image image;
[13127]803 VisRect visibleRect;
[2566]804
805 synchronized (this) {
[9075]806 file = this.file;
807 image = this.image;
808 visibleRect = this.visibleRect;
[2566]809 }
810
[2986]811 if (image == null)
[2566]812 return;
813
814 if (visibleRect.width != image.getWidth(null) || visibleRect.height != image.getHeight(null)) {
815 // The display is not at best fit. => Zoom to best fit
[13127]816 visibleRect.reset();
[2566]817 } else {
818 // The display is at best fit => zoom to 1:1
819 Point center = getCenterImgCoord(visibleRect);
[13127]820 visibleRect.setBounds(center.x - getWidth() / 2, center.y - getHeight() / 2,
[2986]821 getWidth(), getHeight());
[13127]822 visibleRect.checkRectSize();
823 visibleRect.checkRectPos();
[2566]824 }
825
[8510]826 synchronized (this) {
[2566]827 if (file == this.file) {
828 this.visibleRect = visibleRect;
829 }
830 }
831 repaint();
832 }
833}
Note: See TracBrowser for help on using the repository browser.