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

Last change on this file since 12846 was 12846, checked in by bastiK, 9 years ago

see #15229 - use Config.getPref() wherever possible

  • Property svn:eol-style set to native
File size: 25.0 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.Toolkit;
16import java.awt.event.MouseEvent;
17import java.awt.event.MouseListener;
18import java.awt.event.MouseMotionListener;
19import java.awt.event.MouseWheelEvent;
20import java.awt.event.MouseWheelListener;
21import java.awt.geom.AffineTransform;
22import java.awt.geom.Rectangle2D;
23import java.awt.image.BufferedImage;
24import java.io.File;
25
26import javax.swing.JComponent;
27
28import org.openstreetmap.josm.spi.preferences.Config;
29import org.openstreetmap.josm.tools.ExifReader;
30import org.openstreetmap.josm.tools.Logging;
31
32/**
33 * GUI component to display an image (photograph).
34 *
35 * Offers basic mouse interaction (zoom, drag) and on-screen text.
36 */
37public class ImageDisplay extends JComponent {
38
39 /** The file that is currently displayed */
40 private File file;
41
42 /** The image currently displayed */
43 private transient Image image;
44
45 /** The image currently displayed */
46 private boolean errorLoading;
47
48 /** The rectangle (in image coordinates) of the image that is visible. This rectangle is calculated
49 * each time the zoom is modified */
50 private Rectangle visibleRect;
51
52 /** When a selection is done, the rectangle of the selection (in image coordinates) */
53 private Rectangle selectedRect;
54
55 /** The tracker to load the images */
56 private final MediaTracker tracker = new MediaTracker(this);
57
58 private String osdText;
59
60 private static final int DRAG_BUTTON = Config.getPref().getBoolean("geoimage.agpifo-style-drag-and-zoom", false) ? 1 : 3;
61 private static final int ZOOM_BUTTON = DRAG_BUTTON == 1 ? 3 : 1;
62
63 /** The thread that reads the images. */
64 private class LoadImageRunnable implements Runnable {
65
66 private final File file;
67 private final int orientation;
68
69 LoadImageRunnable(File file, Integer orientation) {
70 this.file = file;
71 this.orientation = orientation == null ? -1 : orientation;
72 }
73
74 @Override
75 public void run() {
76 Image img = Toolkit.getDefaultToolkit().createImage(file.getPath());
77 tracker.addImage(img, 1);
78
79 // Wait for the end of loading
80 while (!tracker.checkID(1, true)) {
81 if (this.file != ImageDisplay.this.file) {
82 // The file has changed
83 tracker.removeImage(img);
84 return;
85 }
86 try {
87 Thread.sleep(5);
88 } catch (InterruptedException e) {
89 Logging.warn("InterruptedException in "+getClass().getSimpleName()+" while loading image "+file.getPath());
90 Thread.currentThread().interrupt();
91 }
92 }
93
94 boolean error = tracker.isErrorID(1);
95 if (img.getWidth(null) < 0 || img.getHeight(null) < 0) {
96 error = true;
97 }
98
99 synchronized (ImageDisplay.this) {
100 if (this.file != ImageDisplay.this.file) {
101 // The file has changed
102 tracker.removeImage(img);
103 return;
104 }
105
106 if (!error) {
107 ImageDisplay.this.image = img;
108 visibleRect = new Rectangle(0, 0, img.getWidth(null), img.getHeight(null));
109
110 final int w = (int) visibleRect.getWidth();
111 final int h = (int) visibleRect.getHeight();
112
113 if (ExifReader.orientationNeedsCorrection(orientation)) {
114 final int hh, ww;
115 if (ExifReader.orientationSwitchesDimensions(orientation)) {
116 ww = h;
117 hh = w;
118 } else {
119 ww = w;
120 hh = h;
121 }
122 final BufferedImage rot = new BufferedImage(ww, hh, BufferedImage.TYPE_INT_RGB);
123 final AffineTransform xform = ExifReader.getRestoreOrientationTransform(orientation, w, h);
124 final Graphics2D g = rot.createGraphics();
125 g.drawImage(image, xform, null);
126 g.dispose();
127
128 visibleRect.setSize(ww, hh);
129 image.flush();
130 ImageDisplay.this.image = rot;
131 }
132 }
133
134 selectedRect = null;
135 errorLoading = error;
136 }
137 tracker.removeImage(img);
138 ImageDisplay.this.repaint();
139 }
140 }
141
142 private class ImgDisplayMouseListener implements MouseListener, MouseWheelListener, MouseMotionListener {
143
144 private boolean mouseIsDragging;
145 private long lastTimeForMousePoint;
146 private Point mousePointInImg;
147
148 /** Zoom in and out, trying to preserve the point of the image that was under the mouse cursor
149 * at the same place */
150 @Override
151 public void mouseWheelMoved(MouseWheelEvent e) {
152 File file;
153 Image image;
154 Rectangle visibleRect;
155
156 synchronized (ImageDisplay.this) {
157 file = ImageDisplay.this.file;
158 image = ImageDisplay.this.image;
159 visibleRect = ImageDisplay.this.visibleRect;
160 }
161
162 mouseIsDragging = false;
163 selectedRect = null;
164
165 if (image == null)
166 return;
167
168 // Calculate the mouse cursor position in image coordinates, so that we can center the zoom
169 // on that mouse position.
170 // To avoid issues when the user tries to zoom in on the image borders, this point is not calculated
171 // again if there was less than 1.5seconds since the last event.
172 if (e.getWhen() - lastTimeForMousePoint > 1500 || mousePointInImg == null) {
173 lastTimeForMousePoint = e.getWhen();
174 mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
175 }
176
177 // Applicate the zoom to the visible rectangle in image coordinates
178 if (e.getWheelRotation() > 0) {
179 visibleRect.width = visibleRect.width * 3 / 2;
180 visibleRect.height = visibleRect.height * 3 / 2;
181 } else {
182 visibleRect.width = visibleRect.width * 2 / 3;
183 visibleRect.height = visibleRect.height * 2 / 3;
184 }
185
186 // Check that the zoom doesn't exceed 2:1
187 if (visibleRect.width < getSize().width / 2) {
188 visibleRect.width = getSize().width / 2;
189 }
190 if (visibleRect.height < getSize().height / 2) {
191 visibleRect.height = getSize().height / 2;
192 }
193
194 // Set the same ratio for the visible rectangle and the display area
195 int hFact = visibleRect.height * getSize().width;
196 int wFact = visibleRect.width * getSize().height;
197 if (hFact > wFact) {
198 visibleRect.width = hFact / getSize().height;
199 } else {
200 visibleRect.height = wFact / getSize().width;
201 }
202
203 // The size of the visible rectangle is limited by the image size.
204 checkVisibleRectSize(image, visibleRect);
205
206 // Set the position of the visible rectangle, so that the mouse cursor doesn't move on the image.
207 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, getSize());
208 visibleRect.x = mousePointInImg.x + ((drawRect.x - e.getX()) * visibleRect.width) / drawRect.width;
209 visibleRect.y = mousePointInImg.y + ((drawRect.y - e.getY()) * visibleRect.height) / drawRect.height;
210
211 // The position is also limited by the image size
212 checkVisibleRectPos(image, visibleRect);
213
214 synchronized (ImageDisplay.this) {
215 if (ImageDisplay.this.file == file) {
216 ImageDisplay.this.visibleRect = visibleRect;
217 }
218 }
219 ImageDisplay.this.repaint();
220 }
221
222 /** Center the display on the point that has been clicked */
223 @Override
224 public void mouseClicked(MouseEvent e) {
225 // Move the center to the clicked point.
226 File file;
227 Image image;
228 Rectangle visibleRect;
229
230 synchronized (ImageDisplay.this) {
231 file = ImageDisplay.this.file;
232 image = ImageDisplay.this.image;
233 visibleRect = ImageDisplay.this.visibleRect;
234 }
235
236 if (image == null)
237 return;
238
239 if (e.getButton() != DRAG_BUTTON)
240 return;
241
242 // Calculate the translation to set the clicked point the center of the view.
243 Point click = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
244 Point center = getCenterImgCoord(visibleRect);
245
246 visibleRect.x += click.x - center.x;
247 visibleRect.y += click.y - center.y;
248
249 checkVisibleRectPos(image, visibleRect);
250
251 synchronized (ImageDisplay.this) {
252 if (ImageDisplay.this.file == file) {
253 ImageDisplay.this.visibleRect = visibleRect;
254 }
255 }
256 ImageDisplay.this.repaint();
257 }
258
259 /** Initialize the dragging, either with button 1 (simple dragging) or button 3 (selection of
260 * a picture part) */
261 @Override
262 public void mousePressed(MouseEvent e) {
263 if (image == null) {
264 mouseIsDragging = false;
265 selectedRect = null;
266 return;
267 }
268
269 Image image;
270 Rectangle visibleRect;
271
272 synchronized (ImageDisplay.this) {
273 image = ImageDisplay.this.image;
274 visibleRect = ImageDisplay.this.visibleRect;
275 }
276
277 if (image == null)
278 return;
279
280 if (e.getButton() == DRAG_BUTTON) {
281 mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
282 mouseIsDragging = true;
283 selectedRect = null;
284 } else if (e.getButton() == ZOOM_BUTTON) {
285 mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
286 checkPointInVisibleRect(mousePointInImg, visibleRect);
287 mouseIsDragging = false;
288 selectedRect = new Rectangle(mousePointInImg.x, mousePointInImg.y, 0, 0);
289 ImageDisplay.this.repaint();
290 } else {
291 mouseIsDragging = false;
292 selectedRect = null;
293 }
294 }
295
296 @Override
297 public void mouseDragged(MouseEvent e) {
298 if (!mouseIsDragging && selectedRect == null)
299 return;
300
301 File file;
302 Image image;
303 Rectangle visibleRect;
304
305 synchronized (ImageDisplay.this) {
306 file = ImageDisplay.this.file;
307 image = ImageDisplay.this.image;
308 visibleRect = ImageDisplay.this.visibleRect;
309 }
310
311 if (image == null) {
312 mouseIsDragging = false;
313 selectedRect = null;
314 return;
315 }
316
317 if (mouseIsDragging) {
318 Point p = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
319 visibleRect.x += mousePointInImg.x - p.x;
320 visibleRect.y += mousePointInImg.y - p.y;
321 checkVisibleRectPos(image, visibleRect);
322 synchronized (ImageDisplay.this) {
323 if (ImageDisplay.this.file == file) {
324 ImageDisplay.this.visibleRect = visibleRect;
325 }
326 }
327 ImageDisplay.this.repaint();
328
329 } else if (selectedRect != null) {
330 Point p = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
331 checkPointInVisibleRect(p, visibleRect);
332 Rectangle rect = new Rectangle(
333 p.x < mousePointInImg.x ? p.x : mousePointInImg.x,
334 p.y < mousePointInImg.y ? p.y : mousePointInImg.y,
335 p.x < mousePointInImg.x ? mousePointInImg.x - p.x : p.x - mousePointInImg.x,
336 p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y);
337 checkVisibleRectSize(image, rect);
338 checkVisibleRectPos(image, rect);
339 ImageDisplay.this.selectedRect = rect;
340 ImageDisplay.this.repaint();
341 }
342
343 }
344
345 @Override
346 public void mouseReleased(MouseEvent e) {
347 if (!mouseIsDragging && selectedRect == null)
348 return;
349
350 File file;
351 Image image;
352
353 synchronized (ImageDisplay.this) {
354 file = ImageDisplay.this.file;
355 image = ImageDisplay.this.image;
356 }
357
358 if (image == null) {
359 mouseIsDragging = false;
360 selectedRect = null;
361 return;
362 }
363
364 if (mouseIsDragging) {
365 mouseIsDragging = false;
366
367 } else if (selectedRect != null) {
368 int oldWidth = selectedRect.width;
369 int oldHeight = selectedRect.height;
370
371 // Check that the zoom doesn't exceed 2:1
372 if (selectedRect.width < getSize().width / 2) {
373 selectedRect.width = getSize().width / 2;
374 }
375 if (selectedRect.height < getSize().height / 2) {
376 selectedRect.height = getSize().height / 2;
377 }
378
379 // Set the same ratio for the visible rectangle and the display area
380 int hFact = selectedRect.height * getSize().width;
381 int wFact = selectedRect.width * getSize().height;
382 if (hFact > wFact) {
383 selectedRect.width = hFact / getSize().height;
384 } else {
385 selectedRect.height = wFact / getSize().width;
386 }
387
388 // Keep the center of the selection
389 if (selectedRect.width != oldWidth) {
390 selectedRect.x -= (selectedRect.width - oldWidth) / 2;
391 }
392 if (selectedRect.height != oldHeight) {
393 selectedRect.y -= (selectedRect.height - oldHeight) / 2;
394 }
395
396 checkVisibleRectSize(image, selectedRect);
397 checkVisibleRectPos(image, selectedRect);
398
399 synchronized (ImageDisplay.this) {
400 if (file == ImageDisplay.this.file) {
401 ImageDisplay.this.visibleRect = selectedRect;
402 }
403 }
404 selectedRect = null;
405 ImageDisplay.this.repaint();
406 }
407 }
408
409 @Override
410 public void mouseEntered(MouseEvent e) {
411 // Do nothing
412 }
413
414 @Override
415 public void mouseExited(MouseEvent e) {
416 // Do nothing
417 }
418
419 @Override
420 public void mouseMoved(MouseEvent e) {
421 // Do nothing
422 }
423
424 private void checkPointInVisibleRect(Point p, Rectangle visibleRect) {
425 if (p.x < visibleRect.x) {
426 p.x = visibleRect.x;
427 }
428 if (p.x > visibleRect.x + visibleRect.width) {
429 p.x = visibleRect.x + visibleRect.width;
430 }
431 if (p.y < visibleRect.y) {
432 p.y = visibleRect.y;
433 }
434 if (p.y > visibleRect.y + visibleRect.height) {
435 p.y = visibleRect.y + visibleRect.height;
436 }
437 }
438 }
439
440 /**
441 * Constructs a new {@code ImageDisplay}.
442 */
443 public ImageDisplay() {
444 ImgDisplayMouseListener mouseListener = new ImgDisplayMouseListener();
445 addMouseListener(mouseListener);
446 addMouseWheelListener(mouseListener);
447 addMouseMotionListener(mouseListener);
448 }
449
450 public void setImage(File file, Integer orientation) {
451 synchronized (this) {
452 this.file = file;
453 image = null;
454 selectedRect = null;
455 errorLoading = false;
456 }
457 repaint();
458 if (file != null) {
459 new Thread(new LoadImageRunnable(file, orientation), LoadImageRunnable.class.getName()).start();
460 }
461 }
462
463 public void setOsdText(String text) {
464 this.osdText = text;
465 repaint();
466 }
467
468 @Override
469 public void paintComponent(Graphics g) {
470 Image image;
471 File file;
472 Rectangle visibleRect;
473 boolean errorLoading;
474
475 synchronized (this) {
476 image = this.image;
477 file = this.file;
478 visibleRect = this.visibleRect;
479 errorLoading = this.errorLoading;
480 }
481
482 Dimension size = getSize();
483 if (file == null) {
484 g.setColor(Color.black);
485 String noImageStr = tr("No image");
486 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(noImageStr, g);
487 g.drawString(noImageStr,
488 (int) ((size.width - noImageSize.getWidth()) / 2),
489 (int) ((size.height - noImageSize.getHeight()) / 2));
490 } else if (image == null) {
491 g.setColor(Color.black);
492 String loadingStr;
493 if (!errorLoading) {
494 loadingStr = tr("Loading {0}", file.getName());
495 } else {
496 loadingStr = tr("Error on file {0}", file.getName());
497 }
498 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g);
499 g.drawString(loadingStr,
500 (int) ((size.width - noImageSize.getWidth()) / 2),
501 (int) ((size.height - noImageSize.getHeight()) / 2));
502 } else {
503 Rectangle target = calculateDrawImageRectangle(visibleRect, size);
504 g.drawImage(image,
505 target.x, target.y, target.x + target.width, target.y + target.height,
506 visibleRect.x, visibleRect.y, visibleRect.x + visibleRect.width, visibleRect.y + visibleRect.height,
507 null);
508 if (selectedRect != null) {
509 Point topLeft = img2compCoord(visibleRect, selectedRect.x, selectedRect.y, size);
510 Point bottomRight = img2compCoord(visibleRect,
511 selectedRect.x + selectedRect.width,
512 selectedRect.y + selectedRect.height, size);
513 g.setColor(new Color(128, 128, 128, 180));
514 g.fillRect(target.x, target.y, target.width, topLeft.y - target.y);
515 g.fillRect(target.x, target.y, topLeft.x - target.x, target.height);
516 g.fillRect(bottomRight.x, target.y, target.x + target.width - bottomRight.x, target.height);
517 g.fillRect(target.x, bottomRight.y, target.width, target.y + target.height - bottomRight.y);
518 g.setColor(Color.black);
519 g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y);
520 }
521 if (errorLoading) {
522 String loadingStr = tr("Error on file {0}", file.getName());
523 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g);
524 g.drawString(loadingStr,
525 (int) ((size.width - noImageSize.getWidth()) / 2),
526 (int) ((size.height - noImageSize.getHeight()) / 2));
527 }
528 if (osdText != null) {
529 FontMetrics metrics = g.getFontMetrics(g.getFont());
530 int ascent = metrics.getAscent();
531 Color bkground = new Color(255, 255, 255, 128);
532 int lastPos = 0;
533 int pos = osdText.indexOf('\n');
534 int x = 3;
535 int y = 3;
536 String line;
537 while (pos > 0) {
538 line = osdText.substring(lastPos, pos);
539 Rectangle2D lineSize = metrics.getStringBounds(line, g);
540 g.setColor(bkground);
541 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight());
542 g.setColor(Color.black);
543 g.drawString(line, x, y + ascent);
544 y += (int) lineSize.getHeight();
545 lastPos = pos + 1;
546 pos = osdText.indexOf('\n', lastPos);
547 }
548
549 line = osdText.substring(lastPos);
550 Rectangle2D lineSize = g.getFontMetrics(g.getFont()).getStringBounds(line, g);
551 g.setColor(bkground);
552 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight());
553 g.setColor(Color.black);
554 g.drawString(line, x, y + ascent);
555 }
556 }
557 }
558
559 static Point img2compCoord(Rectangle visibleRect, int xImg, int yImg, Dimension compSize) {
560 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize);
561 return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) / visibleRect.width,
562 drawRect.y + ((yImg - visibleRect.y) * drawRect.height) / visibleRect.height);
563 }
564
565 static Point comp2imgCoord(Rectangle visibleRect, int xComp, int yComp, Dimension compSize) {
566 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize);
567 return new Point(visibleRect.x + ((xComp - drawRect.x) * visibleRect.width) / drawRect.width,
568 visibleRect.y + ((yComp - drawRect.y) * visibleRect.height) / drawRect.height);
569 }
570
571 static Point getCenterImgCoord(Rectangle visibleRect) {
572 return new Point(visibleRect.x + visibleRect.width / 2,
573 visibleRect.y + visibleRect.height / 2);
574 }
575
576 static Rectangle calculateDrawImageRectangle(Rectangle visibleRect, Dimension compSize) {
577 return calculateDrawImageRectangle(visibleRect, new Rectangle(0, 0, compSize.width, compSize.height));
578 }
579
580 /**
581 * calculateDrawImageRectangle
582 *
583 * @param imgRect the part of the image that should be drawn (in image coordinates)
584 * @param compRect the part of the component where the image should be drawn (in component coordinates)
585 * @return the part of compRect with the same width/height ratio as the image
586 */
587 static Rectangle calculateDrawImageRectangle(Rectangle imgRect, Rectangle compRect) {
588 int x = 0;
589 int y = 0;
590 int w = compRect.width;
591 int h = compRect.height;
592
593 int wFact = w * imgRect.height;
594 int hFact = h * imgRect.width;
595 if (wFact != hFact) {
596 if (wFact > hFact) {
597 w = hFact / imgRect.height;
598 x = (compRect.width - w) / 2;
599 } else {
600 h = wFact / imgRect.width;
601 y = (compRect.height - h) / 2;
602 }
603 }
604 return new Rectangle(x + compRect.x, y + compRect.y, w, h);
605 }
606
607 public void zoomBestFitOrOne() {
608 File file;
609 Image image;
610 Rectangle visibleRect;
611
612 synchronized (this) {
613 file = this.file;
614 image = this.image;
615 visibleRect = this.visibleRect;
616 }
617
618 if (image == null)
619 return;
620
621 if (visibleRect.width != image.getWidth(null) || visibleRect.height != image.getHeight(null)) {
622 // The display is not at best fit. => Zoom to best fit
623 visibleRect = new Rectangle(0, 0, image.getWidth(null), image.getHeight(null));
624
625 } else {
626 // The display is at best fit => zoom to 1:1
627 Point center = getCenterImgCoord(visibleRect);
628 visibleRect = new Rectangle(center.x - getWidth() / 2, center.y - getHeight() / 2,
629 getWidth(), getHeight());
630 checkVisibleRectPos(image, visibleRect);
631 }
632
633 synchronized (this) {
634 if (file == this.file) {
635 this.visibleRect = visibleRect;
636 }
637 }
638 repaint();
639 }
640
641 static void checkVisibleRectPos(Image image, Rectangle visibleRect) {
642 if (visibleRect.x < 0) {
643 visibleRect.x = 0;
644 }
645 if (visibleRect.y < 0) {
646 visibleRect.y = 0;
647 }
648 if (visibleRect.x + visibleRect.width > image.getWidth(null)) {
649 visibleRect.x = image.getWidth(null) - visibleRect.width;
650 }
651 if (visibleRect.y + visibleRect.height > image.getHeight(null)) {
652 visibleRect.y = image.getHeight(null) - visibleRect.height;
653 }
654 }
655
656 static void checkVisibleRectSize(Image image, Rectangle visibleRect) {
657 if (visibleRect.width > image.getWidth(null)) {
658 visibleRect.width = image.getWidth(null);
659 }
660 if (visibleRect.height > image.getHeight(null)) {
661 visibleRect.height = image.getHeight(null);
662 }
663 }
664}
Note: See TracBrowser for help on using the repository browser.