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

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

fix sonar squid:S2039 - Member variable visibility should be specified

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