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

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

fix #14179 - remove duplicated code

  • Property svn:eol-style set to native
File size: 36.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.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 // read the image files
195 List<ImageEntry> entries = new ArrayList<>(files.size());
196
197 for (File f : files) {
198
199 if (canceled) {
200 break;
201 }
202
203 progressMonitor.subTask(tr("Reading {0}...", f.getName()));
204 progressMonitor.worked(1);
205
206 ImageEntry e = new ImageEntry(f);
207 e.extractExif();
208 entries.add(e);
209 }
210 layer = new GeoImageLayer(entries, gpxLayer);
211 files.clear();
212 }
213
214 private void addRecursiveFiles(Collection<File> files, Collection<File> sel) {
215 boolean nullFile = false;
216
217 for (File f : sel) {
218
219 if (canceled) {
220 break;
221 }
222
223 if (f == null) {
224 nullFile = true;
225
226 } else if (f.isDirectory()) {
227 String canonical = null;
228 try {
229 canonical = f.getCanonicalPath();
230 } catch (IOException e) {
231 Main.error(e);
232 rememberError(tr("Unable to get canonical path for directory {0}\n",
233 f.getAbsolutePath()));
234 }
235
236 if (canonical == null || loadedDirectories.contains(canonical)) {
237 continue;
238 } else {
239 loadedDirectories.add(canonical);
240 }
241
242 File[] children = f.listFiles(JpgImporter.FILE_FILTER_WITH_FOLDERS);
243 if (children != null) {
244 progressMonitor.subTask(tr("Scanning directory {0}", f.getPath()));
245 addRecursiveFiles(files, Arrays.asList(children));
246 } else {
247 rememberError(tr("Error while getting files from directory {0}\n", f.getPath()));
248 }
249
250 } else {
251 files.add(f);
252 }
253 }
254
255 if (nullFile) {
256 throw new IllegalStateException(tr("One of the selected files was null"));
257 }
258 }
259
260 private String formatErrorMessages() {
261 StringBuilder sb = new StringBuilder();
262 sb.append("<html>");
263 if (errorMessages.size() == 1) {
264 sb.append(errorMessages.iterator().next());
265 } else {
266 sb.append(Utils.joinAsHtmlUnorderedList(errorMessages));
267 }
268 sb.append("</html>");
269 return sb.toString();
270 }
271
272 @Override protected void finish() {
273 if (!errorMessages.isEmpty()) {
274 JOptionPane.showMessageDialog(
275 Main.parent,
276 formatErrorMessages(),
277 tr("Error"),
278 JOptionPane.ERROR_MESSAGE
279 );
280 }
281 if (layer != null) {
282 Main.getLayerManager().addLayer(layer);
283
284 if (!canceled && layer.data != null && !layer.data.isEmpty()) {
285 boolean noGeotagFound = true;
286 for (ImageEntry e : layer.data) {
287 if (e.getPos() != null) {
288 noGeotagFound = false;
289 }
290 }
291 if (noGeotagFound) {
292 new CorrelateGpxWithImages(layer).actionPerformed(null);
293 }
294 }
295 }
296 }
297
298 @Override protected void cancel() {
299 canceled = true;
300 }
301 }
302
303 public static void create(Collection<File> files, GpxLayer gpxLayer) {
304 Main.worker.execute(new Loader(files, gpxLayer));
305 }
306
307 @Override
308 public Icon getIcon() {
309 return ImageProvider.get("dialogs/geoimage");
310 }
311
312 public static void registerMenuAddition(Action addition) {
313 menuAdditions.add(addition);
314 }
315
316 @Override
317 public Action[] getMenuEntries() {
318
319 List<Action> entries = new ArrayList<>();
320 entries.add(LayerListDialog.getInstance().createShowHideLayerAction());
321 entries.add(LayerListDialog.getInstance().createDeleteLayerAction());
322 entries.add(LayerListDialog.getInstance().createMergeLayerAction(this));
323 entries.add(new RenameLayerAction(null, this));
324 entries.add(SeparatorLayerAction.INSTANCE);
325 entries.add(new CorrelateGpxWithImages(this));
326 entries.add(new ShowThumbnailAction(this));
327 if (!menuAdditions.isEmpty()) {
328 entries.add(SeparatorLayerAction.INSTANCE);
329 entries.addAll(menuAdditions);
330 }
331 entries.add(SeparatorLayerAction.INSTANCE);
332 entries.add(new JumpToNextMarker(this));
333 entries.add(new JumpToPreviousMarker(this));
334 entries.add(SeparatorLayerAction.INSTANCE);
335 entries.add(new LayerListPopup.InfoAction(this));
336
337 return entries.toArray(new Action[entries.size()]);
338
339 }
340
341 /**
342 * Prepare the string that is displayed if layer information is requested.
343 * @return String with layer information
344 */
345 private String infoText() {
346 int tagged = 0;
347 int newdata = 0;
348 int n = 0;
349 if (data != null) {
350 n = data.size();
351 for (ImageEntry e : data) {
352 if (e.getPos() != null) {
353 tagged++;
354 }
355 if (e.hasNewGpsData()) {
356 newdata++;
357 }
358 }
359 }
360 return "<html>"
361 + trn("{0} image loaded.", "{0} images loaded.", n, n)
362 + ' ' + trn("{0} was found to be GPS tagged.", "{0} were found to be GPS tagged.", tagged, tagged)
363 + (newdata > 0 ? "<br>" + trn("{0} has updated GPS data.", "{0} have updated GPS data.", newdata, newdata) : "")
364 + "</html>";
365 }
366
367 @Override public Object getInfoComponent() {
368 return infoText();
369 }
370
371 @Override
372 public String getToolTipText() {
373 return infoText();
374 }
375
376 /**
377 * Determines if data managed by this layer has been modified. That is
378 * the case if one image has modified GPS data.
379 * @return {@code true} if data has been modified; {@code false}, otherwise
380 */
381 @Override
382 public boolean isModified() {
383 if (data != null) {
384 for (ImageEntry e : data) {
385 if (e.hasNewGpsData()) {
386 return true;
387 }
388 }
389 }
390 return false;
391 }
392
393 @Override
394 public boolean isMergable(Layer other) {
395 return other instanceof GeoImageLayer;
396 }
397
398 @Override
399 public void mergeFrom(Layer from) {
400 if (!(from instanceof GeoImageLayer))
401 throw new IllegalArgumentException("not a GeoImageLayer: " + from);
402 GeoImageLayer l = (GeoImageLayer) from;
403
404 // Stop to load thumbnails on both layers. Thumbnail loading will continue the next time
405 // the layer is painted.
406 stopLoadThumbs();
407 l.stopLoadThumbs();
408
409 final ImageEntry selected = l.data != null && l.currentPhoto >= 0 ? l.data.get(l.currentPhoto) : null;
410
411 if (l.data != null) {
412 data.addAll(l.data);
413 }
414 Collections.sort(data);
415
416 // Supress the double photos.
417 if (data.size() > 1) {
418 ImageEntry cur;
419 ImageEntry prev = data.get(data.size() - 1);
420 for (int i = data.size() - 2; i >= 0; i--) {
421 cur = data.get(i);
422 if (cur.getFile().equals(prev.getFile())) {
423 data.remove(i);
424 } else {
425 prev = cur;
426 }
427 }
428 }
429
430 if (selected != null && !data.isEmpty()) {
431 GuiHelper.runInEDTAndWait(() -> {
432 for (int i = 0; i < data.size(); i++) {
433 if (selected.equals(data.get(i))) {
434 currentPhoto = i;
435 ImageViewerDialog.showImage(this, data.get(i));
436 break;
437 }
438 }
439 });
440 }
441
442 setName(l.getName());
443 thumbsLoaded &= l.thumbsLoaded;
444 }
445
446 private static 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.hasThumbnail()) {
502 Dimension d = scaledDimension(e.getThumbnail());
503 if (d != null) {
504 Rectangle target = new Rectangle(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height);
505 if (clip.intersects(target)) {
506 tempG.drawImage(e.getThumbnail(), target.x, target.y, target.width, target.height, null);
507 }
508 }
509 } else { // thumbnail not loaded yet
510 icon.paintIcon(mv, tempG,
511 p.x - icon.getIconWidth() / 2,
512 p.y - icon.getIconHeight() / 2);
513 }
514 }
515 }
516 updateOffscreenBuffer = false;
517 }
518 g.drawImage(offscreenBuffer, 0, 0, null);
519 } else if (data != null) {
520 for (ImageEntry e : data) {
521 if (e.getPos() == null) {
522 continue;
523 }
524 Point p = mv.getPoint(e.getPos());
525 icon.paintIcon(mv, g,
526 p.x - icon.getIconWidth() / 2,
527 p.y - icon.getIconHeight() / 2);
528 }
529 }
530
531 if (currentPhoto >= 0 && currentPhoto < data.size()) {
532 ImageEntry e = data.get(currentPhoto);
533
534 if (e.getPos() != null) {
535 Point p = mv.getPoint(e.getPos());
536
537 int imgWidth;
538 int imgHeight;
539 if (useThumbs && e.hasThumbnail()) {
540 Dimension d = scaledDimension(e.getThumbnail());
541 if (d != null) {
542 imgWidth = d.width;
543 imgHeight = d.height;
544 } else {
545 imgWidth = -1;
546 imgHeight = -1;
547 }
548 } else {
549 imgWidth = selectedIcon.getIconWidth();
550 imgHeight = selectedIcon.getIconHeight();
551 }
552
553 if (e.getExifImgDir() != null) {
554 // Multiplier must be larger than sqrt(2)/2=0.71.
555 double arrowlength = Math.max(25, Math.max(imgWidth, imgHeight) * 0.85);
556 double arrowwidth = arrowlength / 1.4;
557
558 double dir = e.getExifImgDir();
559 // Rotate 90 degrees CCW
560 double headdir = (dir < 90) ? dir + 270 : dir - 90;
561 double leftdir = (headdir < 90) ? headdir + 270 : headdir - 90;
562 double rightdir = (headdir > 270) ? headdir - 270 : headdir + 90;
563
564 double ptx = p.x + Math.cos(Math.toRadians(headdir)) * arrowlength;
565 double pty = p.y + Math.sin(Math.toRadians(headdir)) * arrowlength;
566
567 double ltx = p.x + Math.cos(Math.toRadians(leftdir)) * arrowwidth/2;
568 double lty = p.y + Math.sin(Math.toRadians(leftdir)) * arrowwidth/2;
569
570 double rtx = p.x + Math.cos(Math.toRadians(rightdir)) * arrowwidth/2;
571 double rty = p.y + Math.sin(Math.toRadians(rightdir)) * arrowwidth/2;
572
573 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
574 g.setColor(new Color(255, 255, 255, 192));
575 int[] xar = {(int) ltx, (int) ptx, (int) rtx, (int) ltx};
576 int[] yar = {(int) lty, (int) pty, (int) rty, (int) lty};
577 g.fillPolygon(xar, yar, 4);
578 g.setColor(Color.black);
579 g.setStroke(new BasicStroke(1.2f));
580 g.drawPolyline(xar, yar, 3);
581 }
582
583 if (useThumbs && e.hasThumbnail()) {
584 g.setColor(new Color(128, 0, 0, 122));
585 g.fillRect(p.x - imgWidth / 2, p.y - imgHeight / 2, imgWidth, imgHeight);
586 } else {
587 selectedIcon.paintIcon(mv, g,
588 p.x - imgWidth / 2,
589 p.y - imgHeight / 2);
590
591 }
592 }
593 }
594 }
595
596 @Override
597 public void visitBoundingBox(BoundingXYVisitor v) {
598 for (ImageEntry e : data) {
599 v.visit(e.getPos());
600 }
601 }
602
603 /**
604 * Shows next photo.
605 */
606 public void showNextPhoto() {
607 if (data != null && !data.isEmpty()) {
608 currentPhoto++;
609 if (currentPhoto >= data.size()) {
610 currentPhoto = data.size() - 1;
611 }
612 ImageViewerDialog.showImage(this, data.get(currentPhoto));
613 } else {
614 currentPhoto = -1;
615 }
616 Main.map.repaint();
617 }
618
619 /**
620 * Shows previous photo.
621 */
622 public void showPreviousPhoto() {
623 if (data != null && !data.isEmpty()) {
624 currentPhoto--;
625 if (currentPhoto < 0) {
626 currentPhoto = 0;
627 }
628 ImageViewerDialog.showImage(this, data.get(currentPhoto));
629 } else {
630 currentPhoto = -1;
631 }
632 Main.map.repaint();
633 }
634
635 /**
636 * Shows first photo.
637 */
638 public void showFirstPhoto() {
639 if (data != null && !data.isEmpty()) {
640 currentPhoto = 0;
641 ImageViewerDialog.showImage(this, data.get(currentPhoto));
642 } else {
643 currentPhoto = -1;
644 }
645 Main.map.repaint();
646 }
647
648 /**
649 * Shows last photo.
650 */
651 public void showLastPhoto() {
652 if (data != null && !data.isEmpty()) {
653 currentPhoto = data.size() - 1;
654 ImageViewerDialog.showImage(this, data.get(currentPhoto));
655 } else {
656 currentPhoto = -1;
657 }
658 Main.map.repaint();
659 }
660
661 public void checkPreviousNextButtons() {
662 ImageViewerDialog.setNextEnabled(data != null && currentPhoto < data.size() - 1);
663 ImageViewerDialog.setPreviousEnabled(currentPhoto > 0);
664 }
665
666 public void removeCurrentPhoto() {
667 if (data != null && !data.isEmpty() && currentPhoto >= 0 && currentPhoto < data.size()) {
668 data.remove(currentPhoto);
669 if (currentPhoto >= data.size()) {
670 currentPhoto = data.size() - 1;
671 }
672 if (currentPhoto >= 0) {
673 ImageViewerDialog.showImage(this, data.get(currentPhoto));
674 } else {
675 ImageViewerDialog.showImage(this, null);
676 }
677 updateOffscreenBuffer = true;
678 Main.map.repaint();
679 }
680 }
681
682 public void removeCurrentPhotoFromDisk() {
683 ImageEntry toDelete;
684 if (data != null && !data.isEmpty() && currentPhoto >= 0 && currentPhoto < data.size()) {
685 toDelete = data.get(currentPhoto);
686
687 int result = new ExtendedDialog(
688 Main.parent,
689 tr("Delete image file from disk"),
690 new String[] {tr("Cancel"), tr("Delete")})
691 .setButtonIcons(new String[] {"cancel", "dialogs/delete"})
692 .setContent(new JLabel(tr("<html><h3>Delete the file {0} from disk?<p>The image file will be permanently lost!</h3></html>",
693 toDelete.getFile().getName()), ImageProvider.get("dialogs/geoimage/deletefromdisk"), SwingConstants.LEFT))
694 .toggleEnable("geoimage.deleteimagefromdisk")
695 .setCancelButton(1)
696 .setDefaultButton(2)
697 .showDialog()
698 .getValue();
699
700 if (result == 2) {
701 data.remove(currentPhoto);
702 if (currentPhoto >= data.size()) {
703 currentPhoto = data.size() - 1;
704 }
705 if (currentPhoto >= 0) {
706 ImageViewerDialog.showImage(this, data.get(currentPhoto));
707 } else {
708 ImageViewerDialog.showImage(this, null);
709 }
710
711 if (Utils.deleteFile(toDelete.getFile())) {
712 Main.info("File "+toDelete.getFile()+" deleted. ");
713 } else {
714 JOptionPane.showMessageDialog(
715 Main.parent,
716 tr("Image file could not be deleted."),
717 tr("Error"),
718 JOptionPane.ERROR_MESSAGE
719 );
720 }
721
722 updateOffscreenBuffer = true;
723 Main.map.repaint();
724 }
725 }
726 }
727
728 public void copyCurrentPhotoPath() {
729 if (data != null && !data.isEmpty() && currentPhoto >= 0 && currentPhoto < data.size()) {
730 ClipboardUtils.copyString(data.get(currentPhoto).getFile().toString());
731 }
732 }
733
734 /**
735 * Removes a photo from the list of images by index.
736 * @param idx Image index
737 * @since 6392
738 */
739 public void removePhotoByIdx(int idx) {
740 if (idx >= 0 && data != null && idx < data.size()) {
741 data.remove(idx);
742 }
743 }
744
745 /**
746 * Returns the image that matches the position of the mouse event.
747 * @param evt Mouse event
748 * @return Image at mouse position, or {@code null} if there is no image at the mouse position
749 * @since 6392
750 */
751 public ImageEntry getPhotoUnderMouse(MouseEvent evt) {
752 if (data != null) {
753 for (int idx = data.size() - 1; idx >= 0; --idx) {
754 ImageEntry img = data.get(idx);
755 if (img.getPos() == null) {
756 continue;
757 }
758 Point p = Main.map.mapView.getPoint(img.getPos());
759 Rectangle r;
760 if (useThumbs && img.hasThumbnail()) {
761 Dimension d = scaledDimension(img.getThumbnail());
762 if (d != null)
763 r = new Rectangle(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height);
764 else
765 r = null;
766 } else {
767 r = new Rectangle(p.x - icon.getIconWidth() / 2,
768 p.y - icon.getIconHeight() / 2,
769 icon.getIconWidth(),
770 icon.getIconHeight());
771 }
772 if (r != null && r.contains(evt.getPoint())) {
773 return img;
774 }
775 }
776 }
777 return null;
778 }
779
780 /**
781 * Clears the currentPhoto, i.e. remove select marker, and optionally repaint.
782 * @param repaint Repaint flag
783 * @since 6392
784 */
785 public void clearCurrentPhoto(boolean repaint) {
786 currentPhoto = -1;
787 if (repaint) {
788 updateBufferAndRepaint();
789 }
790 }
791
792 /**
793 * Clears the currentPhoto of the other GeoImageLayer's. Otherwise there could be multiple selected photos.
794 */
795 private void clearOtherCurrentPhotos() {
796 for (GeoImageLayer layer:
797 Main.getLayerManager().getLayersOfType(GeoImageLayer.class)) {
798 if (layer != this) {
799 layer.clearCurrentPhoto(false);
800 }
801 }
802 }
803
804 /**
805 * Registers a map mode for which the functionality of this layer should be available.
806 * @param mapMode Map mode to be registered
807 * @since 6392
808 */
809 public static void registerSupportedMapMode(MapMode mapMode) {
810 if (supportedMapModes == null) {
811 supportedMapModes = new ArrayList<>();
812 }
813 supportedMapModes.add(mapMode);
814 }
815
816 /**
817 * Determines if the functionality of this layer is available in
818 * the specified map mode. {@link SelectAction} and {@link LassoModeAction} are supported by default,
819 * other map modes can be registered.
820 * @param mapMode Map mode to be checked
821 * @return {@code true} if the map mode is supported,
822 * {@code false} otherwise
823 */
824 private static boolean isSupportedMapMode(MapMode mapMode) {
825 if (mapMode instanceof SelectAction || mapMode instanceof LassoModeAction) {
826 return true;
827 }
828 if (supportedMapModes != null) {
829 for (MapMode supmmode: supportedMapModes) {
830 if (mapMode == supmmode) {
831 return true;
832 }
833 }
834 }
835 return false;
836 }
837
838 @Override
839 public void hookUpMapView() {
840 mouseAdapter = new MouseAdapter() {
841 private boolean isMapModeOk() {
842 return Main.map.mapMode == null || isSupportedMapMode(Main.map.mapMode);
843 }
844
845 @Override
846 public void mousePressed(MouseEvent e) {
847 if (e.getButton() != MouseEvent.BUTTON1)
848 return;
849 if (isVisible() && isMapModeOk()) {
850 Main.map.mapView.repaint();
851 }
852 }
853
854 @Override
855 public void mouseReleased(MouseEvent ev) {
856 if (ev.getButton() != MouseEvent.BUTTON1)
857 return;
858 if (data == null || !isVisible() || !isMapModeOk())
859 return;
860
861 for (int i = data.size() - 1; i >= 0; --i) {
862 ImageEntry e = data.get(i);
863 if (e.getPos() == null) {
864 continue;
865 }
866 Point p = Main.map.mapView.getPoint(e.getPos());
867 Rectangle r;
868 if (useThumbs && e.hasThumbnail()) {
869 Dimension d = scaledDimension(e.getThumbnail());
870 if (d != null)
871 r = new Rectangle(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height);
872 else
873 r = null;
874 } else {
875 r = new Rectangle(p.x - icon.getIconWidth() / 2,
876 p.y - icon.getIconHeight() / 2,
877 icon.getIconWidth(),
878 icon.getIconHeight());
879 }
880 if (r != null && r.contains(ev.getPoint())) {
881 clearOtherCurrentPhotos();
882 currentPhoto = i;
883 ImageViewerDialog.showImage(GeoImageLayer.this, e);
884 Main.map.repaint();
885 break;
886 }
887 }
888 }
889 };
890
891 mapModeListener = (oldMapMode, newMapMode) -> {
892 if (newMapMode == null || isSupportedMapMode(newMapMode)) {
893 Main.map.mapView.addMouseListener(mouseAdapter);
894 } else {
895 Main.map.mapView.removeMouseListener(mouseAdapter);
896 }
897 };
898
899 MapFrame.addMapModeChangeListener(mapModeListener);
900 mapModeListener.mapModeChange(null, Main.map.mapMode);
901
902 Main.getLayerManager().addActiveLayerChangeListener(e -> {
903 if (Main.getLayerManager().getActiveLayer() == this) {
904 // only in select mode it is possible to click the images
905 Main.map.selectSelectTool(false);
906 }
907 });
908
909 Main.getLayerManager().addLayerChangeListener(new LayerChangeListener() {
910 @Override
911 public void layerAdded(LayerAddEvent e) {
912 // Do nothing
913 }
914
915 @Override
916 public void layerRemoving(LayerRemoveEvent e) {
917 if (e.getRemovedLayer() == GeoImageLayer.this) {
918 stopLoadThumbs();
919 Main.map.mapView.removeMouseListener(mouseAdapter);
920 MapFrame.removeMapModeChangeListener(mapModeListener);
921 currentPhoto = -1;
922 if (data != null) {
923 data.clear();
924 }
925 data = null;
926 // stop listening to layer change events
927 Main.getLayerManager().removeLayerChangeListener(this);
928 }
929 }
930
931 @Override
932 public void layerOrderChanged(LayerOrderChangeEvent e) {
933 // Do nothing
934 }
935 });
936
937 Main.map.mapView.addPropertyChangeListener(this);
938 if (Main.map.getToggleDialog(ImageViewerDialog.class) == null) {
939 ImageViewerDialog.newInstance();
940 Main.map.addToggleDialog(ImageViewerDialog.getInstance());
941 }
942 }
943
944 @Override
945 public void propertyChange(PropertyChangeEvent evt) {
946 if (NavigatableComponent.PROPNAME_CENTER.equals(evt.getPropertyName()) ||
947 NavigatableComponent.PROPNAME_SCALE.equals(evt.getPropertyName())) {
948 updateOffscreenBuffer = true;
949 }
950 }
951
952 /**
953 * Start to load thumbnails.
954 */
955 public synchronized void startLoadThumbs() {
956 if (useThumbs && !thumbsLoaded && !thumbsLoaderRunning) {
957 stopLoadThumbs();
958 thumbsloader = new ThumbsLoader(this);
959 thumbsLoaderExecutor.submit(thumbsloader);
960 thumbsLoaderRunning = true;
961 }
962 }
963
964 /**
965 * Stop to load thumbnails.
966 *
967 * Can be called at any time to make sure that the
968 * thumbnail loader is stopped.
969 */
970 public synchronized void stopLoadThumbs() {
971 if (thumbsloader != null) {
972 thumbsloader.stop = true;
973 }
974 thumbsLoaderRunning = false;
975 }
976
977 /**
978 * Called to signal that the loading of thumbnails has finished.
979 *
980 * Usually called from {@link ThumbsLoader} in another thread.
981 */
982 public void thumbsLoaded() {
983 thumbsLoaded = true;
984 }
985
986 public void updateBufferAndRepaint() {
987 updateOffscreenBuffer = true;
988 invalidate();
989 }
990
991 /**
992 * Get list of images in layer.
993 * @return List of images in layer
994 */
995 public List<ImageEntry> getImages() {
996 return data == null ? Collections.<ImageEntry>emptyList() : new ArrayList<>(data);
997 }
998
999 /**
1000 * Returns the associated GPX layer.
1001 * @return The associated GPX layer
1002 */
1003 public GpxLayer getGpxLayer() {
1004 return gpxLayer;
1005 }
1006
1007 @Override
1008 public void jumpToNextMarker() {
1009 showNextPhoto();
1010 }
1011
1012 @Override
1013 public void jumpToPreviousMarker() {
1014 showPreviousPhoto();
1015 }
1016
1017 /**
1018 * Returns the current thumbnail display status.
1019 * {@code true}: thumbnails are displayed, {@code false}: an icon is displayed instead of thumbnails.
1020 * @return Current thumbnail display status
1021 * @since 6392
1022 */
1023 public boolean isUseThumbs() {
1024 return useThumbs;
1025 }
1026
1027 /**
1028 * Enables or disables the display of thumbnails. Does not update the display.
1029 * @param useThumbs New thumbnail display status
1030 * @since 6392
1031 */
1032 public void setUseThumbs(boolean useThumbs) {
1033 this.useThumbs = useThumbs;
1034 if (useThumbs && !thumbsLoaded) {
1035 startLoadThumbs();
1036 } else if (!useThumbs) {
1037 stopLoadThumbs();
1038 }
1039 }
1040}
Note: See TracBrowser for help on using the repository browser.