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

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

fix #11756 - NPEs

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