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

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

sonar - squid:S2259 - Null pointers should not be dereferenced

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