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

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

see #15476, fix #15511 - fix image scaling regression and makes geoimage feature more configurable through prefs (adjustable max zoom, zoom-step, click zooming with mouse buttons (e.g. if a mouse wheel is not present). Patch by cmuelle8

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