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

Last change on this file since 4241 was 4241, checked in by bastiK, 13 years ago

applied #5605 - Geotagged image viewer should rotate images according to EXIF orientation tag (patch by m.zdila, some modifications)

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