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

Last change on this file was 19581, checked in by GerdP, 9 days ago

see #24523: Add action to close all but the active tab in image viever

  • Unify translatable strings to "Close other tabs"
  • Property svn:eol-style set to native
File size: 54.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 private final CloseOtherTabsAction closeAllTabsAction = new CloseOtherTabsAction();
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 closeAllTabsAction.destroy();
421 cancelLoadingImage();
422 super.destroy();
423 // make sure that Image Display is destroyed here, it might not be a component
424 imgDisplay.destroy();
425 // Ensure that this dialog is removed from memory
426 destroyInstance();
427 }
428
429 /**
430 * This literally exists to silence sonarlint complaints.
431 * @param <I> the type of the operand and result of the operator
432 */
433 @FunctionalInterface
434 private interface SerializableUnaryOperator<I> extends UnaryOperator<I>, Serializable {
435 }
436
437 private abstract class ImageAction extends JosmAction {
438 final SerializableUnaryOperator<IImageEntry<?>> supplier;
439 ImageAction(String name, ImageProvider icon, String tooltip, Shortcut shortcut,
440 boolean registerInToolbar, String toolbarId, boolean installAdaptors,
441 final SerializableUnaryOperator<IImageEntry<?>> supplier) {
442 super(name, icon, tooltip, shortcut, registerInToolbar, toolbarId, installAdaptors);
443 Objects.requireNonNull(supplier);
444 this.supplier = supplier;
445 }
446
447 @Override
448 public void actionPerformed(ActionEvent event) {
449 final IImageEntry<?> entry = ImageViewerDialog.this.currentEntry;
450 if (entry != null) {
451 IImageEntry<?> nextEntry = this.getSupplier().apply(entry);
452 entry.selectImage(ImageViewerDialog.this, nextEntry);
453 }
454 this.resetRememberActions();
455 }
456
457 void resetRememberActions() {
458 for (ImageRememberAction action : Arrays.asList(ImageViewerDialog.this.imageLastAction, ImageViewerDialog.this.imageFirstAction)) {
459 action.last = null;
460 action.updateEnabledState();
461 }
462 }
463
464 SerializableUnaryOperator<IImageEntry<?>> getSupplier() {
465 return this.supplier;
466 }
467
468 @Override
469 protected void updateEnabledState() {
470 final IImageEntry<?> entry = ImageViewerDialog.this.currentEntry;
471 this.setEnabled(entry != null && this.getSupplier().apply(entry) != null);
472 }
473 }
474
475 private class ImageNextAction extends ImageAction {
476 ImageNextAction() {
477 super(null, new ImageProvider(DIALOG_FOLDER, "next"), tr("Next"), Shortcut.registerShortcut(
478 "geoimage:next", tr(GEOIMAGE_FILLER, tr("Show next Image")), KeyEvent.VK_PAGE_DOWN, Shortcut.DIRECT),
479 false, null, false, IImageEntry::getNextImage);
480 }
481 }
482
483 private class ImagePreviousAction extends ImageAction {
484 ImagePreviousAction() {
485 super(null, new ImageProvider(DIALOG_FOLDER, "previous"), tr("Previous"), Shortcut.registerShortcut(
486 "geoimage:previous", tr(GEOIMAGE_FILLER, tr("Show previous Image")), KeyEvent.VK_PAGE_UP, Shortcut.DIRECT),
487 false, null, false, IImageEntry::getPreviousImage);
488 }
489 }
490
491 /** This class exists to remember the last entry, and go back if clicked again when it would not otherwise be enabled */
492 private abstract class ImageRememberAction extends ImageAction {
493 private final ImageProvider defaultIcon;
494 transient IImageEntry<?> last;
495 ImageRememberAction(String name, ImageProvider icon, String tooltip, Shortcut shortcut,
496 boolean registerInToolbar, String toolbarId, boolean installAdaptors, SerializableUnaryOperator<IImageEntry<?>> supplier) {
497 super(name, icon, tooltip, shortcut, registerInToolbar, toolbarId, installAdaptors, supplier);
498 this.defaultIcon = icon;
499 }
500
501 /**
502 * Update the icon for this action
503 */
504 public void updateIcon() {
505 if (this.last != null) {
506 new ImageProvider(DIALOG_FOLDER, "history").getResource().attachImageIcon(this, true);
507 } else {
508 this.defaultIcon.getResource().attachImageIcon(this, true);
509 }
510 }
511
512 @Override
513 public void actionPerformed(ActionEvent event) {
514 final IImageEntry<?> current = ImageViewerDialog.this.currentEntry;
515 final IImageEntry<?> expected = this.supplier.apply(current);
516 if (current != null) {
517 IImageEntry<?> nextEntry = this.getSupplier().apply(current);
518 current.selectImage(ImageViewerDialog.this, nextEntry);
519 }
520 this.resetRememberActions();
521 if (!Objects.equals(current, expected)) {
522 this.last = current;
523 } else {
524 this.last = null;
525 }
526 this.updateEnabledState();
527 }
528
529 @Override
530 protected void updateEnabledState() {
531 final IImageEntry<?> current = ImageViewerDialog.this.currentEntry;
532 final IImageEntry<?> nextEntry = current != null ? this.getSupplier().apply(current) : null;
533 if (this.last == null && nextEntry != null && nextEntry.equals(current)) {
534 this.setEnabled(false);
535 } else {
536 super.updateEnabledState();
537 }
538 this.updateIcon();
539 }
540
541 @Override
542 SerializableUnaryOperator<IImageEntry<?>> getSupplier() {
543 if (this.last != null) {
544 return entry -> this.last;
545 }
546 return super.getSupplier();
547 }
548 }
549
550 private class ImageFirstAction extends ImageRememberAction {
551 ImageFirstAction() {
552 super(null, new ImageProvider(DIALOG_FOLDER, "first"), tr("First"), Shortcut.registerShortcut(
553 "geoimage:first", tr(GEOIMAGE_FILLER, tr("Show first Image")), KeyEvent.VK_HOME, Shortcut.DIRECT),
554 false, null, false, IImageEntry::getFirstImage);
555 }
556 }
557
558 private class ImageLastAction extends ImageRememberAction {
559 ImageLastAction() {
560 super(null, new ImageProvider(DIALOG_FOLDER, "last"), tr("Last"), Shortcut.registerShortcut(
561 "geoimage:last", tr(GEOIMAGE_FILLER, tr("Show last Image")), KeyEvent.VK_END, Shortcut.DIRECT),
562 false, null, false, IImageEntry::getLastImage);
563 }
564 }
565
566 private class ImageCenterViewAction extends JosmAction {
567 ImageCenterViewAction() {
568 super(null, new ImageProvider("dialogs/autoscale", "selection"), tr("Center view"), null,
569 false, null, false);
570 }
571
572 @Override
573 public void actionPerformed(ActionEvent e) {
574 final JToggleButton button = (JToggleButton) e.getSource();
575 centerView = button.isEnabled() && button.isSelected();
576 Config.getPref().putBoolean("geoimage.viewer.centre.on.image", centerView);
577 if (centerView && currentEntry != null && currentEntry.getPos() != null) {
578 MainApplication.getMap().mapView.zoomTo(currentEntry.getPos());
579 }
580 }
581 }
582
583 private class ImageZoomAction extends JosmAction {
584 ImageZoomAction() {
585 super(null, new ImageProvider(DIALOG_FOLDER, "zoom-best-fit"), tr("Zoom best fit and 1:1"), null,
586 false, null, false);
587 }
588
589 @Override
590 public void actionPerformed(ActionEvent e) {
591 imgDisplay.zoomBestFitOrOne();
592 }
593 }
594
595 private class ImageExtendedInfoAction extends JosmAction {
596 ImageExtendedInfoAction() {
597 super(null, new ImageProvider("info"), tr("Display image extended metadata"), Shortcut.registerShortcut(
598 "geoimage:extendedinfos", tr(GEOIMAGE_FILLER, tr("Toggle display of extended information")),
599 KeyEvent.CHAR_UNDEFINED, Shortcut.NONE),
600 false, null, false);
601 }
602
603 @Override
604 public void actionPerformed(ActionEvent e) {
605 final JToggleButton button = (JToggleButton) e.getSource();
606 extendedImgInfo = button.isEnabled() && button.isSelected();
607 Config.getPref().putBoolean("geoimage.viewer.extendedinfo", extendedImgInfo);
608 refresh(false);
609 }
610 }
611
612 private class ImageRemoveAction extends JosmAction {
613 ImageRemoveAction() {
614 super(null, new ImageProvider(DIALOG_FOLDER, "delete"), tr("Remove photo from layer"), Shortcut.registerShortcut(
615 "geoimage:deleteimagefromlayer", tr(GEOIMAGE_FILLER, tr("Remove photo from layer")), KeyEvent.VK_DELETE, Shortcut.SHIFT),
616 false, null, false);
617 }
618
619 @Override
620 public void actionPerformed(ActionEvent e) {
621 if (ImageViewerDialog.this.currentEntry != null) {
622 IImageEntry<?> imageEntry = ImageViewerDialog.this.currentEntry;
623 if (imageEntry.isRemoveSupported()) {
624 imageEntry.remove();
625 }
626 selectNextImageAfterDeletion(imageEntry);
627 }
628 }
629
630 /**
631 * Select the logical next entry after deleting the currently viewed image
632 * @param oldEntry The image entry that was just deleted
633 */
634 private void selectNextImageAfterDeletion(IImageEntry<?> oldEntry) {
635 final IImageEntry<?> currentImageEntry = ImageViewerDialog.this.currentEntry;
636 // This is mostly just in case something changes the displayed entry (aka avoid race condition) or an image provider
637 // sets the next image itself.
638 if (Objects.equals(currentImageEntry, oldEntry)) {
639 final IImageEntry<?> nextImage;
640 if (oldEntry instanceof ImageEntry) {
641 nextImage = ((ImageEntry) oldEntry).getDataSet().getSelectedImage();
642 } else if (oldEntry.getNextImage() != null) {
643 nextImage = oldEntry.getNextImage();
644 } else if (oldEntry.getPreviousImage() != null) {
645 nextImage = oldEntry.getPreviousImage();
646 } else {
647 nextImage = null;
648 }
649 ImageViewerDialog.getInstance().displayImages(nextImage == null ? null : Collections.singletonList(nextImage));
650 }
651 }
652 }
653
654 private class ImageRemoveFromDiskAction extends JosmAction {
655 ImageRemoveFromDiskAction() {
656 super(null, new ImageProvider(DIALOG_FOLDER, "geoimage/deletefromdisk"), tr("Delete image file from disk"),
657 Shortcut.registerShortcut("geoimage:deletefilefromdisk",
658 tr(GEOIMAGE_FILLER, tr("Delete image file from disk")), KeyEvent.VK_DELETE, Shortcut.CTRL_SHIFT),
659 false, null, false);
660 }
661
662 @Override
663 public void actionPerformed(ActionEvent e) {
664 if (currentEntry != null) {
665 IImageEntry<?> oldEntry = ImageViewerDialog.this.currentEntry;
666 List<IImageEntry<?>> toDelete = oldEntry instanceof ImageEntry ?
667 new ArrayList<>(((ImageEntry) oldEntry).getDataSet().getSelectedImages())
668 : Collections.singletonList(oldEntry);
669 int size = toDelete.size();
670
671 int result = new ExtendedDialog(
672 MainApplication.getMainFrame(),
673 tr("Delete image file from disk"),
674 tr("Cancel"), tr("Delete"))
675 .setButtonIcons("cancel", "dialogs/geoimage/deletefromdisk")
676 .setContent(new JLabel("<html><h3>"
677 + trn("Delete the file from disk?",
678 "Delete the {0} files from disk?", size, size)
679 + "<p>" + trn("The image file will be permanently lost!",
680 "The images files will be permanently lost!", size) + "</h3></html>",
681 ImageProvider.get("dialogs/geoimage/deletefromdisk"), SwingConstants.LEADING))
682 .toggleEnable("geoimage.deleteimagefromdisk")
683 .setCancelButton(1)
684 .setDefaultButton(2)
685 .showDialog()
686 .getValue();
687
688 if (result == 2) {
689 final List<ImageData> imageDataCollection = toDelete.stream().filter(ImageEntry.class::isInstance)
690 .map(ImageEntry.class::cast).map(ImageEntry::getDataSet).distinct().collect(Collectors.toList());
691 for (IImageEntry<?> delete : toDelete) {
692 // We have to be able to remove the image from the layer and the image from its storage location
693 // If either are false, then don't remove the image.
694 if (delete.isRemoveSupported() && delete.isDeleteSupported() && delete.remove() && delete.delete()) {
695 Logging.info("File {0} deleted.", delete.getFile());
696 } else {
697 JOptionPane.showMessageDialog(
698 MainApplication.getMainFrame(),
699 tr("Image file could not be deleted."),
700 tr("Error"),
701 JOptionPane.ERROR_MESSAGE
702 );
703 }
704 }
705 imageDataCollection.forEach(data -> {
706 data.notifyImageUpdate();
707 data.updateSelectedImage();
708 });
709 ImageViewerDialog.this.imageRemoveAction.selectNextImageAfterDeletion(oldEntry);
710 }
711 }
712 }
713 }
714
715 private class ImageCopyPathAction extends JosmAction {
716 ImageCopyPathAction() {
717 super(null, new ImageProvider("copy"), tr("Copy image path"), Shortcut.registerShortcut(
718 "geoimage:copypath", tr(GEOIMAGE_FILLER, tr("Copy image path")), KeyEvent.VK_C, Shortcut.ALT_CTRL_SHIFT),
719 false, null, false);
720 }
721
722 @Override
723 public void actionPerformed(ActionEvent e) {
724 if (currentEntry != null) {
725 ClipboardUtils.copyString(String.valueOf(currentEntry.getImageURI()));
726 }
727 }
728 }
729
730 private class ImageCollapseAction extends JosmAction {
731 ImageCollapseAction() {
732 super(null, new ImageProvider(DIALOG_FOLDER, "collapse"), tr("Move dialog to the side pane"), null,
733 false, null, false);
734 }
735
736 @Override
737 public void actionPerformed(ActionEvent e) {
738 collapseButtonClicked = true;
739 detachedDialog.getToolkit().getSystemEventQueue().postEvent(new WindowEvent(detachedDialog, WindowEvent.WINDOW_CLOSING));
740 }
741 }
742
743 private class ImageOpenExternalAction extends JosmAction {
744 ImageOpenExternalAction() {
745 super(null, new ImageProvider("external-link"), tr("Open image in external viewer"), null, false, null, false);
746 }
747
748 @Override
749 public void actionPerformed(ActionEvent e) {
750 if (currentEntry != null && currentEntry.getImageURI() != null) {
751 try {
752 PlatformManager.getPlatform().openUrl(currentEntry.getImageURI().toURL().toExternalForm());
753 } catch (IOException ex) {
754 Logging.error(ex);
755 }
756 }
757 }
758 }
759
760 private class CloseOtherTabsAction extends JosmAction {
761 CloseOtherTabsAction() {
762 super(tr("Close other tabs"), new ImageProvider("misc", "close"), tr("Close other tabs"),
763 Shortcut.registerShortcut("closeother", "Close other tabs", KeyEvent.VK_Y, Shortcut.ALT), false, null,
764 false);
765 }
766
767 @Override
768 public void actionPerformed(ActionEvent e) {
769 for (int i = layers.getTabCount() - 1; i >= 0; i--) {
770 Component component = layers.getComponentAt(i);
771 if (component instanceof MoveImgDisplayPanel) {
772 MoveImgDisplayPanel<?> moveImgDisplayPanel = (MoveImgDisplayPanel<?>) component;
773 if (moveImgDisplayPanel.layer.containsImage(currentEntry))
774 continue;
775 layers.removeTabAt(i);
776 layers.remove(moveImgDisplayPanel);
777 }
778 }
779 }
780 }
781
782 /**
783 * A tab title renderer for {@link HideableTabbedPane} that allows us to close tabs.
784 */
785 private static class CloseableTab extends JPanel implements PropertyChangeListener {
786 private final JLabel title;
787 private final JButton close;
788
789 /**
790 * Create a new {@link CloseableTab}.
791 * @param parent The parent to add property change listeners to. It should be a {@link HideableTabbedPane} in most cases.
792 * @param closeAction The action to run to close the tab. You probably want to call {@link JTabbedPane#removeTabAt(int)}
793 * at the very least.
794 */
795 CloseableTab(Component parent, ActionListener closeAction) {
796 this.title = new JLabel();
797 this.add(this.title);
798 close = new JButton(ImageProvider.get("misc", "close"));
799 close.setBorder(BorderFactory.createEmptyBorder());
800 this.add(close);
801 close.addActionListener(closeAction);
802 close.addActionListener(l -> parent.removePropertyChangeListener("indexForTitle", this));
803 parent.addPropertyChangeListener("indexForTitle", this);
804 }
805
806 @Override
807 public void propertyChange(PropertyChangeEvent evt) {
808 if (evt.getSource() instanceof JTabbedPane) {
809 JTabbedPane source = (JTabbedPane) evt.getSource();
810 if (this.getParent() == null) {
811 source.removePropertyChangeListener(evt.getPropertyName(), this);
812 }
813 if ("indexForTitle".equals(evt.getPropertyName())) {
814 int idx = source.indexOfTabComponent(this);
815 if (idx >= 0) {
816 this.title.setText(source.getTitleAt(idx));
817 }
818 }
819 // Used to hack around UI staying visible. This assumes that the parent component is a HideableTabbedPane.
820 this.title.setVisible(source.getTabCount() != 1);
821 this.close.setVisible(source.getTabCount() != 1);
822 }
823 }
824 }
825
826 /**
827 * A JPanel whose entire purpose is to display an image by (a) moving the imgDisplay around and (b) setting the imgDisplay as a child
828 * for this panel.
829 */
830 private static class MoveImgDisplayPanel<T extends Layer & IGeoImageLayer> extends JPanel {
831 private final T layer;
832 private final ImageDisplay imgDisplay;
833
834 MoveImgDisplayPanel(ImageDisplay imgDisplay, T layer) {
835 super(new BorderLayout());
836 this.layer = layer;
837 this.imgDisplay = imgDisplay;
838 }
839
840 /**
841 * Call when the selection model updates
842 */
843 void fireModelUpdate() {
844 JTabbedPane layers = ImageViewerDialog.getInstance().layers;
845 int index = layers.indexOfComponent(this);
846 if (this == layers.getSelectedComponent()) {
847 if (!this.layer.getSelection().isEmpty() && !this.layer.getSelection().contains(ImageViewerDialog.getCurrentImage())) {
848 ImageViewerDialog.getInstance().displayImages(this.layer.getSelection());
849 this.layer.invalidate(); // This will force the geoimage layers to update properly.
850 }
851 if (this.imgDisplay.getParent() != this) {
852 this.add(this.imgDisplay, BorderLayout.CENTER);
853 this.imgDisplay.invalidate();
854 this.revalidate();
855 }
856 if (index >= 0) {
857 layers.setTitleAt(index, "* " + getLabel(MainApplication.getLayerManager().getLayers()));
858 }
859 } else if (index >= 0) {
860 layers.setTitleAt(index, getLabel(MainApplication.getLayerManager().getLayers()));
861 }
862 }
863
864 /**
865 * Get the label for this panel
866 * @param availableLayers The layers to use to get the index
867 * @return The label for this layer
868 */
869 String getLabel(List<Layer> availableLayers) {
870 final int index = availableLayers.size() - availableLayers.indexOf(layer);
871 return (ExpertToggleAction.isExpert() ? "[" + index + "] " : "") + layer.getLabel();
872 }
873 }
874
875 /**
876 * Enables (or disables) the "Previous" button.
877 * @param value {@code true} to enable the button, {@code false} otherwise
878 */
879 public void setPreviousEnabled(boolean value) {
880 this.imageFirstAction.updateEnabledState();
881 this.btnFirst.setEnabled(value || this.imageFirstAction.isEnabled());
882 btnPrevious.setEnabled(value);
883 }
884
885 /**
886 * Enables (or disables) the "Next" button.
887 * @param value {@code true} to enable the button, {@code false} otherwise
888 */
889 public void setNextEnabled(boolean value) {
890 btnNext.setEnabled(value);
891 this.imageLastAction.updateEnabledState();
892 this.btnLast.setEnabled(value || this.imageLastAction.isEnabled());
893 }
894
895 /**
896 * Enables (or disables) the "Center view" button.
897 * @param value {@code true} to enable the button, {@code false} otherwise
898 * @return the old enabled value. Can be used to restore the original enable state
899 */
900 public static synchronized boolean setCentreEnabled(boolean value) {
901 final ImageViewerDialog instance = getInstance();
902 final boolean wasEnabled = instance.tbCentre.isEnabled();
903 instance.tbCentre.setEnabled(value);
904 instance.tbCentre.getAction().actionPerformed(new ActionEvent(instance.tbCentre, 0, null));
905 return wasEnabled;
906 }
907
908 private transient IImageEntry<? extends IImageEntry<?>> currentEntry;
909
910 /**
911 * Displays a single image for the given layer.
912 * @param ignoredData the image data
913 * @param entry image entry
914 * @see #displayImages
915 */
916 public void displayImage(ImageData ignoredData, ImageEntry entry) {
917 displayImages(Collections.singletonList(entry));
918 }
919
920 /**
921 * Displays a single image for the given layer.
922 * @param entry image entry
923 * @see #displayImages
924 */
925 public void displayImage(IImageEntry<?> entry) {
926 this.displayImages(Collections.singletonList(entry));
927 }
928
929 /**
930 * Displays images for the given layer.
931 * @param entries image entries
932 * @since 18246
933 */
934 public void displayImages(List<? extends IImageEntry<?>> entries) {
935 boolean imageChanged;
936 IImageEntry<?> entry = entries != null && entries.size() == 1 ? entries.get(0) : null;
937
938 synchronized (this) {
939 // TODO: pop up image dialog but don't load image again
940
941 imageChanged = currentEntry != entry;
942
943 if (centerView && imageChanged && entry != null && MainApplication.isDisplayingMapView() && entry.getPos() != null) {
944 MainApplication.getMap().mapView.zoomTo(entry.getPos());
945 }
946
947 currentEntry = entry;
948
949 for (ImageAction action : Arrays.asList(this.imageFirstAction, this.imagePreviousAction,
950 this.imageNextAction, this.imageLastAction)) {
951 action.updateEnabledState();
952 }
953 }
954
955
956 final boolean updateRequired;
957 final List<IGeoImageLayer> imageLayers = MainApplication.getLayerManager().getLayers().stream()
958 .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
959 if (!Config.getPref().getBoolean("geoimage.viewer.show.tabs", true)) {
960 updateRequired = true;
961 layers.removeAll();
962 // Clear the selected images in other geoimage layers
963 this.getImageTabs().map(m -> m.layer).filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast)
964 .filter(l -> !Objects.equals(entries, l.getSelection()))
965 .forEach(IGeoImageLayer::clearSelection);
966 } else {
967 updateRequired = imageLayers.stream().anyMatch(l -> this.getImageTabs().map(m -> m.layer).noneMatch(l::equals));
968 }
969 this.updateLayers(updateRequired);
970 if (entry != null) {
971 this.updateButtonsNonNullEntry(entry, imageChanged);
972 } else if (imageLayers.isEmpty()) {
973 this.updateButtonsNullEntry(entries);
974 return;
975 } else {
976 IGeoImageLayer layer = this.getImageTabs().map(m -> m.layer).filter(l -> l.getSelection().size() == 1).findFirst().orElse(null);
977 if (layer == null) {
978 this.updateButtonsNullEntry(entries);
979 } else {
980 this.displayImages(layer.getSelection());
981 }
982 return;
983 }
984 if (!isDialogShowing()) {
985 setIsDocked(false); // always open a detached window when an image is clicked and dialog is closed
986 unfurlDialog();
987 } else if (isDocked && isCollapsed) {
988 expand();
989 dialogsPanel.reconstruct(DialogsPanel.Action.COLLAPSED_TO_DEFAULT, this);
990 }
991 }
992
993 /**
994 * Update buttons for null entry
995 * @param entries {@code true} if multiple images are selected
996 */
997 private void updateButtonsNullEntry(List<? extends IImageEntry<?>> entries) {
998 boolean hasMultipleImages = entries != null && entries.size() > 1;
999 // if this method is called to reinitialize dialog content with a blank image,
1000 // do not actually show the dialog again with a blank image if currently hidden (fix #10672)
1001 this.updateTitle();
1002 imgDisplay.setImage(null);
1003 imgDisplay.setOsdText("");
1004 setNextEnabled(false);
1005 setPreviousEnabled(false);
1006 btnDelete.setEnabled(hasMultipleImages);
1007 btnDeleteFromDisk.setEnabled(hasMultipleImages);
1008 btnCopyPath.setEnabled(false);
1009 btnOpenExternal.setEnabled(false);
1010 if (hasMultipleImages) {
1011 imgDisplay.setEmptyText(tr("Multiple images selected"));
1012 btnFirst.setEnabled(!isFirstImageSelected(entries));
1013 btnLast.setEnabled(!isLastImageSelected(entries));
1014 }
1015 imgDisplay.setImage(null);
1016 imgDisplay.setOsdText("");
1017 }
1018
1019 /**
1020 * Update the image viewer buttons for the new entry
1021 * @param entry The new entry
1022 * @param imageChanged {@code true} if it is not the same image as the previous image.
1023 */
1024 private void updateButtonsNonNullEntry(IImageEntry<?> entry, boolean imageChanged) {
1025 if (imageChanged) {
1026 cancelLoadingImage();
1027 // don't show unwanted image
1028 imgDisplay.setImage(null);
1029 // Set only if the image is new to preserve zoom and position if the same image is redisplayed
1030 // (e.g. to update the OSD).
1031 imgLoadingFuture = imgDisplay.setImage(entry);
1032 }
1033
1034 // Update buttons after setting the new entry
1035 setNextEnabled(entry.getNextImage() != null);
1036 setPreviousEnabled(entry.getPreviousImage() != null);
1037 btnDelete.setEnabled(entry.isRemoveSupported());
1038 btnDeleteFromDisk.setEnabled(entry.isDeleteSupported() && entry.isRemoveSupported());
1039 btnCopyPath.setEnabled(true);
1040 btnOpenExternal.setEnabled(true);
1041
1042 this.updateTitle();
1043 StringBuilder osd = new StringBuilder(entry.getDisplayName());
1044 if (entry.getElevation() != null) {
1045 osd.append(tr("\nAltitude: {0} m", Math.round(entry.getElevation())));
1046 }
1047 if (entry.getSpeed() != null) {
1048 osd.append(tr("\nSpeed: {0} km/h", Math.round(entry.getSpeed())));
1049 }
1050 DateTimeFormatter dtf = DateUtils.getDateTimeFormatter(FormatStyle.SHORT, FormatStyle.MEDIUM)
1051 // Set timezone to UTC since UTC is assumed when parsing the EXIF timestamp,
1052 // see see org.openstreetmap.josm.tools.ExifReader.readTime(com.drew.metadata.Metadata)
1053 .withZone(ZoneOffset.UTC);
1054 if (entry.hasExifTime()) {
1055 if (Config.getPref().getBoolean("geoimage.viewer.extendedinfo", false)) {
1056 osd.append(tr("\nEXIF DTO time: {0}", dtf.format(entry.getExifInstant())));
1057 } else {
1058 osd.append(tr("\nEXIF time: {0}", dtf.format(entry.getExifInstant())));
1059 }
1060 }
1061
1062 if (Config.getPref().getBoolean("geoimage.viewer.extendedinfo", false)) {
1063 if (entry.getExifGpsInstant() != null) {
1064 osd.append(tr("\nEXIF GPS time: {0}", dtf.format(entry.getExifGpsInstant())));
1065 }
1066 if (entry.hasGpsTime()) {
1067 osd.append(tr("\nCorr GPS time: {0}", dtf.format(entry.getGpsInstant())));
1068 }
1069 if (entry.getExifImgDir() != null) {
1070 osd.append(tr("\nDirection {0}\u00b0", Math.round(entry.getExifImgDir())));
1071 }
1072 if (entry.getExifGpsTrack() != null) {
1073 osd.append(tr("\nGPS direction: {0}\u00b0", Math.round(entry.getExifGpsTrack())));
1074 }
1075 if (entry.getExifHPosErr() != null) {
1076 osd.append(tr("\nHpos errror: {0}m", entry.getExifHPosErr()));
1077 }
1078 if (entry.getGps2d3dMode() != null) {
1079 osd.append(tr("\n2d/3d mode: {0}d", entry.getGps2d3dMode()));
1080 }
1081 if (entry.getGpsDiffMode() != null) {
1082 osd.append(tr("\nDifferential: {0}", entry.getGpsDiffMode()));
1083 }
1084 if (entry.getExifGpsDop() != null) {
1085 osd.append(tr("\nDOP: {0}", entry.getExifGpsDop()));
1086 }
1087 if (entry.getExifGpsDatum() != null) {
1088 osd.append(tr("\nDatum: {0}", entry.getExifGpsDatum().toString()));
1089 }
1090 if (entry.getExifGpsProcMethod() != null) {
1091 osd.append(tr("\nProc. method: {0}", entry.getExifGpsProcMethod().toString()));
1092 }
1093 }
1094 Optional.ofNullable(entry.getIptcCaption()).map(s -> tr("\nCaption: {0}", s)).ifPresent(osd::append);
1095 Optional.ofNullable(entry.getIptcHeadline()).map(s -> tr("\nHeadline: {0}", s)).ifPresent(osd::append);
1096 Optional.ofNullable(entry.getIptcKeywords()).map(s -> tr("\nKeywords: {0}", s)).ifPresent(osd::append);
1097 Optional.ofNullable(entry.getIptcObjectName()).map(s -> tr("\nObject name: {0}", s)).ifPresent(osd::append);
1098
1099 imgDisplay.setOsdText(osd.toString());
1100 }
1101
1102 private void updateTitle() {
1103 final IImageEntry<?> entry;
1104 synchronized (this) {
1105 entry = this.currentEntry;
1106 }
1107 String baseTitle = Optional.ofNullable(this.layers.getSelectedComponent())
1108 .filter(MoveImgDisplayPanel.class::isInstance).map(MoveImgDisplayPanel.class::cast)
1109 .map(m -> m.layer).map(Layer::getLabel).orElse(tr("Geotagged Images"));
1110 if (entry == null) {
1111 this.setTitle(baseTitle);
1112 } else {
1113 this.setTitle(baseTitle + (!entry.getDisplayName().isEmpty() ? " - " + entry.getDisplayName() : ""));
1114 }
1115 }
1116
1117 private static boolean isLastImageSelected(List<? extends IImageEntry<?>> data) {
1118 return data.stream().anyMatch(image -> data.contains(image.getLastImage()));
1119 }
1120
1121 private static boolean isFirstImageSelected(List<? extends IImageEntry<?>> data) {
1122 return data.stream().anyMatch(image -> data.contains(image.getFirstImage()));
1123 }
1124
1125 /**
1126 * When an image is closed, really close it and do not pop
1127 * up the side dialog.
1128 */
1129 @Override
1130 protected boolean dockWhenClosingDetachedDlg() {
1131 if (collapseButtonClicked) {
1132 collapseButtonClicked = false;
1133 return super.dockWhenClosingDetachedDlg();
1134 }
1135 return false;
1136 }
1137
1138 @Override
1139 protected void stateChanged() {
1140 super.stateChanged();
1141 if (btnCollapse != null) {
1142 btnCollapse.setVisible(!isDocked);
1143 }
1144 }
1145
1146 /**
1147 * Returns whether an image is currently displayed
1148 * @return If image is currently displayed
1149 */
1150 public boolean hasImage() {
1151 return currentEntry != null;
1152 }
1153
1154 /**
1155 * Returns the currently displayed image.
1156 * @return Currently displayed image or {@code null}
1157 * @since 18246 (signature)
1158 */
1159 public static IImageEntry<?> getCurrentImage() {
1160 return getInstance().currentEntry;
1161 }
1162
1163 /**
1164 * Returns the rotation of the currently displayed image.
1165 * @param entry The entry to get the rotation for. May be {@code null}.
1166 * @return the rotation of the currently displayed image, or {@code null}
1167 * @since 18263
1168 */
1169 public Vector3D getRotation(IImageEntry<?> entry) {
1170 return imgDisplay.getRotation(entry);
1171 }
1172
1173 /**
1174 * Returns whether the center view is currently active.
1175 * @return {@code true} if the center view is active, {@code false} otherwise
1176 * @since 9416
1177 */
1178 public static boolean isCenterView() {
1179 return getInstance().centerView;
1180 }
1181
1182 @Override
1183 public void layerAdded(LayerAddEvent e) {
1184 registerOnLayer(e.getAddedLayer());
1185 showLayer(e.getAddedLayer());
1186 }
1187
1188 @Override
1189 public void layerRemoving(LayerRemoveEvent e) {
1190 if (e.getRemovedLayer() instanceof IGeoImageLayer && ((IGeoImageLayer) e.getRemovedLayer()).containsImage(this.currentEntry)) {
1191 displayImages(null);
1192 }
1193 this.updateLayers(true);
1194 if (!layers.isVisible()) {
1195 hideNotify();
1196 destroy();
1197 }
1198 }
1199
1200 @Override
1201 public void layerOrderChanged(LayerOrderChangeEvent e) {
1202 this.updateLayers(true);
1203 }
1204
1205 @Override
1206 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
1207 if (!MainApplication.worker.isShutdown()) {
1208 showLayer(e.getSource().getActiveLayer());
1209 }
1210 }
1211
1212 /**
1213 * Reload the image. Call this if you load a low-resolution image first, and then get a high-resolution image, or
1214 * if you know that the image has changed on disk.
1215 * @since 18591
1216 */
1217 public void refresh() {
1218 if (SwingUtilities.isEventDispatchThread()) {
1219 this.updateButtonsNonNullEntry(currentEntry, true);
1220 } else {
1221 GuiHelper.runInEDT(this::refresh);
1222 }
1223 }
1224
1225 /**
1226 * Reload the image or reload only the image info. Call this if want to update the OSD.
1227 * @param imageChanged reload the image if true. Reload only the OSD if false.
1228 * @since 19387
1229 */
1230 public void refresh(boolean imageChanged) {
1231 if (SwingUtilities.isEventDispatchThread()) {
1232 this.updateButtonsNonNullEntry(currentEntry, imageChanged);
1233 } else {
1234 GuiHelper.runInEDT(this::refresh);
1235 }
1236 }
1237
1238 private void registerOnLayer(Layer layer) {
1239 if (layer instanceof IGeoImageLayer) {
1240 layer.addPropertyChangeListener(l -> {
1241 final List<?> currentTabLayers = this.getImageTabs().map(m -> m.layer).collect(Collectors.toList());
1242 if (Layer.NAME_PROP.equals(l.getPropertyName()) && currentTabLayers.contains(layer)) {
1243 this.updateLayers(true);
1244 if (((IGeoImageLayer) layer).containsImage(this.currentEntry)) {
1245 this.updateTitle();
1246 }
1247 } // Use Layer.VISIBLE_PROP here if we decide to do something when layer visibility changes
1248 });
1249 }
1250 }
1251
1252 private void showLayer(Layer newLayer) {
1253 if (this.currentEntry == null && newLayer instanceof GeoImageLayer) {
1254 ImageData imageData = ((GeoImageLayer) newLayer).getImageData();
1255 imageData.setSelectedImage(imageData.getFirstImage());
1256 }
1257 if (newLayer instanceof IGeoImageLayer) {
1258 this.updateLayers(true);
1259 }
1260 }
1261
1262 private void cancelLoadingImage() {
1263 if (imgLoadingFuture != null) {
1264 imgLoadingFuture.cancel(false);
1265 imgLoadingFuture = null;
1266 }
1267 }
1268}
Note: See TracBrowser for help on using the repository browser.