source: josm/trunk/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java@ 7004

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

see #8465 - use multi-catch where applicable

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