source: josm/trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java

Last change on this file was 19392, checked in by stoecker, 3 months ago

fix text

  • Property svn:eol-style set to native
File size: 53.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.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.BorderLayout;
9import java.awt.Component;
10import java.awt.Dimension;
11import java.awt.GridBagConstraints;
12import java.awt.GridBagLayout;
13import java.awt.event.ActionEvent;
14import java.awt.event.ActionListener;
15import java.awt.event.KeyEvent;
16import java.awt.event.WindowEvent;
17import java.beans.PropertyChangeEvent;
18import java.beans.PropertyChangeListener;
19import java.io.IOException;
20import java.io.Serializable;
21import java.time.ZoneOffset;
22import java.time.format.DateTimeFormatter;
23import java.time.format.FormatStyle;
24import java.util.ArrayList;
25import java.util.Arrays;
26import java.util.Collections;
27import java.util.Comparator;
28import java.util.List;
29import java.util.Objects;
30import java.util.Optional;
31import java.util.concurrent.Future;
32import java.util.function.UnaryOperator;
33import java.util.stream.Collectors;
34import java.util.stream.IntStream;
35import java.util.stream.Stream;
36
37import javax.swing.AbstractAction;
38import javax.swing.AbstractButton;
39import javax.swing.BorderFactory;
40import javax.swing.Box;
41import javax.swing.JButton;
42import javax.swing.JLabel;
43import javax.swing.JOptionPane;
44import javax.swing.JPanel;
45import javax.swing.JTabbedPane;
46import javax.swing.JToggleButton;
47import javax.swing.SwingConstants;
48import javax.swing.SwingUtilities;
49
50import org.openstreetmap.josm.actions.ExpertToggleAction;
51import org.openstreetmap.josm.actions.JosmAction;
52import org.openstreetmap.josm.data.ImageData;
53import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
54import org.openstreetmap.josm.gui.ExtendedDialog;
55import org.openstreetmap.josm.gui.MainApplication;
56import org.openstreetmap.josm.gui.MapFrame;
57import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
58import org.openstreetmap.josm.gui.dialogs.DialogsPanel;
59import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
60import org.openstreetmap.josm.gui.dialogs.layer.LayerVisibilityAction;
61import org.openstreetmap.josm.gui.layer.Layer;
62import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
63import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
64import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
65import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
66import org.openstreetmap.josm.gui.layer.MainLayerManager;
67import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
68import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
69import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
70import org.openstreetmap.josm.gui.util.GuiHelper;
71import org.openstreetmap.josm.gui.util.imagery.Vector3D;
72import org.openstreetmap.josm.gui.widgets.HideableTabbedPane;
73import org.openstreetmap.josm.spi.preferences.Config;
74import org.openstreetmap.josm.tools.ImageProvider;
75import org.openstreetmap.josm.tools.Logging;
76import org.openstreetmap.josm.tools.PlatformManager;
77import org.openstreetmap.josm.tools.Shortcut;
78import org.openstreetmap.josm.tools.date.DateUtils;
79
80/**
81 * Dialog to view and manipulate geo-tagged images from a {@link GeoImageLayer}.
82 */
83public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener {
84 private static final String GEOIMAGE_FILLER = marktr("Geoimage: {0}");
85 private static final String DIALOG_FOLDER = "dialogs";
86
87 private final ImageryFilterSettings imageryFilterSettings = new ImageryFilterSettings();
88
89 private final ImageZoomAction imageZoomAction = new ImageZoomAction();
90 private final ImageCenterViewAction imageCenterViewAction = new ImageCenterViewAction();
91 private final ImageExtendedInfoAction imageExtendedInfoAction = new ImageExtendedInfoAction();
92 private final ImageNextAction imageNextAction = new ImageNextAction();
93 private final ImageRemoveAction imageRemoveAction = new ImageRemoveAction();
94 private final ImageRemoveFromDiskAction imageRemoveFromDiskAction = new ImageRemoveFromDiskAction();
95 private final ImagePreviousAction imagePreviousAction = new ImagePreviousAction();
96 private final ImageCollapseAction imageCollapseAction = new ImageCollapseAction();
97 private final ImageFirstAction imageFirstAction = new ImageFirstAction();
98 private final ImageLastAction imageLastAction = new ImageLastAction();
99 private final ImageCopyPathAction imageCopyPathAction = new ImageCopyPathAction();
100 private final ImageOpenExternalAction imageOpenExternalAction = new ImageOpenExternalAction();
101 private final LayerVisibilityAction visibilityAction = new LayerVisibilityAction(Collections::emptyList,
102 () -> Collections.singleton(imageryFilterSettings));
103
104 private final ImageDisplay imgDisplay = new ImageDisplay(imageryFilterSettings);
105 private Future<?> imgLoadingFuture;
106 private boolean centerView;
107 private boolean extendedImgInfo;
108
109 // Only one instance of that class is present at one time
110 private static volatile ImageViewerDialog dialog;
111
112 private boolean collapseButtonClicked;
113
114 static void createInstance() {
115 if (dialog != null)
116 throw new IllegalStateException("ImageViewerDialog instance was already created");
117 dialog = new ImageViewerDialog();
118 }
119
120 /**
121 * Replies the unique instance of this dialog
122 * @return the unique instance
123 */
124 public static ImageViewerDialog getInstance() {
125 MapFrame map = MainApplication.getMap();
126 synchronized (ImageViewerDialog.class) {
127 if (dialog == null)
128 createInstance();
129 if (map != null && map.getToggleDialog(ImageViewerDialog.class) == null) {
130 map.addToggleDialog(dialog);
131 }
132 }
133 return dialog;
134 }
135
136 /**
137 * Check if there is an instance for the {@link ImageViewerDialog}
138 * @return {@code true} if there is a static singleton instance of {@link ImageViewerDialog}
139 * @since 18613
140 */
141 public static boolean hasInstance() {
142 return dialog != null;
143 }
144
145 /**
146 * Destroy the current dialog
147 */
148 private static void destroyInstance() {
149 MapFrame map = MainApplication.getMap();
150 synchronized (ImageViewerDialog.class) {
151 if (dialog != null && map != null && map.getToggleDialog(ImageViewerDialog.class) != null) {
152 map.removeToggleDialog(dialog);
153 }
154 }
155 dialog = null;
156 }
157
158 private JButton btnLast;
159 private JButton btnNext;
160 private JButton btnPrevious;
161 private JButton btnFirst;
162 private JButton btnCollapse;
163 private JButton btnDelete;
164 private JButton btnCopyPath;
165 private JButton btnOpenExternal;
166 private JButton btnDeleteFromDisk;
167 private JToggleButton tbCentre;
168 private JToggleButton tbImgExtInfo;
169 /** The layer tab (used to select images when multiple layers provide images, makes for easy switching) */
170 private final HideableTabbedPane layers = new HideableTabbedPane();
171
172 private ImageViewerDialog() {
173 super(tr("Geotagged Images"), "geoimage", tr("Display geotagged images"), Shortcut.registerShortcut("tools:geotagged",
174 tr("Windows: {0}", tr("Geotagged Images")), KeyEvent.VK_Y, Shortcut.DIRECT), 200);
175 build();
176 MainApplication.getLayerManager().addActiveLayerChangeListener(this);
177 MainApplication.getLayerManager().addLayerChangeListener(this);
178 for (Layer l: MainApplication.getLayerManager().getLayers()) {
179 registerOnLayer(l);
180 }
181 // This listener gets called _prior to_ the reorder event. If we do not delay the execution of the
182 // model update, then the image will change instead of remaining the same.
183 this.layers.getModel().addChangeListener(l -> {
184 // We need to check to see whether or not the worker is shut down. See #22922 for details.
185 if (!MainApplication.worker.isShutdown() && this.isDialogShowing()) {
186 MainApplication.worker.execute(() -> GuiHelper.runInEDT(this::showNotify));
187 }
188 });
189 }
190
191 @Override
192 public void showNotify() {
193 super.showNotify();
194 Component selected = this.layers.getSelectedComponent();
195 if (selected instanceof MoveImgDisplayPanel) {
196 ((MoveImgDisplayPanel<?>) selected).fireModelUpdate();
197 }
198 }
199
200 private static JButton createButton(AbstractAction action, Dimension buttonDim) {
201 JButton btn = new JButton(action);
202 btn.setPreferredSize(buttonDim);
203 btn.addPropertyChangeListener("enabled", e -> action.setEnabled(Boolean.TRUE.equals(e.getNewValue())));
204 return btn;
205 }
206
207 private static JButton createNavigationButton(AbstractAction action, Dimension buttonDim) {
208 JButton btn = createButton(action, buttonDim);
209 btn.setEnabled(false);
210 action.addPropertyChangeListener(l -> {
211 if ("enabled".equals(l.getPropertyName())) {
212 btn.setEnabled(action.isEnabled());
213 }
214 });
215 return btn;
216 }
217
218 private void build() {
219 JPanel content = new JPanel(new BorderLayout());
220 content.add(this.layers, BorderLayout.CENTER);
221
222 Dimension buttonDim = new Dimension(26, 26);
223
224 btnFirst = createNavigationButton(imageFirstAction, buttonDim);
225 btnPrevious = createNavigationButton(imagePreviousAction, buttonDim);
226
227 btnDelete = createButton(imageRemoveAction, buttonDim);
228 btnDeleteFromDisk = createButton(imageRemoveFromDiskAction, buttonDim);
229 btnCopyPath = createButton(imageCopyPathAction, buttonDim);
230 btnOpenExternal = createButton(imageOpenExternalAction, buttonDim);
231
232 btnNext = createNavigationButton(imageNextAction, buttonDim);
233 btnLast = createNavigationButton(imageLastAction, buttonDim);
234
235 centerView = Config.getPref().getBoolean("geoimage.viewer.centre.on.image", false);
236 tbCentre = new JToggleButton(imageCenterViewAction);
237 tbCentre.setSelected(centerView);
238 tbCentre.setPreferredSize(buttonDim);
239
240 extendedImgInfo = Config.getPref().getBoolean("geoimage.viewer.extendedinfo", false);
241 tbImgExtInfo = new JToggleButton(imageExtendedInfoAction);
242 tbImgExtInfo.setSelected(extendedImgInfo);
243 tbImgExtInfo.setPreferredSize(buttonDim);
244
245 JButton btnZoomBestFit = new JButton(imageZoomAction);
246 btnZoomBestFit.setPreferredSize(buttonDim);
247
248 btnCollapse = createButton(imageCollapseAction, new Dimension(20, 20));
249 btnCollapse.setAlignmentY(Component.TOP_ALIGNMENT);
250
251 JPanel buttons = new JPanel();
252 addButtonGroup(buttons, this.btnFirst, this.btnPrevious, this.btnNext, this.btnLast);
253 addButtonGroup(buttons, this.tbCentre, btnZoomBestFit, this.tbImgExtInfo);
254 addButtonGroup(buttons, this.btnDelete, this.btnDeleteFromDisk);
255 addButtonGroup(buttons, this.btnCopyPath, this.btnOpenExternal);
256 addButtonGroup(buttons, createButton(visibilityAction, buttonDim));
257
258 JPanel bottomPane = new JPanel(new GridBagLayout());
259 GridBagConstraints gc = new GridBagConstraints();
260 gc.gridx = 0;
261 gc.gridy = 0;
262 gc.anchor = GridBagConstraints.CENTER;
263 gc.weightx = 1;
264 bottomPane.add(buttons, gc);
265
266 gc.gridx = 1;
267 gc.gridy = 0;
268 gc.anchor = GridBagConstraints.PAGE_END;
269 gc.weightx = 0;
270 bottomPane.add(btnCollapse, gc);
271
272 content.add(bottomPane, BorderLayout.SOUTH);
273
274 createLayout(content, false, null);
275 }
276
277 /**
278 * Add a button group to a panel
279 * @param buttonPanel The panel holding the buttons
280 * @param buttons The button group to add
281 */
282 private static void addButtonGroup(JPanel buttonPanel, AbstractButton... buttons) {
283 if (buttonPanel.getComponentCount() != 0) {
284 buttonPanel.add(Box.createRigidArea(new Dimension(7, 0)));
285 }
286
287 for (AbstractButton jButton : buttons) {
288 buttonPanel.add(jButton);
289 }
290 }
291
292 /**
293 * Update the tabs for the different image layers
294 * @param changed {@code true} if the tabs changed
295 */
296 private void updateLayers(boolean changed) {
297 MainLayerManager layerManager = MainApplication.getLayerManager();
298 List<IGeoImageLayer> geoImageLayers = layerManager.getLayers().stream()
299 .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
300 if (geoImageLayers.isEmpty()) {
301 this.layers.setVisible(false);
302 } else {
303 this.layers.setVisible(true);
304 if (changed) {
305 addButtonsForImageLayers();
306 }
307 MoveImgDisplayPanel<?> selected = (MoveImgDisplayPanel<?>) this.layers.getSelectedComponent();
308 if ((this.imgDisplay.getParent() == null || this.imgDisplay.getParent().getParent() == null)
309 && selected != null && selected.layer.containsImage(this.currentEntry)) {
310 selected.setVisible(selected.isVisible());
311 } else if (selected != null && !selected.layer.containsImage(this.currentEntry)) {
312 this.getImageTabs().filter(m -> m.layer.containsImage(this.currentEntry)).mapToInt(this.layers::indexOfComponent).findFirst()
313 .ifPresent(this.layers::setSelectedIndex);
314 } else if (selected == null) {
315 updateTitle();
316 }
317 this.layers.invalidate();
318 }
319 this.layers.getParent().invalidate();
320 this.revalidate();
321 }
322
323 /**
324 * Add the buttons for image layers
325 */
326 private void addButtonsForImageLayers() {
327 List<MoveImgDisplayPanel<?>> alreadyAdded = this.getImageTabs().collect(Collectors.toList());
328 List<Layer> availableLayers = MainApplication.getLayerManager().getLayers();
329 List<IGeoImageLayer> geoImageLayers = availableLayers.stream()
330 .sorted(Comparator.comparingInt(entry -> /*reverse*/-availableLayers.indexOf(entry)))
331 .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
332 List<IGeoImageLayer> tabLayers = geoImageLayers.stream()
333 .filter(l -> alreadyAdded.stream().anyMatch(m -> Objects.equals(l, m.layer)) || l.containsImage(this.currentEntry))
334 .collect(Collectors.toList());
335 for (IGeoImageLayer layer : tabLayers) {
336 final MoveImgDisplayPanel<?> panel = alreadyAdded.stream()
337 .filter(m -> Objects.equals(m.layer, layer)).findFirst()
338 .orElseGet(() -> new MoveImgDisplayPanel<>(this.imgDisplay, (Layer & IGeoImageLayer) layer));
339 int componentIndex = this.layers.indexOfComponent(panel);
340 if (componentIndex == geoImageLayers.indexOf(layer)) {
341 this.layers.setTitleAt(componentIndex, panel.getLabel(availableLayers));
342 } else {
343 this.removeImageTab((Layer) layer);
344 this.layers.insertTab(panel.getLabel(availableLayers), null, panel, null, tabLayers.indexOf(layer));
345 int idx = this.layers.indexOfComponent(panel);
346 CloseableTab closeableTab = new CloseableTab(this.layers, l -> {
347 Component source = (Component) l.getSource();
348 do {
349 int index = layers.indexOfTabComponent(source);
350 if (index >= 0) {
351 removeImageTab(((MoveImgDisplayPanel<?>) layers.getComponentAt(index)).layer);
352 getImageTabs().forEach(m -> m.setVisible(m.isVisible()));
353 showNotify();
354 return;
355 }
356 source = source.getParent();
357 } while (source != null);
358 });
359 this.layers.setTabComponentAt(idx, closeableTab);
360 }
361 if (layer.containsImage(this.currentEntry)) {
362 this.layers.setSelectedComponent(panel);
363 }
364 }
365 this.getImageTabs().map(p -> p.layer).filter(layer -> !availableLayers.contains(layer))
366 // We have to collect to a list prior to removal -- if we don't, then the stream may get a layer at index 0,
367 // remove that layer, and then get a layer at index 1, which was previously at index 2.
368 .collect(Collectors.toList()).forEach(this::removeImageTab);
369
370 // After that, trigger the visibility set code
371 this.getImageTabs().forEach(m -> m.setVisible(m.isVisible()));
372 }
373
374 /**
375 * Remove a tab for a layer from the {@link #layers} tab pane
376 * @param layer The layer to remove
377 */
378 private void removeImageTab(Layer layer) {
379 // This must be reversed to avoid removing the wrong tab
380 for (int i = this.layers.getTabCount() - 1; i >= 0; i--) {
381 Component component = this.layers.getComponentAt(i);
382 if (component instanceof MoveImgDisplayPanel) {
383 MoveImgDisplayPanel<?> moveImgDisplayPanel = (MoveImgDisplayPanel<?>) component;
384 if (Objects.equals(layer, moveImgDisplayPanel.layer)) {
385 this.layers.removeTabAt(i);
386 this.layers.remove(moveImgDisplayPanel);
387 }
388 }
389 }
390 }
391
392 /**
393 * Get the {@link MoveImgDisplayPanel} objects in {@link #layers}.
394 * @return The individual panels
395 */
396 private Stream<MoveImgDisplayPanel<?>> getImageTabs() {
397 return IntStream.range(0, this.layers.getTabCount())
398 .mapToObj(this.layers::getComponentAt)
399 .filter(MoveImgDisplayPanel.class::isInstance)
400 .map(m -> (MoveImgDisplayPanel<?>) m);
401 }
402
403 @Override
404 public void destroy() {
405 MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
406 MainApplication.getLayerManager().removeLayerChangeListener(this);
407 // Manually destroy actions until JButtons are replaced by standard SideButtons
408 imageFirstAction.destroy();
409 imageLastAction.destroy();
410 imagePreviousAction.destroy();
411 imageNextAction.destroy();
412 imageCenterViewAction.destroy();
413 imageCollapseAction.destroy();
414 imageCopyPathAction.destroy();
415 imageOpenExternalAction.destroy();
416 imageRemoveAction.destroy();
417 imageRemoveFromDiskAction.destroy();
418 imageZoomAction.destroy();
419 toggleAction.destroy();
420 cancelLoadingImage();
421 super.destroy();
422 // make sure that Image Display is destroyed here, it might not be a component
423 imgDisplay.destroy();
424 // Ensure that this dialog is removed from memory
425 destroyInstance();
426 }
427
428 /**
429 * This literally exists to silence sonarlint complaints.
430 * @param <I> the type of the operand and result of the operator
431 */
432 @FunctionalInterface
433 private interface SerializableUnaryOperator<I> extends UnaryOperator<I>, Serializable {
434 }
435
436 private abstract class ImageAction extends JosmAction {
437 final SerializableUnaryOperator<IImageEntry<?>> supplier;
438 ImageAction(String name, ImageProvider icon, String tooltip, Shortcut shortcut,
439 boolean registerInToolbar, String toolbarId, boolean installAdaptors,
440 final SerializableUnaryOperator<IImageEntry<?>> supplier) {
441 super(name, icon, tooltip, shortcut, registerInToolbar, toolbarId, installAdaptors);
442 Objects.requireNonNull(supplier);
443 this.supplier = supplier;
444 }
445
446 @Override
447 public void actionPerformed(ActionEvent event) {
448 final IImageEntry<?> entry = ImageViewerDialog.this.currentEntry;
449 if (entry != null) {
450 IImageEntry<?> nextEntry = this.getSupplier().apply(entry);
451 entry.selectImage(ImageViewerDialog.this, nextEntry);
452 }
453 this.resetRememberActions();
454 }
455
456 void resetRememberActions() {
457 for (ImageRememberAction action : Arrays.asList(ImageViewerDialog.this.imageLastAction, ImageViewerDialog.this.imageFirstAction)) {
458 action.last = null;
459 action.updateEnabledState();
460 }
461 }
462
463 SerializableUnaryOperator<IImageEntry<?>> getSupplier() {
464 return this.supplier;
465 }
466
467 @Override
468 protected void updateEnabledState() {
469 final IImageEntry<?> entry = ImageViewerDialog.this.currentEntry;
470 this.setEnabled(entry != null && this.getSupplier().apply(entry) != null);
471 }
472 }
473
474 private class ImageNextAction extends ImageAction {
475 ImageNextAction() {
476 super(null, new ImageProvider(DIALOG_FOLDER, "next"), tr("Next"), Shortcut.registerShortcut(
477 "geoimage:next", tr(GEOIMAGE_FILLER, tr("Show next Image")), KeyEvent.VK_PAGE_DOWN, Shortcut.DIRECT),
478 false, null, false, IImageEntry::getNextImage);
479 }
480 }
481
482 private class ImagePreviousAction extends ImageAction {
483 ImagePreviousAction() {
484 super(null, new ImageProvider(DIALOG_FOLDER, "previous"), tr("Previous"), Shortcut.registerShortcut(
485 "geoimage:previous", tr(GEOIMAGE_FILLER, tr("Show previous Image")), KeyEvent.VK_PAGE_UP, Shortcut.DIRECT),
486 false, null, false, IImageEntry::getPreviousImage);
487 }
488 }
489
490 /** This class exists to remember the last entry, and go back if clicked again when it would not otherwise be enabled */
491 private abstract class ImageRememberAction extends ImageAction {
492 private final ImageProvider defaultIcon;
493 transient IImageEntry<?> last;
494 ImageRememberAction(String name, ImageProvider icon, String tooltip, Shortcut shortcut,
495 boolean registerInToolbar, String toolbarId, boolean installAdaptors, SerializableUnaryOperator<IImageEntry<?>> supplier) {
496 super(name, icon, tooltip, shortcut, registerInToolbar, toolbarId, installAdaptors, supplier);
497 this.defaultIcon = icon;
498 }
499
500 /**
501 * Update the icon for this action
502 */
503 public void updateIcon() {
504 if (this.last != null) {
505 new ImageProvider(DIALOG_FOLDER, "history").getResource().attachImageIcon(this, true);
506 } else {
507 this.defaultIcon.getResource().attachImageIcon(this, true);
508 }
509 }
510
511 @Override
512 public void actionPerformed(ActionEvent event) {
513 final IImageEntry<?> current = ImageViewerDialog.this.currentEntry;
514 final IImageEntry<?> expected = this.supplier.apply(current);
515 if (current != null) {
516 IImageEntry<?> nextEntry = this.getSupplier().apply(current);
517 current.selectImage(ImageViewerDialog.this, nextEntry);
518 }
519 this.resetRememberActions();
520 if (!Objects.equals(current, expected)) {
521 this.last = current;
522 } else {
523 this.last = null;
524 }
525 this.updateEnabledState();
526 }
527
528 @Override
529 protected void updateEnabledState() {
530 final IImageEntry<?> current = ImageViewerDialog.this.currentEntry;
531 final IImageEntry<?> nextEntry = current != null ? this.getSupplier().apply(current) : null;
532 if (this.last == null && nextEntry != null && nextEntry.equals(current)) {
533 this.setEnabled(false);
534 } else {
535 super.updateEnabledState();
536 }
537 this.updateIcon();
538 }
539
540 @Override
541 SerializableUnaryOperator<IImageEntry<?>> getSupplier() {
542 if (this.last != null) {
543 return entry -> this.last;
544 }
545 return super.getSupplier();
546 }
547 }
548
549 private class ImageFirstAction extends ImageRememberAction {
550 ImageFirstAction() {
551 super(null, new ImageProvider(DIALOG_FOLDER, "first"), tr("First"), Shortcut.registerShortcut(
552 "geoimage:first", tr(GEOIMAGE_FILLER, tr("Show first Image")), KeyEvent.VK_HOME, Shortcut.DIRECT),
553 false, null, false, IImageEntry::getFirstImage);
554 }
555 }
556
557 private class ImageLastAction extends ImageRememberAction {
558 ImageLastAction() {
559 super(null, new ImageProvider(DIALOG_FOLDER, "last"), tr("Last"), Shortcut.registerShortcut(
560 "geoimage:last", tr(GEOIMAGE_FILLER, tr("Show last Image")), KeyEvent.VK_END, Shortcut.DIRECT),
561 false, null, false, IImageEntry::getLastImage);
562 }
563 }
564
565 private class ImageCenterViewAction extends JosmAction {
566 ImageCenterViewAction() {
567 super(null, new ImageProvider("dialogs/autoscale", "selection"), tr("Center view"), null,
568 false, null, false);
569 }
570
571 @Override
572 public void actionPerformed(ActionEvent e) {
573 final JToggleButton button = (JToggleButton) e.getSource();
574 centerView = button.isEnabled() && button.isSelected();
575 Config.getPref().putBoolean("geoimage.viewer.centre.on.image", centerView);
576 if (centerView && currentEntry != null && currentEntry.getPos() != null) {
577 MainApplication.getMap().mapView.zoomTo(currentEntry.getPos());
578 }
579 }
580 }
581
582 private class ImageZoomAction extends JosmAction {
583 ImageZoomAction() {
584 super(null, new ImageProvider(DIALOG_FOLDER, "zoom-best-fit"), tr("Zoom best fit and 1:1"), null,
585 false, null, false);
586 }
587
588 @Override
589 public void actionPerformed(ActionEvent e) {
590 imgDisplay.zoomBestFitOrOne();
591 }
592 }
593
594 private class ImageExtendedInfoAction extends JosmAction {
595 ImageExtendedInfoAction() {
596 super(null, new ImageProvider("info"), tr("Display image extended metadata"), Shortcut.registerShortcut(
597 "geoimage:extendedinfos", tr(GEOIMAGE_FILLER, tr("Toggle display of extended information")),
598 KeyEvent.CHAR_UNDEFINED, Shortcut.NONE),
599 false, null, false);
600 }
601
602 @Override
603 public void actionPerformed(ActionEvent e) {
604 final JToggleButton button = (JToggleButton) e.getSource();
605 extendedImgInfo = button.isEnabled() && button.isSelected();
606 Config.getPref().putBoolean("geoimage.viewer.extendedinfo", extendedImgInfo);
607 refresh(false);
608 }
609 }
610
611 private class ImageRemoveAction extends JosmAction {
612 ImageRemoveAction() {
613 super(null, new ImageProvider(DIALOG_FOLDER, "delete"), tr("Remove photo from layer"), Shortcut.registerShortcut(
614 "geoimage:deleteimagefromlayer", tr(GEOIMAGE_FILLER, tr("Remove photo from layer")), KeyEvent.VK_DELETE, Shortcut.SHIFT),
615 false, null, false);
616 }
617
618 @Override
619 public void actionPerformed(ActionEvent e) {
620 if (ImageViewerDialog.this.currentEntry != null) {
621 IImageEntry<?> imageEntry = ImageViewerDialog.this.currentEntry;
622 if (imageEntry.isRemoveSupported()) {
623 imageEntry.remove();
624 }
625 selectNextImageAfterDeletion(imageEntry);
626 }
627 }
628
629 /**
630 * Select the logical next entry after deleting the currently viewed image
631 * @param oldEntry The image entry that was just deleted
632 */
633 private void selectNextImageAfterDeletion(IImageEntry<?> oldEntry) {
634 final IImageEntry<?> currentImageEntry = ImageViewerDialog.this.currentEntry;
635 // This is mostly just in case something changes the displayed entry (aka avoid race condition) or an image provider
636 // sets the next image itself.
637 if (Objects.equals(currentImageEntry, oldEntry)) {
638 final IImageEntry<?> nextImage;
639 if (oldEntry instanceof ImageEntry) {
640 nextImage = ((ImageEntry) oldEntry).getDataSet().getSelectedImage();
641 } else if (oldEntry.getNextImage() != null) {
642 nextImage = oldEntry.getNextImage();
643 } else if (oldEntry.getPreviousImage() != null) {
644 nextImage = oldEntry.getPreviousImage();
645 } else {
646 nextImage = null;
647 }
648 ImageViewerDialog.getInstance().displayImages(nextImage == null ? null : Collections.singletonList(nextImage));
649 }
650 }
651 }
652
653 private class ImageRemoveFromDiskAction extends JosmAction {
654 ImageRemoveFromDiskAction() {
655 super(null, new ImageProvider(DIALOG_FOLDER, "geoimage/deletefromdisk"), tr("Delete image file from disk"),
656 Shortcut.registerShortcut("geoimage:deletefilefromdisk",
657 tr(GEOIMAGE_FILLER, tr("Delete image file from disk")), KeyEvent.VK_DELETE, Shortcut.CTRL_SHIFT),
658 false, null, false);
659 }
660
661 @Override
662 public void actionPerformed(ActionEvent e) {
663 if (currentEntry != null) {
664 IImageEntry<?> oldEntry = ImageViewerDialog.this.currentEntry;
665 List<IImageEntry<?>> toDelete = oldEntry instanceof ImageEntry ?
666 new ArrayList<>(((ImageEntry) oldEntry).getDataSet().getSelectedImages())
667 : Collections.singletonList(oldEntry);
668 int size = toDelete.size();
669
670 int result = new ExtendedDialog(
671 MainApplication.getMainFrame(),
672 tr("Delete image file from disk"),
673 tr("Cancel"), tr("Delete"))
674 .setButtonIcons("cancel", "dialogs/geoimage/deletefromdisk")
675 .setContent(new JLabel("<html><h3>"
676 + trn("Delete the file from disk?",
677 "Delete the {0} files from disk?", size, size)
678 + "<p>" + trn("The image file will be permanently lost!",
679 "The images files will be permanently lost!", size) + "</h3></html>",
680 ImageProvider.get("dialogs/geoimage/deletefromdisk"), SwingConstants.LEADING))
681 .toggleEnable("geoimage.deleteimagefromdisk")
682 .setCancelButton(1)
683 .setDefaultButton(2)
684 .showDialog()
685 .getValue();
686
687 if (result == 2) {
688 final List<ImageData> imageDataCollection = toDelete.stream().filter(ImageEntry.class::isInstance)
689 .map(ImageEntry.class::cast).map(ImageEntry::getDataSet).distinct().collect(Collectors.toList());
690 for (IImageEntry<?> delete : toDelete) {
691 // We have to be able to remove the image from the layer and the image from its storage location
692 // If either are false, then don't remove the image.
693 if (delete.isRemoveSupported() && delete.isDeleteSupported() && delete.remove() && delete.delete()) {
694 Logging.info("File {0} deleted.", delete.getFile());
695 } else {
696 JOptionPane.showMessageDialog(
697 MainApplication.getMainFrame(),
698 tr("Image file could not be deleted."),
699 tr("Error"),
700 JOptionPane.ERROR_MESSAGE
701 );
702 }
703 }
704 imageDataCollection.forEach(data -> {
705 data.notifyImageUpdate();
706 data.updateSelectedImage();
707 });
708 ImageViewerDialog.this.imageRemoveAction.selectNextImageAfterDeletion(oldEntry);
709 }
710 }
711 }
712 }
713
714 private class ImageCopyPathAction extends JosmAction {
715 ImageCopyPathAction() {
716 super(null, new ImageProvider("copy"), tr("Copy image path"), Shortcut.registerShortcut(
717 "geoimage:copypath", tr(GEOIMAGE_FILLER, tr("Copy image path")), KeyEvent.VK_C, Shortcut.ALT_CTRL_SHIFT),
718 false, null, false);
719 }
720
721 @Override
722 public void actionPerformed(ActionEvent e) {
723 if (currentEntry != null) {
724 ClipboardUtils.copyString(String.valueOf(currentEntry.getImageURI()));
725 }
726 }
727 }
728
729 private class ImageCollapseAction extends JosmAction {
730 ImageCollapseAction() {
731 super(null, new ImageProvider(DIALOG_FOLDER, "collapse"), tr("Move dialog to the side pane"), null,
732 false, null, false);
733 }
734
735 @Override
736 public void actionPerformed(ActionEvent e) {
737 collapseButtonClicked = true;
738 detachedDialog.getToolkit().getSystemEventQueue().postEvent(new WindowEvent(detachedDialog, WindowEvent.WINDOW_CLOSING));
739 }
740 }
741
742 private class ImageOpenExternalAction extends JosmAction {
743 ImageOpenExternalAction() {
744 super(null, new ImageProvider("external-link"), tr("Open image in external viewer"), null, false, null, false);
745 }
746
747 @Override
748 public void actionPerformed(ActionEvent e) {
749 if (currentEntry != null && currentEntry.getImageURI() != null) {
750 try {
751 PlatformManager.getPlatform().openUrl(currentEntry.getImageURI().toURL().toExternalForm());
752 } catch (IOException ex) {
753 Logging.error(ex);
754 }
755 }
756 }
757 }
758
759 /**
760 * A tab title renderer for {@link HideableTabbedPane} that allows us to close tabs.
761 */
762 private static class CloseableTab extends JPanel implements PropertyChangeListener {
763 private final JLabel title;
764 private final JButton close;
765
766 /**
767 * Create a new {@link CloseableTab}.
768 * @param parent The parent to add property change listeners to. It should be a {@link HideableTabbedPane} in most cases.
769 * @param closeAction The action to run to close the tab. You probably want to call {@link JTabbedPane#removeTabAt(int)}
770 * at the very least.
771 */
772 CloseableTab(Component parent, ActionListener closeAction) {
773 this.title = new JLabel();
774 this.add(this.title);
775 close = new JButton(ImageProvider.get("misc", "close"));
776 close.setBorder(BorderFactory.createEmptyBorder());
777 this.add(close);
778 close.addActionListener(closeAction);
779 close.addActionListener(l -> parent.removePropertyChangeListener("indexForTitle", this));
780 parent.addPropertyChangeListener("indexForTitle", this);
781 }
782
783 @Override
784 public void propertyChange(PropertyChangeEvent evt) {
785 if (evt.getSource() instanceof JTabbedPane) {
786 JTabbedPane source = (JTabbedPane) evt.getSource();
787 if (this.getParent() == null) {
788 source.removePropertyChangeListener(evt.getPropertyName(), this);
789 }
790 if ("indexForTitle".equals(evt.getPropertyName())) {
791 int idx = source.indexOfTabComponent(this);
792 if (idx >= 0) {
793 this.title.setText(source.getTitleAt(idx));
794 }
795 }
796 // Used to hack around UI staying visible. This assumes that the parent component is a HideableTabbedPane.
797 this.title.setVisible(source.getTabCount() != 1);
798 this.close.setVisible(source.getTabCount() != 1);
799 }
800 }
801 }
802
803 /**
804 * A JPanel whose entire purpose is to display an image by (a) moving the imgDisplay around and (b) setting the imgDisplay as a child
805 * for this panel.
806 */
807 private static class MoveImgDisplayPanel<T extends Layer & IGeoImageLayer> extends JPanel {
808 private final T layer;
809 private final ImageDisplay imgDisplay;
810
811 MoveImgDisplayPanel(ImageDisplay imgDisplay, T layer) {
812 super(new BorderLayout());
813 this.layer = layer;
814 this.imgDisplay = imgDisplay;
815 }
816
817 /**
818 * Call when the selection model updates
819 */
820 void fireModelUpdate() {
821 JTabbedPane layers = ImageViewerDialog.getInstance().layers;
822 int index = layers.indexOfComponent(this);
823 if (this == layers.getSelectedComponent()) {
824 if (!this.layer.getSelection().isEmpty() && !this.layer.getSelection().contains(ImageViewerDialog.getCurrentImage())) {
825 ImageViewerDialog.getInstance().displayImages(this.layer.getSelection());
826 this.layer.invalidate(); // This will force the geoimage layers to update properly.
827 }
828 if (this.imgDisplay.getParent() != this) {
829 this.add(this.imgDisplay, BorderLayout.CENTER);
830 this.imgDisplay.invalidate();
831 this.revalidate();
832 }
833 if (index >= 0) {
834 layers.setTitleAt(index, "* " + getLabel(MainApplication.getLayerManager().getLayers()));
835 }
836 } else if (index >= 0) {
837 layers.setTitleAt(index, getLabel(MainApplication.getLayerManager().getLayers()));
838 }
839 }
840
841 /**
842 * Get the label for this panel
843 * @param availableLayers The layers to use to get the index
844 * @return The label for this layer
845 */
846 String getLabel(List<Layer> availableLayers) {
847 final int index = availableLayers.size() - availableLayers.indexOf(layer);
848 return (ExpertToggleAction.isExpert() ? "[" + index + "] " : "") + layer.getLabel();
849 }
850 }
851
852 /**
853 * Enables (or disables) the "Previous" button.
854 * @param value {@code true} to enable the button, {@code false} otherwise
855 */
856 public void setPreviousEnabled(boolean value) {
857 this.imageFirstAction.updateEnabledState();
858 this.btnFirst.setEnabled(value || this.imageFirstAction.isEnabled());
859 btnPrevious.setEnabled(value);
860 }
861
862 /**
863 * Enables (or disables) the "Next" button.
864 * @param value {@code true} to enable the button, {@code false} otherwise
865 */
866 public void setNextEnabled(boolean value) {
867 btnNext.setEnabled(value);
868 this.imageLastAction.updateEnabledState();
869 this.btnLast.setEnabled(value || this.imageLastAction.isEnabled());
870 }
871
872 /**
873 * Enables (or disables) the "Center view" button.
874 * @param value {@code true} to enable the button, {@code false} otherwise
875 * @return the old enabled value. Can be used to restore the original enable state
876 */
877 public static synchronized boolean setCentreEnabled(boolean value) {
878 final ImageViewerDialog instance = getInstance();
879 final boolean wasEnabled = instance.tbCentre.isEnabled();
880 instance.tbCentre.setEnabled(value);
881 instance.tbCentre.getAction().actionPerformed(new ActionEvent(instance.tbCentre, 0, null));
882 return wasEnabled;
883 }
884
885 private transient IImageEntry<? extends IImageEntry<?>> currentEntry;
886
887 /**
888 * Displays a single image for the given layer.
889 * @param ignoredData the image data
890 * @param entry image entry
891 * @see #displayImages
892 */
893 public void displayImage(ImageData ignoredData, ImageEntry entry) {
894 displayImages(Collections.singletonList(entry));
895 }
896
897 /**
898 * Displays a single image for the given layer.
899 * @param entry image entry
900 * @see #displayImages
901 */
902 public void displayImage(IImageEntry<?> entry) {
903 this.displayImages(Collections.singletonList(entry));
904 }
905
906 /**
907 * Displays images for the given layer.
908 * @param entries image entries
909 * @since 18246
910 */
911 public void displayImages(List<? extends IImageEntry<?>> entries) {
912 boolean imageChanged;
913 IImageEntry<?> entry = entries != null && entries.size() == 1 ? entries.get(0) : null;
914
915 synchronized (this) {
916 // TODO: pop up image dialog but don't load image again
917
918 imageChanged = currentEntry != entry;
919
920 if (centerView && imageChanged && entry != null && MainApplication.isDisplayingMapView() && entry.getPos() != null) {
921 MainApplication.getMap().mapView.zoomTo(entry.getPos());
922 }
923
924 currentEntry = entry;
925
926 for (ImageAction action : Arrays.asList(this.imageFirstAction, this.imagePreviousAction,
927 this.imageNextAction, this.imageLastAction)) {
928 action.updateEnabledState();
929 }
930 }
931
932
933 final boolean updateRequired;
934 final List<IGeoImageLayer> imageLayers = MainApplication.getLayerManager().getLayers().stream()
935 .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
936 if (!Config.getPref().getBoolean("geoimage.viewer.show.tabs", true)) {
937 updateRequired = true;
938 layers.removeAll();
939 // Clear the selected images in other geoimage layers
940 this.getImageTabs().map(m -> m.layer).filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast)
941 .filter(l -> !Objects.equals(entries, l.getSelection()))
942 .forEach(IGeoImageLayer::clearSelection);
943 } else {
944 updateRequired = imageLayers.stream().anyMatch(l -> this.getImageTabs().map(m -> m.layer).noneMatch(l::equals));
945 }
946 this.updateLayers(updateRequired);
947 if (entry != null) {
948 this.updateButtonsNonNullEntry(entry, imageChanged);
949 } else if (imageLayers.isEmpty()) {
950 this.updateButtonsNullEntry(entries);
951 return;
952 } else {
953 IGeoImageLayer layer = this.getImageTabs().map(m -> m.layer).filter(l -> l.getSelection().size() == 1).findFirst().orElse(null);
954 if (layer == null) {
955 this.updateButtonsNullEntry(entries);
956 } else {
957 this.displayImages(layer.getSelection());
958 }
959 return;
960 }
961 if (!isDialogShowing()) {
962 setIsDocked(false); // always open a detached window when an image is clicked and dialog is closed
963 unfurlDialog();
964 } else if (isDocked && isCollapsed) {
965 expand();
966 dialogsPanel.reconstruct(DialogsPanel.Action.COLLAPSED_TO_DEFAULT, this);
967 }
968 }
969
970 /**
971 * Update buttons for null entry
972 * @param entries {@code true} if multiple images are selected
973 */
974 private void updateButtonsNullEntry(List<? extends IImageEntry<?>> entries) {
975 boolean hasMultipleImages = entries != null && entries.size() > 1;
976 // if this method is called to reinitialize dialog content with a blank image,
977 // do not actually show the dialog again with a blank image if currently hidden (fix #10672)
978 this.updateTitle();
979 imgDisplay.setImage(null);
980 imgDisplay.setOsdText("");
981 setNextEnabled(false);
982 setPreviousEnabled(false);
983 btnDelete.setEnabled(hasMultipleImages);
984 btnDeleteFromDisk.setEnabled(hasMultipleImages);
985 btnCopyPath.setEnabled(false);
986 btnOpenExternal.setEnabled(false);
987 if (hasMultipleImages) {
988 imgDisplay.setEmptyText(tr("Multiple images selected"));
989 btnFirst.setEnabled(!isFirstImageSelected(entries));
990 btnLast.setEnabled(!isLastImageSelected(entries));
991 }
992 imgDisplay.setImage(null);
993 imgDisplay.setOsdText("");
994 }
995
996 /**
997 * Update the image viewer buttons for the new entry
998 * @param entry The new entry
999 * @param imageChanged {@code true} if it is not the same image as the previous image.
1000 */
1001 private void updateButtonsNonNullEntry(IImageEntry<?> entry, boolean imageChanged) {
1002 if (imageChanged) {
1003 cancelLoadingImage();
1004 // don't show unwanted image
1005 imgDisplay.setImage(null);
1006 // Set only if the image is new to preserve zoom and position if the same image is redisplayed
1007 // (e.g. to update the OSD).
1008 imgLoadingFuture = imgDisplay.setImage(entry);
1009 }
1010
1011 // Update buttons after setting the new entry
1012 setNextEnabled(entry.getNextImage() != null);
1013 setPreviousEnabled(entry.getPreviousImage() != null);
1014 btnDelete.setEnabled(entry.isRemoveSupported());
1015 btnDeleteFromDisk.setEnabled(entry.isDeleteSupported() && entry.isRemoveSupported());
1016 btnCopyPath.setEnabled(true);
1017 btnOpenExternal.setEnabled(true);
1018
1019 this.updateTitle();
1020 StringBuilder osd = new StringBuilder(entry.getDisplayName());
1021 if (entry.getElevation() != null) {
1022 osd.append(tr("\nAltitude: {0} m", Math.round(entry.getElevation())));
1023 }
1024 if (entry.getSpeed() != null) {
1025 osd.append(tr("\nSpeed: {0} km/h", Math.round(entry.getSpeed())));
1026 }
1027 DateTimeFormatter dtf = DateUtils.getDateTimeFormatter(FormatStyle.SHORT, FormatStyle.MEDIUM)
1028 // Set timezone to UTC since UTC is assumed when parsing the EXIF timestamp,
1029 // see see org.openstreetmap.josm.tools.ExifReader.readTime(com.drew.metadata.Metadata)
1030 .withZone(ZoneOffset.UTC);
1031 if (entry.hasExifTime()) {
1032 if (Config.getPref().getBoolean("geoimage.viewer.extendedinfo", false)) {
1033 osd.append(tr("\nEXIF DTO time: {0}", dtf.format(entry.getExifInstant())));
1034 } else {
1035 osd.append(tr("\nEXIF time: {0}", dtf.format(entry.getExifInstant())));
1036 }
1037 }
1038
1039 if (Config.getPref().getBoolean("geoimage.viewer.extendedinfo", false)) {
1040 if (entry.getExifGpsInstant() != null) {
1041 osd.append(tr("\nEXIF GPS time: {0}", dtf.format(entry.getExifGpsInstant())));
1042 }
1043 if (entry.hasGpsTime()) {
1044 osd.append(tr("\nCorr GPS time: {0}", dtf.format(entry.getGpsInstant())));
1045 }
1046 if (entry.getExifImgDir() != null) {
1047 osd.append(tr("\nDirection {0}\u00b0", Math.round(entry.getExifImgDir())));
1048 }
1049 if (entry.getExifGpsTrack() != null) {
1050 osd.append(tr("\nGPS direction: {0}\u00b0", Math.round(entry.getExifGpsTrack())));
1051 }
1052 if (entry.getExifHPosErr() != null) {
1053 osd.append(tr("\nHpos errror: {0}m", entry.getExifHPosErr()));
1054 }
1055 if (entry.getGps2d3dMode() != null) {
1056 osd.append(tr("\n2d/3d mode: {0}d", entry.getGps2d3dMode()));
1057 }
1058 if (entry.getGpsDiffMode() != null) {
1059 osd.append(tr("\nDifferential: {0}", entry.getGpsDiffMode()));
1060 }
1061 if (entry.getExifGpsDop() != null) {
1062 osd.append(tr("\nDOP: {0}", entry.getExifGpsDop()));
1063 }
1064 if (entry.getExifGpsDatum() != null) {
1065 osd.append(tr("\nDatum: {0}", entry.getExifGpsDatum().toString()));
1066 }
1067 if (entry.getExifGpsProcMethod() != null) {
1068 osd.append(tr("\nProc. method: {0}", entry.getExifGpsProcMethod().toString()));
1069 }
1070 }
1071 Optional.ofNullable(entry.getIptcCaption()).map(s -> tr("\nCaption: {0}", s)).ifPresent(osd::append);
1072 Optional.ofNullable(entry.getIptcHeadline()).map(s -> tr("\nHeadline: {0}", s)).ifPresent(osd::append);
1073 Optional.ofNullable(entry.getIptcKeywords()).map(s -> tr("\nKeywords: {0}", s)).ifPresent(osd::append);
1074 Optional.ofNullable(entry.getIptcObjectName()).map(s -> tr("\nObject name: {0}", s)).ifPresent(osd::append);
1075
1076 imgDisplay.setOsdText(osd.toString());
1077 }
1078
1079 private void updateTitle() {
1080 final IImageEntry<?> entry;
1081 synchronized (this) {
1082 entry = this.currentEntry;
1083 }
1084 String baseTitle = Optional.ofNullable(this.layers.getSelectedComponent())
1085 .filter(MoveImgDisplayPanel.class::isInstance).map(MoveImgDisplayPanel.class::cast)
1086 .map(m -> m.layer).map(Layer::getLabel).orElse(tr("Geotagged Images"));
1087 if (entry == null) {
1088 this.setTitle(baseTitle);
1089 } else {
1090 this.setTitle(baseTitle + (!entry.getDisplayName().isEmpty() ? " - " + entry.getDisplayName() : ""));
1091 }
1092 }
1093
1094 private static boolean isLastImageSelected(List<? extends IImageEntry<?>> data) {
1095 return data.stream().anyMatch(image -> data.contains(image.getLastImage()));
1096 }
1097
1098 private static boolean isFirstImageSelected(List<? extends IImageEntry<?>> data) {
1099 return data.stream().anyMatch(image -> data.contains(image.getFirstImage()));
1100 }
1101
1102 /**
1103 * When an image is closed, really close it and do not pop
1104 * up the side dialog.
1105 */
1106 @Override
1107 protected boolean dockWhenClosingDetachedDlg() {
1108 if (collapseButtonClicked) {
1109 collapseButtonClicked = false;
1110 return super.dockWhenClosingDetachedDlg();
1111 }
1112 return false;
1113 }
1114
1115 @Override
1116 protected void stateChanged() {
1117 super.stateChanged();
1118 if (btnCollapse != null) {
1119 btnCollapse.setVisible(!isDocked);
1120 }
1121 }
1122
1123 /**
1124 * Returns whether an image is currently displayed
1125 * @return If image is currently displayed
1126 */
1127 public boolean hasImage() {
1128 return currentEntry != null;
1129 }
1130
1131 /**
1132 * Returns the currently displayed image.
1133 * @return Currently displayed image or {@code null}
1134 * @since 18246 (signature)
1135 */
1136 public static IImageEntry<?> getCurrentImage() {
1137 return getInstance().currentEntry;
1138 }
1139
1140 /**
1141 * Returns the rotation of the currently displayed image.
1142 * @param entry The entry to get the rotation for. May be {@code null}.
1143 * @return the rotation of the currently displayed image, or {@code null}
1144 * @since 18263
1145 */
1146 public Vector3D getRotation(IImageEntry<?> entry) {
1147 return imgDisplay.getRotation(entry);
1148 }
1149
1150 /**
1151 * Returns whether the center view is currently active.
1152 * @return {@code true} if the center view is active, {@code false} otherwise
1153 * @since 9416
1154 */
1155 public static boolean isCenterView() {
1156 return getInstance().centerView;
1157 }
1158
1159 @Override
1160 public void layerAdded(LayerAddEvent e) {
1161 registerOnLayer(e.getAddedLayer());
1162 showLayer(e.getAddedLayer());
1163 }
1164
1165 @Override
1166 public void layerRemoving(LayerRemoveEvent e) {
1167 if (e.getRemovedLayer() instanceof IGeoImageLayer && ((IGeoImageLayer) e.getRemovedLayer()).containsImage(this.currentEntry)) {
1168 displayImages(null);
1169 }
1170 this.updateLayers(true);
1171 if (!layers.isVisible()) {
1172 hideNotify();
1173 destroy();
1174 }
1175 }
1176
1177 @Override
1178 public void layerOrderChanged(LayerOrderChangeEvent e) {
1179 this.updateLayers(true);
1180 }
1181
1182 @Override
1183 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
1184 if (!MainApplication.worker.isShutdown()) {
1185 showLayer(e.getSource().getActiveLayer());
1186 }
1187 }
1188
1189 /**
1190 * Reload the image. Call this if you load a low-resolution image first, and then get a high-resolution image, or
1191 * if you know that the image has changed on disk.
1192 * @since 18591
1193 */
1194 public void refresh() {
1195 if (SwingUtilities.isEventDispatchThread()) {
1196 this.updateButtonsNonNullEntry(currentEntry, true);
1197 } else {
1198 GuiHelper.runInEDT(this::refresh);
1199 }
1200 }
1201
1202 /**
1203 * Reload the image or reload only the image info. Call this if want to update the OSD.
1204 * @param imageChanged reload the image if true. Reload only the OSD if false.
1205 * @since 19387
1206 */
1207 public void refresh(boolean imageChanged) {
1208 if (SwingUtilities.isEventDispatchThread()) {
1209 this.updateButtonsNonNullEntry(currentEntry, imageChanged);
1210 } else {
1211 GuiHelper.runInEDT(this::refresh);
1212 }
1213 }
1214
1215 private void registerOnLayer(Layer layer) {
1216 if (layer instanceof IGeoImageLayer) {
1217 layer.addPropertyChangeListener(l -> {
1218 final List<?> currentTabLayers = this.getImageTabs().map(m -> m.layer).collect(Collectors.toList());
1219 if (Layer.NAME_PROP.equals(l.getPropertyName()) && currentTabLayers.contains(layer)) {
1220 this.updateLayers(true);
1221 if (((IGeoImageLayer) layer).containsImage(this.currentEntry)) {
1222 this.updateTitle();
1223 }
1224 } // Use Layer.VISIBLE_PROP here if we decide to do something when layer visibility changes
1225 });
1226 }
1227 }
1228
1229 private void showLayer(Layer newLayer) {
1230 if (this.currentEntry == null && newLayer instanceof GeoImageLayer) {
1231 ImageData imageData = ((GeoImageLayer) newLayer).getImageData();
1232 imageData.setSelectedImage(imageData.getFirstImage());
1233 }
1234 if (newLayer instanceof IGeoImageLayer) {
1235 this.updateLayers(true);
1236 }
1237 }
1238
1239 private void cancelLoadingImage() {
1240 if (imgLoadingFuture != null) {
1241 imgLoadingFuture.cancel(false);
1242 imgLoadingFuture = null;
1243 }
1244 }
1245}
Note: See TracBrowser for help on using the repository browser.