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

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

global use of Utils.joinAsHtmlUnorderedList()

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