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

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

add unit test

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