001 // License: GPL. See LICENSE file for details. 002 // Copyright 2007 by Christian Gallioz (aka khris78) 003 // Parts of code from Geotagged plugin (by Rob Neild) 004 // and the core JOSM source code (by Immanuel Scholz and others) 005 package org.openstreetmap.josm.gui.layer.geoimage; 006 007 import static org.openstreetmap.josm.tools.I18n.tr; 008 import static org.openstreetmap.josm.tools.I18n.trn; 009 010 import java.awt.AlphaComposite; 011 import java.awt.Color; 012 import java.awt.Composite; 013 import java.awt.Dimension; 014 import java.awt.Graphics2D; 015 import java.awt.Image; 016 import java.awt.Point; 017 import java.awt.Rectangle; 018 import java.awt.event.MouseAdapter; 019 import java.awt.event.MouseEvent; 020 import java.awt.image.BufferedImage; 021 import java.beans.PropertyChangeEvent; 022 import java.beans.PropertyChangeListener; 023 import java.io.File; 024 import java.io.IOException; 025 import java.text.ParseException; 026 import java.util.ArrayList; 027 import java.util.Arrays; 028 import java.util.Collection; 029 import java.util.Collections; 030 import java.util.HashSet; 031 import java.util.LinkedHashSet; 032 import java.util.LinkedList; 033 import java.util.List; 034 035 import javax.swing.Action; 036 import javax.swing.Icon; 037 import javax.swing.JLabel; 038 import javax.swing.JOptionPane; 039 import javax.swing.SwingConstants; 040 041 import org.openstreetmap.josm.Main; 042 import org.openstreetmap.josm.actions.RenameLayerAction; 043 import org.openstreetmap.josm.actions.mapmode.MapMode; 044 import org.openstreetmap.josm.actions.mapmode.SelectAction; 045 import org.openstreetmap.josm.data.Bounds; 046 import org.openstreetmap.josm.data.coor.LatLon; 047 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 048 import org.openstreetmap.josm.gui.ExtendedDialog; 049 import org.openstreetmap.josm.gui.MapFrame; 050 import org.openstreetmap.josm.gui.MapFrame.MapModeChangeListener; 051 import org.openstreetmap.josm.gui.MapView; 052 import org.openstreetmap.josm.gui.MapView.LayerChangeListener; 053 import org.openstreetmap.josm.gui.NavigatableComponent; 054 import org.openstreetmap.josm.gui.PleaseWaitRunnable; 055 import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 056 import org.openstreetmap.josm.gui.dialogs.LayerListPopup; 057 import org.openstreetmap.josm.gui.layer.GpxLayer; 058 import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToMarkerLayer; 059 import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToNextMarker; 060 import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker; 061 import org.openstreetmap.josm.gui.layer.Layer; 062 import org.openstreetmap.josm.tools.ExifReader; 063 import org.openstreetmap.josm.tools.ImageProvider; 064 065 import com.drew.imaging.jpeg.JpegMetadataReader; 066 import com.drew.lang.CompoundException; 067 import com.drew.lang.Rational; 068 import com.drew.metadata.Directory; 069 import com.drew.metadata.Metadata; 070 import com.drew.metadata.MetadataException; 071 import com.drew.metadata.exif.ExifDirectory; 072 import com.drew.metadata.exif.GpsDirectory; 073 074 public class GeoImageLayer extends Layer implements PropertyChangeListener, JumpToMarkerLayer { 075 076 List<ImageEntry> data; 077 GpxLayer gpxLayer; 078 079 private Icon icon = ImageProvider.get("dialogs/geoimage/photo-marker"); 080 private Icon selectedIcon = ImageProvider.get("dialogs/geoimage/photo-marker-selected"); 081 082 private int currentPhoto = -1; 083 084 boolean useThumbs = false; 085 ThumbsLoader thumbsloader; 086 boolean thumbsLoaded = false; 087 private BufferedImage offscreenBuffer; 088 boolean updateOffscreenBuffer = true; 089 090 /** Loads a set of images, while displaying a dialog that indicates what the plugin is currently doing. 091 * In facts, this object is instantiated with a list of files. These files may be JPEG files or 092 * directories. In case of directories, they are scanned to find all the images they contain. 093 * Then all the images that have be found are loaded as ImageEntry instances. 094 */ 095 private static final class Loader extends PleaseWaitRunnable { 096 097 private boolean canceled = false; 098 private GeoImageLayer layer; 099 private Collection<File> selection; 100 private HashSet<String> loadedDirectories = new HashSet<String>(); 101 private LinkedHashSet<String> errorMessages; 102 private GpxLayer gpxLayer; 103 104 protected void rememberError(String message) { 105 this.errorMessages.add(message); 106 } 107 108 public Loader(Collection<File> selection, GpxLayer gpxLayer) { 109 super(tr("Extracting GPS locations from EXIF")); 110 this.selection = selection; 111 this.gpxLayer = gpxLayer; 112 errorMessages = new LinkedHashSet<String>(); 113 } 114 115 @Override protected void realRun() throws IOException { 116 117 progressMonitor.subTask(tr("Starting directory scan")); 118 Collection<File> files = new ArrayList<File>(); 119 try { 120 addRecursiveFiles(files, selection); 121 } catch(NullPointerException npe) { 122 rememberError(tr("One of the selected files was null")); 123 } 124 125 if (canceled) 126 return; 127 progressMonitor.subTask(tr("Read photos...")); 128 progressMonitor.setTicksCount(files.size()); 129 130 progressMonitor.subTask(tr("Read photos...")); 131 progressMonitor.setTicksCount(files.size()); 132 133 // read the image files 134 List<ImageEntry> data = new ArrayList<ImageEntry>(files.size()); 135 136 for (File f : files) { 137 138 if (canceled) { 139 break; 140 } 141 142 progressMonitor.subTask(tr("Reading {0}...", f.getName())); 143 progressMonitor.worked(1); 144 145 ImageEntry e = new ImageEntry(); 146 147 // Changed to silently cope with no time info in exif. One case 148 // of person having time that couldn't be parsed, but valid GPS info 149 150 try { 151 e.setExifTime(ExifReader.readTime(f)); 152 } catch (ParseException e1) { 153 e.setExifTime(null); 154 } 155 e.setFile(f); 156 extractExif(e); 157 data.add(e); 158 } 159 layer = new GeoImageLayer(data, gpxLayer); 160 files.clear(); 161 } 162 163 private void addRecursiveFiles(Collection<File> files, Collection<File> sel) { 164 boolean nullFile = false; 165 166 for (File f : sel) { 167 168 if(canceled) { 169 break; 170 } 171 172 if (f == null) { 173 nullFile = true; 174 175 } else if (f.isDirectory()) { 176 String canonical = null; 177 try { 178 canonical = f.getCanonicalPath(); 179 } catch (IOException e) { 180 e.printStackTrace(); 181 rememberError(tr("Unable to get canonical path for directory {0}\n", 182 f.getAbsolutePath())); 183 } 184 185 if (canonical == null || loadedDirectories.contains(canonical)) { 186 continue; 187 } else { 188 loadedDirectories.add(canonical); 189 } 190 191 Collection<File> children = Arrays.asList(f.listFiles(JpegFileFilter.getInstance())); 192 if (children != null) { 193 progressMonitor.subTask(tr("Scanning directory {0}", f.getPath())); 194 try { 195 addRecursiveFiles(files, children); 196 } catch(NullPointerException npe) { 197 npe.printStackTrace(); 198 rememberError(tr("Found null file in directory {0}\n", f.getPath())); 199 } 200 } else { 201 rememberError(tr("Error while getting files from directory {0}\n", f.getPath())); 202 } 203 204 } else { 205 files.add(f); 206 } 207 } 208 209 if (nullFile) 210 throw new NullPointerException(); 211 } 212 213 protected String formatErrorMessages() { 214 StringBuilder sb = new StringBuilder(); 215 sb.append("<html>"); 216 if (errorMessages.size() == 1) { 217 sb.append(errorMessages.iterator().next()); 218 } else { 219 sb.append("<ul>"); 220 for (String msg: errorMessages) { 221 sb.append("<li>").append(msg).append("</li>"); 222 } 223 sb.append("/ul>"); 224 } 225 sb.append("</html>"); 226 return sb.toString(); 227 } 228 229 @Override protected void finish() { 230 if (!errorMessages.isEmpty()) { 231 JOptionPane.showMessageDialog( 232 Main.parent, 233 formatErrorMessages(), 234 tr("Error"), 235 JOptionPane.ERROR_MESSAGE 236 ); 237 } 238 if (layer != null) { 239 Main.main.addLayer(layer); 240 241 if (! canceled && layer.data.size() > 0) { 242 boolean noGeotagFound = true; 243 for (ImageEntry e : layer.data) { 244 if (e.getPos() != null) { 245 noGeotagFound = false; 246 } 247 } 248 if (noGeotagFound) { 249 new CorrelateGpxWithImages(layer).actionPerformed(null); 250 } 251 } 252 } 253 } 254 255 @Override protected void cancel() { 256 canceled = true; 257 } 258 } 259 260 public static void create(Collection<File> files, GpxLayer gpxLayer) { 261 Loader loader = new Loader(files, gpxLayer); 262 Main.worker.execute(loader); 263 } 264 265 public GeoImageLayer(final List<ImageEntry> data, GpxLayer gpxLayer) { 266 267 super(tr("Geotagged Images")); 268 269 Collections.sort(data); 270 this.data = data; 271 this.gpxLayer = gpxLayer; 272 } 273 274 @Override 275 public Icon getIcon() { 276 return ImageProvider.get("dialogs/geoimage"); 277 } 278 279 private static List<Action> menuAdditions = new LinkedList<Action>(); 280 public static void registerMenuAddition(Action addition) { 281 menuAdditions.add(addition); 282 } 283 284 @Override 285 public Action[] getMenuEntries() { 286 287 List<Action> entries = new ArrayList<Action>(); 288 entries.add(LayerListDialog.getInstance().createShowHideLayerAction()); 289 entries.add(LayerListDialog.getInstance().createDeleteLayerAction()); 290 entries.add(new RenameLayerAction(null, this)); 291 entries.add(SeparatorLayerAction.INSTANCE); 292 entries.add(new CorrelateGpxWithImages(this)); 293 if (!menuAdditions.isEmpty()) { 294 entries.add(SeparatorLayerAction.INSTANCE); 295 entries.addAll(menuAdditions); 296 } 297 entries.add(SeparatorLayerAction.INSTANCE); 298 entries.add(new JumpToNextMarker(this)); 299 entries.add(new JumpToPreviousMarker(this)); 300 entries.add(SeparatorLayerAction.INSTANCE); 301 entries.add(new LayerListPopup.InfoAction(this)); 302 303 return entries.toArray(new Action[0]); 304 305 } 306 307 private String infoText() { 308 int i = 0; 309 for (ImageEntry e : data) 310 if (e.getPos() != null) { 311 i++; 312 } 313 return trn("{0} image loaded.", "{0} images loaded.", data.size(), data.size()) 314 + " " + trn("{0} was found to be GPS tagged.", "{0} were found to be GPS tagged.", i, i); 315 } 316 317 @Override public Object getInfoComponent() { 318 return infoText(); 319 } 320 321 @Override 322 public String getToolTipText() { 323 return infoText(); 324 } 325 326 @Override 327 public boolean isMergable(Layer other) { 328 return other instanceof GeoImageLayer; 329 } 330 331 @Override 332 public void mergeFrom(Layer from) { 333 GeoImageLayer l = (GeoImageLayer) from; 334 335 ImageEntry selected = null; 336 if (l.currentPhoto >= 0) { 337 selected = l.data.get(l.currentPhoto); 338 } 339 340 data.addAll(l.data); 341 Collections.sort(data); 342 343 // Supress the double photos. 344 if (data.size() > 1) { 345 ImageEntry cur; 346 ImageEntry prev = data.get(data.size() - 1); 347 for (int i = data.size() - 2; i >= 0; i--) { 348 cur = data.get(i); 349 if (cur.getFile().equals(prev.getFile())) { 350 data.remove(i); 351 } else { 352 prev = cur; 353 } 354 } 355 } 356 357 if (selected != null) { 358 for (int i = 0; i < data.size() ; i++) { 359 if (data.get(i) == selected) { 360 currentPhoto = i; 361 ImageViewerDialog.showImage(GeoImageLayer.this, data.get(i)); 362 break; 363 } 364 } 365 } 366 367 setName(l.getName()); 368 } 369 370 private Dimension scaledDimension(Image thumb) { 371 final double d = Main.map.mapView.getDist100Pixel(); 372 final double size = 10 /*meter*/; /* size of the photo on the map */ 373 double s = size * 100 /*px*/ / d; 374 375 final double sMin = ThumbsLoader.minSize; 376 final double sMax = ThumbsLoader.maxSize; 377 378 if (s < sMin) { 379 s = sMin; 380 } 381 if (s > sMax) { 382 s = sMax; 383 } 384 final double f = s / sMax; /* scale factor */ 385 386 if (thumb == null) 387 return null; 388 389 return new Dimension( 390 (int) Math.round(f * thumb.getWidth(null)), 391 (int) Math.round(f * thumb.getHeight(null))); 392 } 393 394 @Override 395 public void paint(Graphics2D g, MapView mv, Bounds bounds) { 396 int width = Main.map.mapView.getWidth(); 397 int height = Main.map.mapView.getHeight(); 398 Rectangle clip = g.getClipBounds(); 399 if (useThumbs) { 400 if (null == offscreenBuffer || offscreenBuffer.getWidth() != width // reuse the old buffer if possible 401 || offscreenBuffer.getHeight() != height) { 402 offscreenBuffer = new BufferedImage(width, height, 403 BufferedImage.TYPE_INT_ARGB); 404 updateOffscreenBuffer = true; 405 } 406 407 if (updateOffscreenBuffer) { 408 Graphics2D tempG = offscreenBuffer.createGraphics(); 409 tempG.setColor(new Color(0,0,0,0)); 410 Composite saveComp = tempG.getComposite(); 411 tempG.setComposite(AlphaComposite.Clear); // remove the old images 412 tempG.fillRect(0, 0, width, height); 413 tempG.setComposite(saveComp); 414 415 for (ImageEntry e : data) { 416 if (e.getPos() == null) { 417 continue; 418 } 419 Point p = mv.getPoint(e.getPos()); 420 if (e.thumbnail != null) { 421 Dimension d = scaledDimension(e.thumbnail); 422 Rectangle target = new Rectangle(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height); 423 if (clip.intersects(target)) { 424 tempG.drawImage(e.thumbnail, target.x, target.y, target.width, target.height, null); 425 } 426 } 427 else { // thumbnail not loaded yet 428 icon.paintIcon(mv, tempG, 429 p.x - icon.getIconWidth() / 2, 430 p.y - icon.getIconHeight() / 2); 431 } 432 } 433 updateOffscreenBuffer = false; 434 } 435 g.drawImage(offscreenBuffer, 0, 0, null); 436 } 437 else { 438 for (ImageEntry e : data) { 439 if (e.getPos() == null) { 440 continue; 441 } 442 Point p = mv.getPoint(e.getPos()); 443 icon.paintIcon(mv, g, 444 p.x - icon.getIconWidth() / 2, 445 p.y - icon.getIconHeight() / 2); 446 } 447 } 448 449 if (currentPhoto >= 0 && currentPhoto < data.size()) { 450 ImageEntry e = data.get(currentPhoto); 451 452 if (e.getPos() != null) { 453 Point p = mv.getPoint(e.getPos()); 454 455 if (e.thumbnail != null) { 456 Dimension d = scaledDimension(e.thumbnail); 457 g.setColor(new Color(128, 0, 0, 122)); 458 g.fillRect(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height); 459 } else { 460 if (e.getExifImgDir() != null) { 461 double arrowlength = 25; 462 double arrowwidth = 18; 463 464 double dir = e.getExifImgDir(); 465 // Rotate 90 degrees CCW 466 double headdir = ( dir < 90 ) ? dir + 270 : dir - 90; 467 double leftdir = ( headdir < 90 ) ? headdir + 270 : headdir - 90; 468 double rightdir = ( headdir > 270 ) ? headdir - 270 : headdir + 90; 469 470 double ptx = p.x + Math.cos(Math.toRadians(headdir)) * arrowlength; 471 double pty = p.y + Math.sin(Math.toRadians(headdir)) * arrowlength; 472 473 double ltx = p.x + Math.cos(Math.toRadians(leftdir)) * arrowwidth/2; 474 double lty = p.y + Math.sin(Math.toRadians(leftdir)) * arrowwidth/2; 475 476 double rtx = p.x + Math.cos(Math.toRadians(rightdir)) * arrowwidth/2; 477 double rty = p.y + Math.sin(Math.toRadians(rightdir)) * arrowwidth/2; 478 479 g.setColor(Color.white); 480 int[] xar = {(int) ltx, (int) ptx, (int) rtx, (int) ltx}; 481 int[] yar = {(int) lty, (int) pty, (int) rty, (int) lty}; 482 g.fillPolygon(xar, yar, 4); 483 } 484 485 selectedIcon.paintIcon(mv, g, 486 p.x - selectedIcon.getIconWidth() / 2, 487 p.y - selectedIcon.getIconHeight() / 2); 488 489 } 490 } 491 } 492 } 493 494 @Override 495 public void visitBoundingBox(BoundingXYVisitor v) { 496 for (ImageEntry e : data) { 497 v.visit(e.getPos()); 498 } 499 } 500 501 /* 502 * Extract gps from image exif 503 * 504 * If successful, fills in the LatLon and EastNorth attributes of passed in 505 * image; 506 */ 507 508 private static void extractExif(ImageEntry e) { 509 510 double deg; 511 double min, sec; 512 double lon, lat; 513 Metadata metadata = null; 514 Directory dirExif = null, dirGps = null; 515 516 try { 517 metadata = JpegMetadataReader.readMetadata(e.getFile()); 518 dirExif = metadata.getDirectory(ExifDirectory.class); 519 dirGps = metadata.getDirectory(GpsDirectory.class); 520 } catch (CompoundException p) { 521 e.setExifCoor(null); 522 e.setPos(null); 523 return; 524 } 525 526 try { 527 int orientation = dirExif.getInt(ExifDirectory.TAG_ORIENTATION); 528 e.setExifOrientation(orientation); 529 } catch (MetadataException ex) { 530 } 531 532 try { 533 double ele=dirGps.getDouble(GpsDirectory.TAG_GPS_ALTITUDE); 534 int d = dirGps.getInt(GpsDirectory.TAG_GPS_ALTITUDE_REF); 535 if (d == 1) { 536 ele *= -1; 537 } 538 e.setElevation(ele); 539 } catch (MetadataException ex) { 540 } 541 542 try { 543 // longitude 544 545 Rational[] components = dirGps.getRationalArray(GpsDirectory.TAG_GPS_LONGITUDE); 546 547 deg = components[0].doubleValue(); 548 min = components[1].doubleValue(); 549 sec = components[2].doubleValue(); 550 551 if (Double.isNaN(deg) && Double.isNaN(min) && Double.isNaN(sec)) 552 throw new IllegalArgumentException(); 553 554 lon = (Double.isNaN(deg) ? 0 : deg + (Double.isNaN(min) ? 0 : (min / 60)) + (Double.isNaN(sec) ? 0 : (sec / 3600))); 555 556 if (dirGps.getString(GpsDirectory.TAG_GPS_LONGITUDE_REF).charAt(0) == 'W') { 557 lon = -lon; 558 } 559 560 // latitude 561 562 components = dirGps.getRationalArray(GpsDirectory.TAG_GPS_LATITUDE); 563 564 deg = components[0].doubleValue(); 565 min = components[1].doubleValue(); 566 sec = components[2].doubleValue(); 567 568 if (Double.isNaN(deg) && Double.isNaN(min) && Double.isNaN(sec)) 569 throw new IllegalArgumentException(); 570 571 lat = (Double.isNaN(deg) ? 0 : deg + (Double.isNaN(min) ? 0 : (min / 60)) + (Double.isNaN(sec) ? 0 : (sec / 3600))); 572 573 if (Double.isNaN(lat)) 574 throw new IllegalArgumentException(); 575 576 if (dirGps.getString(GpsDirectory.TAG_GPS_LATITUDE_REF).charAt(0) == 'S') { 577 lat = -lat; 578 } 579 580 // Store values 581 582 e.setExifCoor(new LatLon(lat, lon)); 583 e.setPos(e.getExifCoor()); 584 585 } catch (CompoundException p) { 586 // Try to read lon/lat as double value (Nonstandard, created by some cameras -> #5220) 587 try { 588 Double longitude = dirGps.getDouble(GpsDirectory.TAG_GPS_LONGITUDE); 589 Double latitude = dirGps.getDouble(GpsDirectory.TAG_GPS_LATITUDE); 590 if (longitude == null || latitude == null) 591 throw new CompoundException(""); 592 593 // Store values 594 595 e.setExifCoor(new LatLon(latitude, longitude)); 596 e.setPos(e.getExifCoor()); 597 } catch (CompoundException ex) { 598 e.setExifCoor(null); 599 e.setPos(null); 600 } 601 } catch (Exception ex) { // (other exceptions, e.g. #5271) 602 System.err.println("Error reading EXIF from file: "+ex); 603 e.setExifCoor(null); 604 e.setPos(null); 605 } 606 607 // compass direction value 608 609 Rational direction = null; 610 611 try { 612 direction = dirGps.getRational(GpsDirectory.TAG_GPS_IMG_DIRECTION); 613 if (direction != null) { 614 e.setExifImgDir(direction.doubleValue()); 615 } 616 } catch (Exception ex) { // (CompoundException and other exceptions, e.g. #5271) 617 // Do nothing 618 } 619 } 620 621 public void showNextPhoto() { 622 if (data != null && data.size() > 0) { 623 currentPhoto++; 624 if (currentPhoto >= data.size()) { 625 currentPhoto = data.size() - 1; 626 } 627 ImageViewerDialog.showImage(this, data.get(currentPhoto)); 628 } else { 629 currentPhoto = -1; 630 } 631 Main.map.repaint(); 632 } 633 634 public void showPreviousPhoto() { 635 if (data != null && data.size() > 0) { 636 currentPhoto--; 637 if (currentPhoto < 0) { 638 currentPhoto = 0; 639 } 640 ImageViewerDialog.showImage(this, data.get(currentPhoto)); 641 } else { 642 currentPhoto = -1; 643 } 644 Main.map.repaint(); 645 } 646 647 public void checkPreviousNextButtons() { 648 ImageViewerDialog.setNextEnabled(currentPhoto < data.size() - 1); 649 ImageViewerDialog.setPreviousEnabled(currentPhoto > 0); 650 } 651 652 public void removeCurrentPhoto() { 653 if (data != null && data.size() > 0 && currentPhoto >= 0 && currentPhoto < data.size()) { 654 data.remove(currentPhoto); 655 if (currentPhoto >= data.size()) { 656 currentPhoto = data.size() - 1; 657 } 658 if (currentPhoto >= 0) { 659 ImageViewerDialog.showImage(this, data.get(currentPhoto)); 660 } else { 661 ImageViewerDialog.showImage(this, null); 662 } 663 updateOffscreenBuffer = true; 664 Main.map.repaint(); 665 } 666 } 667 668 public void removeCurrentPhotoFromDisk() { 669 ImageEntry toDelete = null; 670 if (data != null && data.size() > 0 && currentPhoto >= 0 && currentPhoto < data.size()) { 671 toDelete = data.get(currentPhoto); 672 673 int result = new ExtendedDialog( 674 Main.parent, 675 tr("Delete image file from disk"), 676 new String[] {tr("Cancel"), tr("Delete")}) 677 .setButtonIcons(new String[] {"cancel.png", "dialogs/delete.png"}) 678 .setContent(new JLabel(tr("<html><h3>Delete the file {0} from disk?<p>The image file will be permanently lost!</h3></html>" 679 ,toDelete.getFile().getName()), ImageProvider.get("dialogs/geoimage/deletefromdisk"),SwingConstants.LEFT)) 680 .toggleEnable("geoimage.deleteimagefromdisk") 681 .setCancelButton(1) 682 .setDefaultButton(2) 683 .showDialog() 684 .getValue(); 685 686 if(result == 2) 687 { 688 data.remove(currentPhoto); 689 if (currentPhoto >= data.size()) { 690 currentPhoto = data.size() - 1; 691 } 692 if (currentPhoto >= 0) { 693 ImageViewerDialog.showImage(this, data.get(currentPhoto)); 694 } else { 695 ImageViewerDialog.showImage(this, null); 696 } 697 698 if (toDelete.getFile().delete()) { 699 System.out.println("File "+toDelete.getFile().toString()+" deleted. "); 700 } else { 701 JOptionPane.showMessageDialog( 702 Main.parent, 703 tr("Image file could not be deleted."), 704 tr("Error"), 705 JOptionPane.ERROR_MESSAGE 706 ); 707 } 708 709 updateOffscreenBuffer = true; 710 Main.map.repaint(); 711 } 712 } 713 } 714 715 private MouseAdapter mouseAdapter = null; 716 private MapModeChangeListener mapModeListener = null; 717 718 @Override 719 public void hookUpMapView() { 720 mouseAdapter = new MouseAdapter() { 721 private final boolean isMapModeOk() { 722 return Main.map.mapMode == null || Main.map.mapMode instanceof SelectAction; 723 } 724 @Override public void mousePressed(MouseEvent e) { 725 726 if (e.getButton() != MouseEvent.BUTTON1) 727 return; 728 if (isVisible() && isMapModeOk()) { 729 Main.map.mapView.repaint(); 730 } 731 } 732 733 @Override public void mouseReleased(MouseEvent ev) { 734 if (ev.getButton() != MouseEvent.BUTTON1) 735 return; 736 if (data == null || !isVisible() || !isMapModeOk()) 737 return; 738 739 for (int i = data.size() - 1; i >= 0; --i) { 740 ImageEntry e = data.get(i); 741 if (e.getPos() == null) { 742 continue; 743 } 744 Point p = Main.map.mapView.getPoint(e.getPos()); 745 Rectangle r; 746 if (e.thumbnail != null) { 747 Dimension d = scaledDimension(e.thumbnail); 748 r = new Rectangle(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height); 749 } else { 750 r = new Rectangle(p.x - icon.getIconWidth() / 2, 751 p.y - icon.getIconHeight() / 2, 752 icon.getIconWidth(), 753 icon.getIconHeight()); 754 } 755 if (r.contains(ev.getPoint())) { 756 currentPhoto = i; 757 ImageViewerDialog.showImage(GeoImageLayer.this, e); 758 Main.map.repaint(); 759 break; 760 } 761 } 762 } 763 }; 764 765 mapModeListener = new MapModeChangeListener() { 766 public void mapModeChange(MapMode oldMapMode, MapMode newMapMode) { 767 if (newMapMode == null || (newMapMode instanceof org.openstreetmap.josm.actions.mapmode.SelectAction)) { 768 Main.map.mapView.addMouseListener(mouseAdapter); 769 } else { 770 Main.map.mapView.removeMouseListener(mouseAdapter); 771 } 772 } 773 }; 774 775 MapFrame.addMapModeChangeListener(mapModeListener); 776 mapModeListener.mapModeChange(null, Main.map.mapMode); 777 778 MapView.addLayerChangeListener(new LayerChangeListener() { 779 public void activeLayerChange(Layer oldLayer, Layer newLayer) { 780 if (newLayer == GeoImageLayer.this) { 781 // only in select mode it is possible to click the images 782 Main.map.selectSelectTool(false); 783 } 784 } 785 786 public void layerAdded(Layer newLayer) { 787 } 788 789 public void layerRemoved(Layer oldLayer) { 790 if (oldLayer == GeoImageLayer.this) { 791 if (thumbsloader != null) { 792 thumbsloader.stop = true; 793 } 794 Main.map.mapView.removeMouseListener(mouseAdapter); 795 MapFrame.removeMapModeChangeListener(mapModeListener); 796 currentPhoto = -1; 797 data.clear(); 798 data = null; 799 // stop listening to layer change events 800 MapView.removeLayerChangeListener(this); 801 } 802 } 803 }); 804 805 Main.map.mapView.addPropertyChangeListener(this); 806 if (Main.map.getToggleDialog(ImageViewerDialog.class) == null) { 807 ImageViewerDialog.newInstance(); 808 Main.map.addToggleDialog(ImageViewerDialog.getInstance()); 809 } 810 } 811 812 public void propertyChange(PropertyChangeEvent evt) { 813 if (NavigatableComponent.PROPNAME_CENTER.equals(evt.getPropertyName()) || NavigatableComponent.PROPNAME_SCALE.equals(evt.getPropertyName())) { 814 updateOffscreenBuffer = true; 815 } 816 } 817 818 public void loadThumbs() { 819 if (useThumbs && !thumbsLoaded) { 820 thumbsLoaded = true; 821 thumbsloader = new ThumbsLoader(this); 822 Thread t = new Thread(thumbsloader); 823 t.setPriority(Thread.MIN_PRIORITY); 824 t.start(); 825 } 826 } 827 828 public void updateBufferAndRepaint() { 829 updateOffscreenBuffer = true; 830 Main.map.mapView.repaint(); 831 } 832 833 public List<ImageEntry> getImages() { 834 List<ImageEntry> copy = new ArrayList<ImageEntry>(); 835 for (ImageEntry ie : data) { 836 copy.add(ie.clone()); 837 } 838 return copy; 839 } 840 841 public GpxLayer getGpxLayer() { 842 return gpxLayer; 843 } 844 845 public void jumpToNextMarker() { 846 showNextPhoto(); 847 } 848 849 public void jumpToPreviousMarker() { 850 showPreviousPhoto(); 851 } 852 }