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

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

fix #12249 - Fix Findbugs warnings "Exceptional return value of java.io.File.delete() ignored"

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