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

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

sonar - squid:S3052 - Fields should not be initialized to default values

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