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

Last change on this file since 12460 was 12460, checked in by bastiK, 7 years ago

see #14794 - javadoc

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