001// License: GPL. See LICENSE file for details. 002package org.openstreetmap.josm.gui.layer.geoimage; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.awt.AlphaComposite; 008import java.awt.Color; 009import java.awt.Composite; 010import java.awt.Dimension; 011import java.awt.Graphics2D; 012import java.awt.Image; 013import java.awt.Point; 014import java.awt.Rectangle; 015import java.awt.event.MouseAdapter; 016import java.awt.event.MouseEvent; 017import java.awt.image.BufferedImage; 018import java.beans.PropertyChangeEvent; 019import java.beans.PropertyChangeListener; 020import java.io.File; 021import java.io.IOException; 022import java.text.ParseException; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Calendar; 026import java.util.Collection; 027import java.util.Collections; 028import java.util.GregorianCalendar; 029import java.util.HashSet; 030import java.util.LinkedHashSet; 031import java.util.LinkedList; 032import java.util.List; 033import java.util.Set; 034import java.util.TimeZone; 035 036import javax.swing.Action; 037import javax.swing.Icon; 038import javax.swing.JLabel; 039import javax.swing.JOptionPane; 040import javax.swing.SwingConstants; 041 042import org.openstreetmap.josm.Main; 043import org.openstreetmap.josm.actions.RenameLayerAction; 044import org.openstreetmap.josm.actions.mapmode.MapMode; 045import org.openstreetmap.josm.actions.mapmode.SelectAction; 046import org.openstreetmap.josm.data.Bounds; 047import org.openstreetmap.josm.data.coor.LatLon; 048import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 049import org.openstreetmap.josm.gui.ExtendedDialog; 050import org.openstreetmap.josm.gui.MapFrame; 051import org.openstreetmap.josm.gui.MapFrame.MapModeChangeListener; 052import org.openstreetmap.josm.gui.MapView; 053import org.openstreetmap.josm.gui.MapView.LayerChangeListener; 054import org.openstreetmap.josm.gui.NavigatableComponent; 055import org.openstreetmap.josm.gui.PleaseWaitRunnable; 056import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 057import org.openstreetmap.josm.gui.dialogs.LayerListPopup; 058import org.openstreetmap.josm.gui.layer.GpxLayer; 059import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToMarkerLayer; 060import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToNextMarker; 061import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker; 062import org.openstreetmap.josm.gui.layer.Layer; 063import org.openstreetmap.josm.tools.ExifReader; 064import org.openstreetmap.josm.tools.ImageProvider; 065 066import com.drew.imaging.jpeg.JpegMetadataReader; 067import com.drew.lang.CompoundException; 068import com.drew.metadata.Directory; 069import com.drew.metadata.Metadata; 070import com.drew.metadata.MetadataException; 071import com.drew.metadata.exif.ExifIFD0Directory; 072import com.drew.metadata.exif.GpsDirectory; 073 074/** 075 * Layer displaying geottaged pictures. 076 */ 077public class GeoImageLayer extends Layer implements PropertyChangeListener, JumpToMarkerLayer { 078 079 List<ImageEntry> data; 080 GpxLayer gpxLayer; 081 082 private Icon icon = ImageProvider.get("dialogs/geoimage/photo-marker"); 083 private Icon selectedIcon = ImageProvider.get("dialogs/geoimage/photo-marker-selected"); 084 085 private int currentPhoto = -1; 086 087 boolean useThumbs = false; 088 ThumbsLoader thumbsloader; 089 boolean thumbsLoaded = false; 090 private BufferedImage offscreenBuffer; 091 boolean updateOffscreenBuffer = true; 092 093 /** Loads a set of images, while displaying a dialog that indicates what the plugin is currently doing. 094 * In facts, this object is instantiated with a list of files. These files may be JPEG files or 095 * directories. In case of directories, they are scanned to find all the images they contain. 096 * Then all the images that have be found are loaded as ImageEntry instances. 097 */ 098 private static final class Loader extends PleaseWaitRunnable { 099 100 private boolean canceled = false; 101 private GeoImageLayer layer; 102 private Collection<File> selection; 103 private Set<String> loadedDirectories = new HashSet<String>(); 104 private Set<String> errorMessages; 105 private GpxLayer gpxLayer; 106 107 protected void rememberError(String message) { 108 this.errorMessages.add(message); 109 } 110 111 public Loader(Collection<File> selection, GpxLayer gpxLayer) { 112 super(tr("Extracting GPS locations from EXIF")); 113 this.selection = selection; 114 this.gpxLayer = gpxLayer; 115 errorMessages = new LinkedHashSet<String>(); 116 } 117 118 @Override protected void realRun() throws IOException { 119 120 progressMonitor.subTask(tr("Starting directory scan")); 121 Collection<File> files = new ArrayList<File>(); 122 try { 123 addRecursiveFiles(files, selection); 124 } catch (IllegalStateException e) { 125 rememberError(e.getMessage()); 126 } 127 128 if (canceled) 129 return; 130 progressMonitor.subTask(tr("Read photos...")); 131 progressMonitor.setTicksCount(files.size()); 132 133 progressMonitor.subTask(tr("Read photos...")); 134 progressMonitor.setTicksCount(files.size()); 135 136 // read the image files 137 List<ImageEntry> data = new ArrayList<ImageEntry>(files.size()); 138 139 for (File f : files) { 140 141 if (canceled) { 142 break; 143 } 144 145 progressMonitor.subTask(tr("Reading {0}...", f.getName())); 146 progressMonitor.worked(1); 147 148 ImageEntry e = new ImageEntry(); 149 150 // Changed to silently cope with no time info in exif. One case 151 // of person having time that couldn't be parsed, but valid GPS info 152 153 try { 154 e.setExifTime(ExifReader.readTime(f)); 155 } catch (ParseException ex) { 156 e.setExifTime(null); 157 } 158 e.setFile(f); 159 extractExif(e); 160 data.add(e); 161 } 162 layer = new GeoImageLayer(data, gpxLayer); 163 files.clear(); 164 } 165 166 private void addRecursiveFiles(Collection<File> files, Collection<File> sel) { 167 boolean nullFile = false; 168 169 for (File f : sel) { 170 171 if(canceled) { 172 break; 173 } 174 175 if (f == null) { 176 nullFile = true; 177 178 } else if (f.isDirectory()) { 179 String canonical = null; 180 try { 181 canonical = f.getCanonicalPath(); 182 } catch (IOException e) { 183 e.printStackTrace(); 184 rememberError(tr("Unable to get canonical path for directory {0}\n", 185 f.getAbsolutePath())); 186 } 187 188 if (canonical == null || loadedDirectories.contains(canonical)) { 189 continue; 190 } else { 191 loadedDirectories.add(canonical); 192 } 193 194 File[] children = f.listFiles(JpegFileFilter.getInstance()); 195 if (children != null) { 196 progressMonitor.subTask(tr("Scanning directory {0}", f.getPath())); 197 try { 198 addRecursiveFiles(files, Arrays.asList(children)); 199 } catch(NullPointerException npe) { 200 npe.printStackTrace(); 201 rememberError(tr("Found null file in directory {0}\n", f.getPath())); 202 } 203 } else { 204 rememberError(tr("Error while getting files from directory {0}\n", f.getPath())); 205 } 206 207 } else { 208 files.add(f); 209 } 210 } 211 212 if (nullFile) { 213 throw new IllegalStateException(tr("One of the selected files was null")); 214 } 215 } 216 217 protected String formatErrorMessages() { 218 StringBuilder sb = new StringBuilder(); 219 sb.append("<html>"); 220 if (errorMessages.size() == 1) { 221 sb.append(errorMessages.iterator().next()); 222 } else { 223 sb.append("<ul>"); 224 for (String msg: errorMessages) { 225 sb.append("<li>").append(msg).append("</li>"); 226 } 227 sb.append("/ul>"); 228 } 229 sb.append("</html>"); 230 return sb.toString(); 231 } 232 233 @Override protected void finish() { 234 if (!errorMessages.isEmpty()) { 235 JOptionPane.showMessageDialog( 236 Main.parent, 237 formatErrorMessages(), 238 tr("Error"), 239 JOptionPane.ERROR_MESSAGE 240 ); 241 } 242 if (layer != null) { 243 Main.main.addLayer(layer); 244 245 if (!canceled && !layer.data.isEmpty()) { 246 boolean noGeotagFound = true; 247 for (ImageEntry e : layer.data) { 248 if (e.getPos() != null) { 249 noGeotagFound = false; 250 } 251 } 252 if (noGeotagFound) { 253 new CorrelateGpxWithImages(layer).actionPerformed(null); 254 } 255 } 256 } 257 } 258 259 @Override protected void cancel() { 260 canceled = true; 261 } 262 } 263 264 public static void create(Collection<File> files, GpxLayer gpxLayer) { 265 Loader loader = new Loader(files, gpxLayer); 266 Main.worker.execute(loader); 267 } 268 269 /** 270 * Constructs a new {@code GeoImageLayer}. 271 * @param data The list of images to display 272 * @param gpxLayer The associated GPX layer 273 */ 274 public GeoImageLayer(final List<ImageEntry> data, GpxLayer gpxLayer) { 275 this(data, gpxLayer, null, false); 276 } 277 278 /** 279 * Constructs a new {@code GeoImageLayer}. 280 * @param data The list of images to display 281 * @param gpxLayer The associated GPX layer 282 * @param name Layer name 283 * @since 6392 284 */ 285 public GeoImageLayer(final List<ImageEntry> data, GpxLayer gpxLayer, final String name) { 286 this(data, gpxLayer, name, false); 287 } 288 289 /** 290 * Constructs a new {@code GeoImageLayer}. 291 * @param data The list of images to display 292 * @param gpxLayer The associated GPX layer 293 * @param useThumbs Thumbnail display flag 294 * @since 6392 295 */ 296 public GeoImageLayer(final List<ImageEntry> data, GpxLayer gpxLayer, boolean useThumbs) { 297 this(data, gpxLayer, null, useThumbs); 298 } 299 300 /** 301 * Constructs a new {@code GeoImageLayer}. 302 * @param data The list of images to display 303 * @param gpxLayer The associated GPX layer 304 * @param name Layer name 305 * @param useThumbs Thumbnail display flag 306 * @since 6392 307 */ 308 public GeoImageLayer(final List<ImageEntry> data, GpxLayer gpxLayer, final String name, boolean useThumbs) { 309 super(name != null ? name : tr("Geotagged Images")); 310 Collections.sort(data); 311 this.data = data; 312 this.gpxLayer = gpxLayer; 313 this.useThumbs = useThumbs; 314 } 315 316 @Override 317 public Icon getIcon() { 318 return ImageProvider.get("dialogs/geoimage"); 319 } 320 321 private static List<Action> menuAdditions = new LinkedList<Action>(); 322 public static void registerMenuAddition(Action addition) { 323 menuAdditions.add(addition); 324 } 325 326 @Override 327 public Action[] getMenuEntries() { 328 329 List<Action> entries = new ArrayList<Action>(); 330 entries.add(LayerListDialog.getInstance().createShowHideLayerAction()); 331 entries.add(LayerListDialog.getInstance().createDeleteLayerAction()); 332 entries.add(new RenameLayerAction(null, this)); 333 entries.add(SeparatorLayerAction.INSTANCE); 334 entries.add(new CorrelateGpxWithImages(this)); 335 if (!menuAdditions.isEmpty()) { 336 entries.add(SeparatorLayerAction.INSTANCE); 337 entries.addAll(menuAdditions); 338 } 339 entries.add(SeparatorLayerAction.INSTANCE); 340 entries.add(new JumpToNextMarker(this)); 341 entries.add(new JumpToPreviousMarker(this)); 342 entries.add(SeparatorLayerAction.INSTANCE); 343 entries.add(new LayerListPopup.InfoAction(this)); 344 345 return entries.toArray(new Action[entries.size()]); 346 347 } 348 349 private String infoText() { 350 int i = 0; 351 for (ImageEntry e : data) 352 if (e.getPos() != null) { 353 i++; 354 } 355 return trn("{0} image loaded.", "{0} images loaded.", data.size(), data.size()) 356 + " " + trn("{0} was found to be GPS tagged.", "{0} were found to be GPS tagged.", i, i); 357 } 358 359 @Override public Object getInfoComponent() { 360 return infoText(); 361 } 362 363 @Override 364 public String getToolTipText() { 365 return infoText(); 366 } 367 368 @Override 369 public boolean isMergable(Layer other) { 370 return other instanceof GeoImageLayer; 371 } 372 373 @Override 374 public void mergeFrom(Layer from) { 375 GeoImageLayer l = (GeoImageLayer) from; 376 377 ImageEntry selected = null; 378 if (l.currentPhoto >= 0) { 379 selected = l.data.get(l.currentPhoto); 380 } 381 382 data.addAll(l.data); 383 Collections.sort(data); 384 385 // Supress the double photos. 386 if (data.size() > 1) { 387 ImageEntry cur; 388 ImageEntry prev = data.get(data.size() - 1); 389 for (int i = data.size() - 2; i >= 0; i--) { 390 cur = data.get(i); 391 if (cur.getFile().equals(prev.getFile())) { 392 data.remove(i); 393 } else { 394 prev = cur; 395 } 396 } 397 } 398 399 if (selected != null) { 400 for (int i = 0; i < data.size() ; i++) { 401 if (data.get(i) == selected) { 402 currentPhoto = i; 403 ImageViewerDialog.showImage(GeoImageLayer.this, data.get(i)); 404 break; 405 } 406 } 407 } 408 409 setName(l.getName()); 410 } 411 412 private Dimension scaledDimension(Image thumb) { 413 final double d = Main.map.mapView.getDist100Pixel(); 414 final double size = 10 /*meter*/; /* size of the photo on the map */ 415 double s = size * 100 /*px*/ / d; 416 417 final double sMin = ThumbsLoader.minSize; 418 final double sMax = ThumbsLoader.maxSize; 419 420 if (s < sMin) { 421 s = sMin; 422 } 423 if (s > sMax) { 424 s = sMax; 425 } 426 final double f = s / sMax; /* scale factor */ 427 428 if (thumb == null) 429 return null; 430 431 return new Dimension( 432 (int) Math.round(f * thumb.getWidth(null)), 433 (int) Math.round(f * thumb.getHeight(null))); 434 } 435 436 @Override 437 public void paint(Graphics2D g, MapView mv, Bounds bounds) { 438 int width = mv.getWidth(); 439 int height = mv.getHeight(); 440 Rectangle clip = g.getClipBounds(); 441 if (useThumbs) { 442 if (!thumbsLoaded) { 443 loadThumbs(); 444 } 445 446 if (null == offscreenBuffer || offscreenBuffer.getWidth() != width // reuse the old buffer if possible 447 || offscreenBuffer.getHeight() != height) { 448 offscreenBuffer = new BufferedImage(width, height, 449 BufferedImage.TYPE_INT_ARGB); 450 updateOffscreenBuffer = true; 451 } 452 453 if (updateOffscreenBuffer) { 454 Graphics2D tempG = offscreenBuffer.createGraphics(); 455 tempG.setColor(new Color(0,0,0,0)); 456 Composite saveComp = tempG.getComposite(); 457 tempG.setComposite(AlphaComposite.Clear); // remove the old images 458 tempG.fillRect(0, 0, width, height); 459 tempG.setComposite(saveComp); 460 461 for (ImageEntry e : data) { 462 if (e.getPos() == null) { 463 continue; 464 } 465 Point p = mv.getPoint(e.getPos()); 466 if (e.thumbnail != null) { 467 Dimension d = scaledDimension(e.thumbnail); 468 Rectangle target = new Rectangle(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height); 469 if (clip.intersects(target)) { 470 tempG.drawImage(e.thumbnail, target.x, target.y, target.width, target.height, null); 471 } 472 } 473 else { // thumbnail not loaded yet 474 icon.paintIcon(mv, tempG, 475 p.x - icon.getIconWidth() / 2, 476 p.y - icon.getIconHeight() / 2); 477 } 478 } 479 updateOffscreenBuffer = false; 480 } 481 g.drawImage(offscreenBuffer, 0, 0, null); 482 } 483 else { 484 for (ImageEntry e : data) { 485 if (e.getPos() == null) { 486 continue; 487 } 488 Point p = mv.getPoint(e.getPos()); 489 icon.paintIcon(mv, g, 490 p.x - icon.getIconWidth() / 2, 491 p.y - icon.getIconHeight() / 2); 492 } 493 } 494 495 if (currentPhoto >= 0 && currentPhoto < data.size()) { 496 ImageEntry e = data.get(currentPhoto); 497 498 if (e.getPos() != null) { 499 Point p = mv.getPoint(e.getPos()); 500 501 if (useThumbs && e.thumbnail != null) { 502 Dimension d = scaledDimension(e.thumbnail); 503 g.setColor(new Color(128, 0, 0, 122)); 504 g.fillRect(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height); 505 } else { 506 if (e.getExifImgDir() != null) { 507 double arrowlength = 25; 508 double arrowwidth = 18; 509 510 double dir = e.getExifImgDir(); 511 // Rotate 90 degrees CCW 512 double headdir = ( dir < 90 ) ? dir + 270 : dir - 90; 513 double leftdir = ( headdir < 90 ) ? headdir + 270 : headdir - 90; 514 double rightdir = ( headdir > 270 ) ? headdir - 270 : headdir + 90; 515 516 double ptx = p.x + Math.cos(Math.toRadians(headdir)) * arrowlength; 517 double pty = p.y + Math.sin(Math.toRadians(headdir)) * arrowlength; 518 519 double ltx = p.x + Math.cos(Math.toRadians(leftdir)) * arrowwidth/2; 520 double lty = p.y + Math.sin(Math.toRadians(leftdir)) * arrowwidth/2; 521 522 double rtx = p.x + Math.cos(Math.toRadians(rightdir)) * arrowwidth/2; 523 double rty = p.y + Math.sin(Math.toRadians(rightdir)) * arrowwidth/2; 524 525 g.setColor(Color.white); 526 int[] xar = {(int) ltx, (int) ptx, (int) rtx, (int) ltx}; 527 int[] yar = {(int) lty, (int) pty, (int) rty, (int) lty}; 528 g.fillPolygon(xar, yar, 4); 529 } 530 531 selectedIcon.paintIcon(mv, g, 532 p.x - selectedIcon.getIconWidth() / 2, 533 p.y - selectedIcon.getIconHeight() / 2); 534 535 } 536 } 537 } 538 } 539 540 @Override 541 public void visitBoundingBox(BoundingXYVisitor v) { 542 for (ImageEntry e : data) { 543 v.visit(e.getPos()); 544 } 545 } 546 547 /** 548 * Extract GPS metadata from image EXIF 549 * 550 * If successful, fills in the LatLon and EastNorth attributes of passed in image 551 */ 552 private static void extractExif(ImageEntry e) { 553 554 Metadata metadata; 555 Directory dirExif; 556 GpsDirectory dirGps; 557 558 try { 559 metadata = JpegMetadataReader.readMetadata(e.getFile()); 560 dirExif = metadata.getDirectory(ExifIFD0Directory.class); 561 dirGps = metadata.getDirectory(GpsDirectory.class); 562 } catch (CompoundException p) { 563 e.setExifCoor(null); 564 e.setPos(null); 565 return; 566 } catch (IOException p) { 567 e.setExifCoor(null); 568 e.setPos(null); 569 return; 570 } 571 572 try { 573 if (dirExif != null) { 574 int orientation = dirExif.getInt(ExifIFD0Directory.TAG_ORIENTATION); 575 e.setExifOrientation(orientation); 576 } 577 } catch (MetadataException ex) { 578 } 579 580 if (dirGps == null) { 581 e.setExifCoor(null); 582 e.setPos(null); 583 return; 584 } 585 586 try { 587 double ele = dirGps.getDouble(GpsDirectory.TAG_GPS_ALTITUDE); 588 int d = dirGps.getInt(GpsDirectory.TAG_GPS_ALTITUDE_REF); 589 if (d == 1) { 590 ele *= -1; 591 } 592 e.setElevation(ele); 593 } catch (MetadataException ex) { 594 } 595 596 try { 597 LatLon latlon = ExifReader.readLatLon(dirGps); 598 e.setExifCoor(latlon); 599 e.setPos(e.getExifCoor()); 600 601 } catch (Exception ex) { // (other exceptions, e.g. #5271) 602 Main.error("Error reading EXIF from file: "+ex); 603 e.setExifCoor(null); 604 e.setPos(null); 605 } 606 607 try { 608 Double direction = ExifReader.readDirection(dirGps); 609 if (direction != null) { 610 e.setExifImgDir(direction.doubleValue()); 611 } 612 } catch (Exception ex) { // (CompoundException and other exceptions, e.g. #5271) 613 // Do nothing 614 } 615 616 // Time and date. We can have these cases: 617 // 1) GPS_TIME_STAMP not set -> date/time will be null 618 // 2) GPS_DATE_STAMP not set -> use EXIF date or set to default 619 // 3) GPS_TIME_STAMP and GPS_DATE_STAMP are set 620 int[] timeStampComps = dirGps.getIntArray(GpsDirectory.TAG_GPS_TIME_STAMP); 621 if (timeStampComps != null) { 622 int gpsHour = timeStampComps[0]; 623 int gpsMin = timeStampComps[1]; 624 int gpsSec = timeStampComps[2]; 625 Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC")); 626 627 // We have the time. Next step is to check if the GPS date stamp is set. 628 // dirGps.getString() always succeeds, but the return value might be null. 629 String dateStampStr = dirGps.getString(GpsDirectory.TAG_GPS_DATE_STAMP); 630 if (dateStampStr != null && dateStampStr.matches("^\\d+:\\d+:\\d+$")) { 631 String[] dateStampComps = dateStampStr.split(":"); 632 cal.set(Calendar.YEAR, Integer.parseInt(dateStampComps[0])); 633 cal.set(Calendar.MONTH, Integer.parseInt(dateStampComps[1]) - 1); 634 cal.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dateStampComps[2])); 635 } 636 else { 637 // No GPS date stamp in EXIF data. Copy it from EXIF time. 638 // Date is not set if EXIF time is not available. 639 if (e.hasExifTime()) { 640 // Time not set yet, so we can copy everything, not just date. 641 cal.setTime(e.getExifTime()); 642 } 643 } 644 645 cal.set(Calendar.HOUR_OF_DAY, gpsHour); 646 cal.set(Calendar.MINUTE, gpsMin); 647 cal.set(Calendar.SECOND, gpsSec); 648 649 e.setExifGpsTime(cal.getTime()); 650 } 651 } 652 653 public void showNextPhoto() { 654 if (data != null && data.size() > 0) { 655 currentPhoto++; 656 if (currentPhoto >= data.size()) { 657 currentPhoto = data.size() - 1; 658 } 659 ImageViewerDialog.showImage(this, data.get(currentPhoto)); 660 } else { 661 currentPhoto = -1; 662 } 663 Main.map.repaint(); 664 } 665 666 public void showPreviousPhoto() { 667 if (data != null && !data.isEmpty()) { 668 currentPhoto--; 669 if (currentPhoto < 0) { 670 currentPhoto = 0; 671 } 672 ImageViewerDialog.showImage(this, data.get(currentPhoto)); 673 } else { 674 currentPhoto = -1; 675 } 676 Main.map.repaint(); 677 } 678 679 public void showFirstPhoto() { 680 if (data != null && data.size() > 0) { 681 currentPhoto = 0; 682 ImageViewerDialog.showImage(this, data.get(currentPhoto)); 683 } else { 684 currentPhoto = -1; 685 } 686 Main.map.repaint(); 687 } 688 689 public void showLastPhoto() { 690 if (data != null && data.size() > 0) { 691 currentPhoto = data.size() - 1; 692 ImageViewerDialog.showImage(this, data.get(currentPhoto)); 693 } else { 694 currentPhoto = -1; 695 } 696 Main.map.repaint(); 697 } 698 699 public void checkPreviousNextButtons() { 700 ImageViewerDialog.setNextEnabled(currentPhoto < data.size() - 1); 701 ImageViewerDialog.setPreviousEnabled(currentPhoto > 0); 702 } 703 704 public void removeCurrentPhoto() { 705 if (data != null && data.size() > 0 && currentPhoto >= 0 && currentPhoto < data.size()) { 706 data.remove(currentPhoto); 707 if (currentPhoto >= data.size()) { 708 currentPhoto = data.size() - 1; 709 } 710 if (currentPhoto >= 0) { 711 ImageViewerDialog.showImage(this, data.get(currentPhoto)); 712 } else { 713 ImageViewerDialog.showImage(this, null); 714 } 715 updateOffscreenBuffer = true; 716 Main.map.repaint(); 717 } 718 } 719 720 public void removeCurrentPhotoFromDisk() { 721 ImageEntry toDelete = null; 722 if (data != null && data.size() > 0 && currentPhoto >= 0 && currentPhoto < data.size()) { 723 toDelete = data.get(currentPhoto); 724 725 int result = new ExtendedDialog( 726 Main.parent, 727 tr("Delete image file from disk"), 728 new String[] {tr("Cancel"), tr("Delete")}) 729 .setButtonIcons(new String[] {"cancel.png", "dialogs/delete.png"}) 730 .setContent(new JLabel(tr("<html><h3>Delete the file {0} from disk?<p>The image file will be permanently lost!</h3></html>" 731 ,toDelete.getFile().getName()), ImageProvider.get("dialogs/geoimage/deletefromdisk"),SwingConstants.LEFT)) 732 .toggleEnable("geoimage.deleteimagefromdisk") 733 .setCancelButton(1) 734 .setDefaultButton(2) 735 .showDialog() 736 .getValue(); 737 738 if(result == 2) 739 { 740 data.remove(currentPhoto); 741 if (currentPhoto >= data.size()) { 742 currentPhoto = data.size() - 1; 743 } 744 if (currentPhoto >= 0) { 745 ImageViewerDialog.showImage(this, data.get(currentPhoto)); 746 } else { 747 ImageViewerDialog.showImage(this, null); 748 } 749 750 if (toDelete.getFile().delete()) { 751 Main.info("File "+toDelete.getFile().toString()+" deleted. "); 752 } else { 753 JOptionPane.showMessageDialog( 754 Main.parent, 755 tr("Image file could not be deleted."), 756 tr("Error"), 757 JOptionPane.ERROR_MESSAGE 758 ); 759 } 760 761 updateOffscreenBuffer = true; 762 Main.map.repaint(); 763 } 764 } 765 } 766 767 /** 768 * Removes a photo from the list of images by index. 769 * @param idx Image index 770 * @since 6392 771 */ 772 public void removePhotoByIdx(int idx) { 773 if (idx >= 0 && data != null && idx < data.size()) { 774 data.remove(idx); 775 } 776 } 777 778 /** 779 * Returns the image that matches the position of the mouse event. 780 * @param evt Mouse event 781 * @return Image at mouse position, or {@code null} if there is no image at the mouse position 782 * @since 6392 783 */ 784 public ImageEntry getPhotoUnderMouse(MouseEvent evt) { 785 if (data != null) { 786 for (int idx = data.size() - 1; idx >= 0; --idx) { 787 ImageEntry img = data.get(idx); 788 if (img.getPos() == null) { 789 continue; 790 } 791 Point p = Main.map.mapView.getPoint(img.getPos()); 792 Rectangle r; 793 if (useThumbs && img.thumbnail != null) { 794 Dimension d = scaledDimension(img.thumbnail); 795 r = new Rectangle(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height); 796 } else { 797 r = new Rectangle(p.x - icon.getIconWidth() / 2, 798 p.y - icon.getIconHeight() / 2, 799 icon.getIconWidth(), 800 icon.getIconHeight()); 801 } 802 if (r.contains(evt.getPoint())) { 803 return img; 804 } 805 } 806 } 807 return null; 808 } 809 810 /** 811 * Clears the currentPhoto, i.e. remove select marker, and optionally repaint. 812 * @param repaint Repaint flag 813 * @since 6392 814 */ 815 public void clearCurrentPhoto(boolean repaint) { 816 currentPhoto = -1; 817 if (repaint) { 818 updateBufferAndRepaint(); 819 } 820 } 821 822 /** 823 * Clears the currentPhoto of the other GeoImageLayer's. Otherwise there could be multiple selected photos. 824 */ 825 private void clearOtherCurrentPhotos() { 826 for (GeoImageLayer layer: 827 Main.map.mapView.getLayersOfType(GeoImageLayer.class)) { 828 if (layer != this) { 829 layer.clearCurrentPhoto(false); 830 } 831 } 832 } 833 834 private static List<MapMode> supportedMapModes = null; 835 836 /** 837 * Registers a map mode for which the functionality of this layer should be available. 838 * @param mapMode Map mode to be registered 839 * @since 6392 840 */ 841 public static void registerSupportedMapMode(MapMode mapMode) { 842 if (supportedMapModes == null) { 843 supportedMapModes = new ArrayList<MapMode>(); 844 } 845 supportedMapModes.add(mapMode); 846 } 847 848 /** 849 * Determines if the functionality of this layer is available in 850 * the specified map mode. SelectAction is supported by default, 851 * other map modes can be registered. 852 * @param mapMode Map mode to be checked 853 * @return {@code true} if the map mode is supported, 854 * {@code false} otherwise 855 */ 856 private static final boolean isSupportedMapMode(MapMode mapMode) { 857 if (mapMode instanceof SelectAction) return true; 858 if (supportedMapModes != null) { 859 for (MapMode supmmode: supportedMapModes) { 860 if (mapMode == supmmode) { 861 return true; 862 } 863 } 864 } 865 return false; 866 } 867 868 private MouseAdapter mouseAdapter = null; 869 private MapModeChangeListener mapModeListener = null; 870 871 @Override 872 public void hookUpMapView() { 873 mouseAdapter = new MouseAdapter() { 874 private final boolean isMapModeOk() { 875 return Main.map.mapMode == null || isSupportedMapMode(Main.map.mapMode); 876 } 877 @Override public void mousePressed(MouseEvent e) { 878 879 if (e.getButton() != MouseEvent.BUTTON1) 880 return; 881 if (isVisible() && isMapModeOk()) { 882 Main.map.mapView.repaint(); 883 } 884 } 885 886 @Override public void mouseReleased(MouseEvent ev) { 887 if (ev.getButton() != MouseEvent.BUTTON1) 888 return; 889 if (data == null || !isVisible() || !isMapModeOk()) 890 return; 891 892 for (int i = data.size() - 1; i >= 0; --i) { 893 ImageEntry e = data.get(i); 894 if (e.getPos() == null) { 895 continue; 896 } 897 Point p = Main.map.mapView.getPoint(e.getPos()); 898 Rectangle r; 899 if (useThumbs && e.thumbnail != null) { 900 Dimension d = scaledDimension(e.thumbnail); 901 r = new Rectangle(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height); 902 } else { 903 r = new Rectangle(p.x - icon.getIconWidth() / 2, 904 p.y - icon.getIconHeight() / 2, 905 icon.getIconWidth(), 906 icon.getIconHeight()); 907 } 908 if (r.contains(ev.getPoint())) { 909 clearOtherCurrentPhotos(); 910 currentPhoto = i; 911 ImageViewerDialog.showImage(GeoImageLayer.this, e); 912 Main.map.repaint(); 913 break; 914 } 915 } 916 } 917 }; 918 919 mapModeListener = new MapModeChangeListener() { 920 @Override 921 public void mapModeChange(MapMode oldMapMode, MapMode newMapMode) { 922 if (newMapMode == null || isSupportedMapMode(newMapMode)) { 923 Main.map.mapView.addMouseListener(mouseAdapter); 924 } else { 925 Main.map.mapView.removeMouseListener(mouseAdapter); 926 } 927 } 928 }; 929 930 MapFrame.addMapModeChangeListener(mapModeListener); 931 mapModeListener.mapModeChange(null, Main.map.mapMode); 932 933 MapView.addLayerChangeListener(new LayerChangeListener() { 934 @Override 935 public void activeLayerChange(Layer oldLayer, Layer newLayer) { 936 if (newLayer == GeoImageLayer.this) { 937 // only in select mode it is possible to click the images 938 Main.map.selectSelectTool(false); 939 } 940 } 941 942 @Override 943 public void layerAdded(Layer newLayer) { 944 } 945 946 @Override 947 public void layerRemoved(Layer oldLayer) { 948 if (oldLayer == GeoImageLayer.this) { 949 if (thumbsloader != null) { 950 thumbsloader.stop = true; 951 } 952 Main.map.mapView.removeMouseListener(mouseAdapter); 953 MapFrame.removeMapModeChangeListener(mapModeListener); 954 currentPhoto = -1; 955 data.clear(); 956 data = null; 957 // stop listening to layer change events 958 MapView.removeLayerChangeListener(this); 959 } 960 } 961 }); 962 963 Main.map.mapView.addPropertyChangeListener(this); 964 if (Main.map.getToggleDialog(ImageViewerDialog.class) == null) { 965 ImageViewerDialog.newInstance(); 966 Main.map.addToggleDialog(ImageViewerDialog.getInstance()); 967 } 968 } 969 970 @Override 971 public void propertyChange(PropertyChangeEvent evt) { 972 if (NavigatableComponent.PROPNAME_CENTER.equals(evt.getPropertyName()) || NavigatableComponent.PROPNAME_SCALE.equals(evt.getPropertyName())) { 973 updateOffscreenBuffer = true; 974 } 975 } 976 977 public void loadThumbs() { 978 if (useThumbs && !thumbsLoaded) { 979 thumbsLoaded = true; 980 thumbsloader = new ThumbsLoader(this); 981 Thread t = new Thread(thumbsloader); 982 t.setPriority(Thread.MIN_PRIORITY); 983 t.start(); 984 } 985 } 986 987 public void updateBufferAndRepaint() { 988 updateOffscreenBuffer = true; 989 Main.map.mapView.repaint(); 990 } 991 992 public List<ImageEntry> getImages() { 993 List<ImageEntry> copy = new ArrayList<ImageEntry>(data.size()); 994 for (ImageEntry ie : data) { 995 copy.add(ie.clone()); 996 } 997 return copy; 998 } 999 1000 /** 1001 * Returns the associated GPX layer. 1002 * @return The associated GPX layer 1003 */ 1004 public GpxLayer getGpxLayer() { 1005 return gpxLayer; 1006 } 1007 1008 @Override 1009 public void jumpToNextMarker() { 1010 showNextPhoto(); 1011 } 1012 1013 @Override 1014 public void jumpToPreviousMarker() { 1015 showPreviousPhoto(); 1016 } 1017 1018 /** 1019 * Returns the current thumbnail display status. 1020 * {@code true}: thumbnails are displayed, {@code false}: an icon is displayed instead of thumbnails. 1021 * @return Current thumbnail display status 1022 * @since 6392 1023 */ 1024 public boolean isUseThumbs() { 1025 return useThumbs; 1026 } 1027 1028 /** 1029 * Enables or disables the display of thumbnails. Does not update the display. 1030 * @param useThumbs New thumbnail display status 1031 * @since 6392 1032 */ 1033 public void setUseThumbs(boolean useThumbs) { 1034 this.useThumbs = useThumbs; 1035 if (useThumbs && !thumbsLoaded) { 1036 loadThumbs(); 1037 } 1038 } 1039}