001 // License: GPL. See LICENSE file for details.
002 // Copyright 2007 by Christian Gallioz (aka khris78)
003
004 package org.openstreetmap.josm.gui.layer.geoimage;
005
006 import static org.openstreetmap.josm.tools.I18n.tr;
007
008 import java.awt.Color;
009 import java.awt.Dimension;
010 import java.awt.FontMetrics;
011 import java.awt.Graphics;
012 import java.awt.Graphics2D;
013 import java.awt.Image;
014 import java.awt.MediaTracker;
015 import java.awt.Point;
016 import java.awt.Rectangle;
017 import java.awt.Toolkit;
018 import java.awt.event.MouseEvent;
019 import java.awt.event.MouseListener;
020 import java.awt.event.MouseMotionListener;
021 import java.awt.event.MouseWheelEvent;
022 import java.awt.event.MouseWheelListener;
023 import java.awt.geom.AffineTransform;
024 import java.awt.geom.Rectangle2D;
025 import java.awt.image.BufferedImage;
026 import java.io.File;
027
028 import javax.swing.JComponent;
029
030 import org.openstreetmap.josm.Main;
031
032 public class ImageDisplay extends JComponent {
033
034 /** The file that is currently displayed */
035 private File file = null;
036
037 /** The image currently displayed */
038 private Image image = null;
039
040 /** The image currently displayed */
041 private boolean errorLoading = false;
042
043 /** The rectangle (in image coordinates) of the image that is visible. This rectangle is calculated
044 * each time the zoom is modified */
045 private Rectangle visibleRect = null;
046
047 /** When a selection is done, the rectangle of the selection (in image coordinates) */
048 private Rectangle selectedRect = null;
049
050 /** The tracker to load the images */
051 private MediaTracker tracker = new MediaTracker(this);
052
053 private String osdText = null;
054
055 private static int DRAG_BUTTON = Main.pref.getBoolean("geoimage.agpifo-style-drag-and-zoom", false) ? 1 : 3;
056 private static int ZOOM_BUTTON = DRAG_BUTTON == 1 ? 3 : 1;
057
058 /** The thread that reads the images. */
059 private class LoadImageRunnable implements Runnable {
060
061 private File file;
062 private int orientation;
063
064 public LoadImageRunnable(File file, Integer orientation) {
065 this.file = file;
066 this.orientation = orientation == null ? -1 : orientation;
067 }
068
069 public void run() {
070 Image img = Toolkit.getDefaultToolkit().createImage(file.getPath());
071 tracker.addImage(img, 1);
072
073 // Wait for the end of loading
074 while (! tracker.checkID(1, true)) {
075 if (this.file != ImageDisplay.this.file) {
076 // The file has changed
077 tracker.removeImage(img);
078 return;
079 }
080 try {
081 Thread.sleep(5);
082 } catch (InterruptedException e) {
083 }
084 }
085
086 boolean error = tracker.isErrorID(1);
087 if (img.getWidth(null) < 0 || img.getHeight(null) < 0) {
088 error = true;
089 }
090
091 synchronized(ImageDisplay.this) {
092 if (this.file != ImageDisplay.this.file) {
093 // The file has changed
094 tracker.removeImage(img);
095 return;
096 }
097
098 if (!error) {
099 ImageDisplay.this.image = img;
100 visibleRect = new Rectangle(0, 0, img.getWidth(null), img.getHeight(null));
101
102 final int w = (int) visibleRect.getWidth();
103 final int h = (int) visibleRect.getHeight();
104
105 outer: {
106 final int hh, ww, q;
107 final double ax, ay;
108 switch (orientation) {
109 case 8:
110 q = -1;
111 ax = w / 2;
112 ay = w / 2;
113 ww = h;
114 hh = w;
115 break;
116 case 3:
117 q = 2;
118 ax = w / 2;
119 ay = h / 2;
120 ww = w;
121 hh = h;
122 break;
123 case 6:
124 q = 1;
125 ax = h / 2;
126 ay = h / 2;
127 ww = h;
128 hh = w;
129 break;
130 default:
131 break outer;
132 }
133
134 final BufferedImage rot = new BufferedImage(ww, hh, BufferedImage.TYPE_INT_RGB);
135 final AffineTransform xform = AffineTransform.getQuadrantRotateInstance(q, ax, ay);
136 final Graphics2D g = rot.createGraphics();
137 g.drawImage(image, xform, null);
138 g.dispose();
139
140 visibleRect.setSize(ww, hh);
141 image.flush();
142 ImageDisplay.this.image = rot;
143 }
144 }
145
146 selectedRect = null;
147 errorLoading = error;
148 }
149 tracker.removeImage(img);
150 ImageDisplay.this.repaint();
151 }
152 }
153
154 private class ImgDisplayMouseListener implements MouseListener, MouseWheelListener, MouseMotionListener {
155
156 boolean mouseIsDragging = false;
157 long lastTimeForMousePoint = 0l;
158 Point mousePointInImg = null;
159
160 /** Zoom in and out, trying to preserve the point of the image that was under the mouse cursor
161 * at the same place */
162 public void mouseWheelMoved(MouseWheelEvent e) {
163 File file;
164 Image image;
165 Rectangle visibleRect;
166
167 synchronized (ImageDisplay.this) {
168 file = ImageDisplay.this.file;
169 image = ImageDisplay.this.image;
170 visibleRect = ImageDisplay.this.visibleRect;
171 }
172
173 mouseIsDragging = false;
174 selectedRect = null;
175
176 if (image == null)
177 return;
178
179 // Calculate the mouse cursor position in image coordinates, so that we can center the zoom
180 // on that mouse position.
181 // To avoid issues when the user tries to zoom in on the image borders, this point is not calculated
182 // again if there was less than 1.5seconds since the last event.
183 if (e.getWhen() - lastTimeForMousePoint > 1500 || mousePointInImg == null) {
184 lastTimeForMousePoint = e.getWhen();
185 mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY());
186 }
187
188 // Applicate the zoom to the visible rectangle in image coordinates
189 if (e.getWheelRotation() > 0) {
190 visibleRect.width = visibleRect.width * 3 / 2;
191 visibleRect.height = visibleRect.height * 3 / 2;
192 } else {
193 visibleRect.width = visibleRect.width * 2 / 3;
194 visibleRect.height = visibleRect.height * 2 / 3;
195 }
196
197 // Check that the zoom doesn't exceed 2:1
198 if (visibleRect.width < getSize().width / 2) {
199 visibleRect.width = getSize().width / 2;
200 }
201 if (visibleRect.height < getSize().height / 2) {
202 visibleRect.height = getSize().height / 2;
203 }
204
205 // Set the same ratio for the visible rectangle and the display area
206 int hFact = visibleRect.height * getSize().width;
207 int wFact = visibleRect.width * getSize().height;
208 if (hFact > wFact) {
209 visibleRect.width = hFact / getSize().height;
210 } else {
211 visibleRect.height = wFact / getSize().width;
212 }
213
214 // The size of the visible rectangle is limited by the image size.
215 checkVisibleRectSize(image, visibleRect);
216
217 // Set the position of the visible rectangle, so that the mouse cursor doesn't move on the image.
218 Rectangle drawRect = calculateDrawImageRectangle(visibleRect);
219 visibleRect.x = mousePointInImg.x + ((drawRect.x - e.getX()) * visibleRect.width) / drawRect.width;
220 visibleRect.y = mousePointInImg.y + ((drawRect.y - e.getY()) * visibleRect.height) / drawRect.height;
221
222 // The position is also limited by the image size
223 checkVisibleRectPos(image, visibleRect);
224
225 synchronized(ImageDisplay.this) {
226 if (ImageDisplay.this.file == file) {
227 ImageDisplay.this.visibleRect = visibleRect;
228 }
229 }
230 ImageDisplay.this.repaint();
231 }
232
233 /** Center the display on the point that has been clicked */
234 public void mouseClicked(MouseEvent e) {
235 // Move the center to the clicked point.
236 File file;
237 Image image;
238 Rectangle visibleRect;
239
240 synchronized (ImageDisplay.this) {
241 file = ImageDisplay.this.file;
242 image = ImageDisplay.this.image;
243 visibleRect = ImageDisplay.this.visibleRect;
244 }
245
246 if (image == null)
247 return;
248
249 if (e.getButton() != DRAG_BUTTON)
250 return;
251
252 // Calculate the translation to set the clicked point the center of the view.
253 Point click = comp2imgCoord(visibleRect, e.getX(), e.getY());
254 Point center = getCenterImgCoord(visibleRect);
255
256 visibleRect.x += click.x - center.x;
257 visibleRect.y += click.y - center.y;
258
259 checkVisibleRectPos(image, visibleRect);
260
261 synchronized(ImageDisplay.this) {
262 if (ImageDisplay.this.file == file) {
263 ImageDisplay.this.visibleRect = visibleRect;
264 }
265 }
266 ImageDisplay.this.repaint();
267 }
268
269 /** Initialize the dragging, either with button 1 (simple dragging) or button 3 (selection of
270 * a picture part) */
271 public void mousePressed(MouseEvent e) {
272 if (image == null) {
273 mouseIsDragging = false;
274 selectedRect = null;
275 return;
276 }
277
278 Image image;
279 Rectangle visibleRect;
280
281 synchronized (ImageDisplay.this) {
282 image = ImageDisplay.this.image;
283 visibleRect = ImageDisplay.this.visibleRect;
284 }
285
286 if (image == null)
287 return;
288
289 if (e.getButton() == DRAG_BUTTON) {
290 mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY());
291 mouseIsDragging = true;
292 selectedRect = null;
293 } else if (e.getButton() == ZOOM_BUTTON) {
294 mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY());
295 checkPointInVisibleRect(mousePointInImg, visibleRect);
296 mouseIsDragging = false;
297 selectedRect = new Rectangle(mousePointInImg.x, mousePointInImg.y, 0, 0);
298 ImageDisplay.this.repaint();
299 } else {
300 mouseIsDragging = false;
301 selectedRect = null;
302 }
303 }
304
305 public void mouseDragged(MouseEvent e) {
306 if (! mouseIsDragging && selectedRect == null)
307 return;
308
309 File file;
310 Image image;
311 Rectangle visibleRect;
312
313 synchronized (ImageDisplay.this) {
314 file = ImageDisplay.this.file;
315 image = ImageDisplay.this.image;
316 visibleRect = ImageDisplay.this.visibleRect;
317 }
318
319 if (image == null) {
320 mouseIsDragging = false;
321 selectedRect = null;
322 return;
323 }
324
325 if (mouseIsDragging) {
326 Point p = comp2imgCoord(visibleRect, e.getX(), e.getY());
327 visibleRect.x += mousePointInImg.x - p.x;
328 visibleRect.y += mousePointInImg.y - p.y;
329 checkVisibleRectPos(image, visibleRect);
330 synchronized(ImageDisplay.this) {
331 if (ImageDisplay.this.file == file) {
332 ImageDisplay.this.visibleRect = visibleRect;
333 }
334 }
335 ImageDisplay.this.repaint();
336
337 } else if (selectedRect != null) {
338 Point p = comp2imgCoord(visibleRect, e.getX(), e.getY());
339 checkPointInVisibleRect(p, visibleRect);
340 Rectangle rect = new Rectangle(
341 (p.x < mousePointInImg.x ? p.x : mousePointInImg.x),
342 (p.y < mousePointInImg.y ? p.y : mousePointInImg.y),
343 (p.x < mousePointInImg.x ? mousePointInImg.x - p.x : p.x - mousePointInImg.x),
344 (p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y));
345 checkVisibleRectSize(image, rect);
346 checkVisibleRectPos(image, rect);
347 ImageDisplay.this.selectedRect = rect;
348 ImageDisplay.this.repaint();
349 }
350
351 }
352
353 public void mouseReleased(MouseEvent e) {
354 if (! mouseIsDragging && selectedRect == null)
355 return;
356
357 File file;
358 Image image;
359
360 synchronized (ImageDisplay.this) {
361 file = ImageDisplay.this.file;
362 image = ImageDisplay.this.image;
363 }
364
365 if (image == null) {
366 mouseIsDragging = false;
367 selectedRect = null;
368 return;
369 }
370
371 if (mouseIsDragging) {
372 mouseIsDragging = false;
373
374 } else if (selectedRect != null) {
375 int oldWidth = selectedRect.width;
376 int oldHeight = selectedRect.height;
377
378 // Check that the zoom doesn't exceed 2:1
379 if (selectedRect.width < getSize().width / 2) {
380 selectedRect.width = getSize().width / 2;
381 }
382 if (selectedRect.height < getSize().height / 2) {
383 selectedRect.height = getSize().height / 2;
384 }
385
386 // Set the same ratio for the visible rectangle and the display area
387 int hFact = selectedRect.height * getSize().width;
388 int wFact = selectedRect.width * getSize().height;
389 if (hFact > wFact) {
390 selectedRect.width = hFact / getSize().height;
391 } else {
392 selectedRect.height = wFact / getSize().width;
393 }
394
395 // Keep the center of the selection
396 if (selectedRect.width != oldWidth) {
397 selectedRect.x -= (selectedRect.width - oldWidth) / 2;
398 }
399 if (selectedRect.height != oldHeight) {
400 selectedRect.y -= (selectedRect.height - oldHeight) / 2;
401 }
402
403 checkVisibleRectSize(image, selectedRect);
404 checkVisibleRectPos(image, selectedRect);
405
406 synchronized (ImageDisplay.this) {
407 if (file == ImageDisplay.this.file) {
408 ImageDisplay.this.visibleRect = selectedRect;
409 }
410 }
411 selectedRect = null;
412 ImageDisplay.this.repaint();
413 }
414 }
415
416 public void mouseEntered(MouseEvent e) {
417 }
418
419 public void mouseExited(MouseEvent e) {
420 }
421
422 public void mouseMoved(MouseEvent e) {
423 }
424
425 private void checkPointInVisibleRect(Point p, Rectangle visibleRect) {
426 if (p.x < visibleRect.x) {
427 p.x = visibleRect.x;
428 }
429 if (p.x > visibleRect.x + visibleRect.width) {
430 p.x = visibleRect.x + visibleRect.width;
431 }
432 if (p.y < visibleRect.y) {
433 p.y = visibleRect.y;
434 }
435 if (p.y > visibleRect.y + visibleRect.height) {
436 p.y = visibleRect.y + visibleRect.height;
437 }
438 }
439 }
440
441 public ImageDisplay() {
442 ImgDisplayMouseListener mouseListener = new ImgDisplayMouseListener();
443 addMouseListener(mouseListener);
444 addMouseWheelListener(mouseListener);
445 addMouseMotionListener(mouseListener);
446 }
447
448 public void setImage(File file, Integer orientation) {
449 synchronized(this) {
450 this.file = file;
451 image = null;
452 selectedRect = null;
453 errorLoading = false;
454 }
455 repaint();
456 if (file != null) {
457 new Thread(new LoadImageRunnable(file, orientation)).start();
458 }
459 }
460
461 public void setOsdText(String text) {
462 this.osdText = text;
463 }
464
465 @Override
466 public void paintComponent(Graphics g) {
467 Image image;
468 File file;
469 Rectangle visibleRect;
470 boolean errorLoading;
471
472 synchronized(this) {
473 image = this.image;
474 file = this.file;
475 visibleRect = this.visibleRect;
476 errorLoading = this.errorLoading;
477 }
478
479 if (file == null) {
480 g.setColor(Color.black);
481 String noImageStr = tr("No image");
482 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(noImageStr, g);
483 Dimension size = getSize();
484 g.drawString(noImageStr,
485 (int) ((size.width - noImageSize.getWidth()) / 2),
486 (int) ((size.height - noImageSize.getHeight()) / 2));
487 } else if (image == null) {
488 g.setColor(Color.black);
489 String loadingStr;
490 if (! errorLoading) {
491 loadingStr = tr("Loading {0}", file.getName());
492 } else {
493 loadingStr = tr("Error on file {0}", file.getName());
494 }
495 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g);
496 Dimension size = getSize();
497 g.drawString(loadingStr,
498 (int) ((size.width - noImageSize.getWidth()) / 2),
499 (int) ((size.height - noImageSize.getHeight()) / 2));
500 } else {
501 Rectangle target = calculateDrawImageRectangle(visibleRect);
502 g.drawImage(image,
503 target.x, target.y, target.x + target.width, target.y + target.height,
504 visibleRect.x, visibleRect.y, visibleRect.x + visibleRect.width, visibleRect.y + visibleRect.height,
505 null);
506 if (selectedRect != null) {
507 Point topLeft = img2compCoord(visibleRect, selectedRect.x, selectedRect.y);
508 Point bottomRight = img2compCoord(visibleRect,
509 selectedRect.x + selectedRect.width,
510 selectedRect.y + selectedRect.height);
511 g.setColor(new Color(128, 128, 128, 180));
512 g.fillRect(target.x, target.y, target.width, topLeft.y - target.y);
513 g.fillRect(target.x, target.y, topLeft.x - target.x, target.height);
514 g.fillRect(bottomRight.x, target.y, target.x + target.width - bottomRight.x, target.height);
515 g.fillRect(target.x, bottomRight.y, target.width, target.y + target.height - bottomRight.y);
516 g.setColor(Color.black);
517 g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y);
518 }
519 if (errorLoading) {
520 String loadingStr = tr("Error on file {0}", file.getName());
521 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g);
522 Dimension size = getSize();
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 private final Point img2compCoord(Rectangle visibleRect, int xImg, int yImg) {
559 Rectangle drawRect = calculateDrawImageRectangle(visibleRect);
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 private final Point comp2imgCoord(Rectangle visibleRect, int xComp, int yComp) {
565 Rectangle drawRect = calculateDrawImageRectangle(visibleRect);
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 private final Point getCenterImgCoord(Rectangle visibleRect) {
571 return new Point(visibleRect.x + visibleRect.width / 2,
572 visibleRect.y + visibleRect.height / 2);
573 }
574
575 private Rectangle calculateDrawImageRectangle(Rectangle visibleRect) {
576 return calculateDrawImageRectangle(visibleRect, new Rectangle(0, 0, getSize().width, getSize().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, y, w, h;
588 x = 0;
589 y = 0;
590 w = compRect.width;
591 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 = ImageDisplay.this.file;
614 image = ImageDisplay.this.image;
615 visibleRect = ImageDisplay.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 private final 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 private 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 }