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

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

fix #15709 - revert r13264 to restore proper lazy initialization of ImageViewerDialog (patch by cmuelle8)

  • Property svn:eol-style set to native
File size: 18.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer.geoimage;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.BorderLayout;
7import java.awt.Component;
8import java.awt.Dimension;
9import java.awt.GridBagConstraints;
10import java.awt.GridBagLayout;
11import java.awt.event.ActionEvent;
12import java.awt.event.KeyEvent;
13import java.awt.event.WindowEvent;
14import java.text.DateFormat;
15import java.text.SimpleDateFormat;
16
17import javax.swing.Box;
18import javax.swing.JButton;
19import javax.swing.JPanel;
20import javax.swing.JToggleButton;
21
22import org.openstreetmap.josm.actions.JosmAction;
23import org.openstreetmap.josm.gui.MainApplication;
24import org.openstreetmap.josm.gui.dialogs.DialogsPanel.Action;
25import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
26import org.openstreetmap.josm.gui.layer.Layer;
27import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
28import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
29import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
30import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
31import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
32import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
33import org.openstreetmap.josm.tools.ImageProvider;
34import org.openstreetmap.josm.tools.Shortcut;
35import org.openstreetmap.josm.tools.date.DateUtils;
36
37/**
38 * Dialog to view and manipulate geo-tagged images from a {@link GeoImageLayer}.
39 */
40public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener {
41
42 private final ImageZoomAction imageZoomAction = new ImageZoomAction();
43 private final ImageCenterViewAction imageCenterViewAction = new ImageCenterViewAction();
44 private final ImageNextAction imageNextAction = new ImageNextAction();
45 private final ImageRemoveAction imageRemoveAction = new ImageRemoveAction();
46 private final ImageRemoveFromDiskAction imageRemoveFromDiskAction = new ImageRemoveFromDiskAction();
47 private final ImagePreviousAction imagePreviousAction = new ImagePreviousAction();
48 private final ImageCollapseAction imageCollapseAction = new ImageCollapseAction();
49 private final ImageFirstAction imageFirstAction = new ImageFirstAction();
50 private final ImageLastAction imageLastAction = new ImageLastAction();
51 private final ImageCopyPathAction imageCopyPathAction = new ImageCopyPathAction();
52
53 private final ImageDisplay imgDisplay = new ImageDisplay();
54 private boolean centerView;
55
56 // Only one instance of that class is present at one time
57 private static volatile ImageViewerDialog dialog;
58
59 private boolean collapseButtonClicked;
60
61 static void createInstance() {
62 if (dialog != null)
63 throw new IllegalStateException("ImageViewerDialog instance was already created");
64 dialog = new ImageViewerDialog();
65 }
66
67 /**
68 * Replies the unique instance of this dialog
69 * @return the unique instance
70 */
71 public static ImageViewerDialog getInstance() {
72 if (dialog == null)
73 throw new AssertionError("a new instance needs to be created first");
74 return dialog;
75 }
76
77 private JButton btnNext;
78 private JButton btnPrevious;
79 private JButton btnCollapse;
80 private JToggleButton tbCentre;
81
82 private ImageViewerDialog() {
83 super(tr("Geotagged Images"), "geoimage", tr("Display geotagged images"), Shortcut.registerShortcut("tools:geotagged",
84 tr("Tool: {0}", tr("Display geotagged images")), KeyEvent.VK_Y, Shortcut.DIRECT), 200);
85 build();
86 MainApplication.getLayerManager().addActiveLayerChangeListener(this);
87 MainApplication.getLayerManager().addLayerChangeListener(this);
88 }
89
90 private void build() {
91 JPanel content = new JPanel(new BorderLayout());
92
93 content.add(imgDisplay, BorderLayout.CENTER);
94
95 Dimension buttonDim = new Dimension(26, 26);
96
97 btnPrevious = new JButton(imagePreviousAction);
98 btnPrevious.setPreferredSize(buttonDim);
99 btnPrevious.setEnabled(false);
100
101 JButton btnDelete = new JButton(imageRemoveAction);
102 btnDelete.setPreferredSize(buttonDim);
103
104 JButton btnDeleteFromDisk = new JButton(imageRemoveFromDiskAction);
105 btnDeleteFromDisk.setPreferredSize(buttonDim);
106
107 JButton btnCopyPath = new JButton(imageCopyPathAction);
108 btnCopyPath.setPreferredSize(buttonDim);
109
110 btnNext = new JButton(imageNextAction);
111 btnNext.setPreferredSize(buttonDim);
112 btnNext.setEnabled(false);
113
114 tbCentre = new JToggleButton(imageCenterViewAction);
115 tbCentre.setPreferredSize(buttonDim);
116
117 JButton btnZoomBestFit = new JButton(imageZoomAction);
118 btnZoomBestFit.setPreferredSize(buttonDim);
119
120 btnCollapse = new JButton(imageCollapseAction);
121 btnCollapse.setPreferredSize(new Dimension(20, 20));
122 btnCollapse.setAlignmentY(Component.TOP_ALIGNMENT);
123
124 JPanel buttons = new JPanel();
125 buttons.add(btnPrevious);
126 buttons.add(btnNext);
127 buttons.add(Box.createRigidArea(new Dimension(7, 0)));
128 buttons.add(tbCentre);
129 buttons.add(btnZoomBestFit);
130 buttons.add(Box.createRigidArea(new Dimension(7, 0)));
131 buttons.add(btnDelete);
132 buttons.add(btnDeleteFromDisk);
133 buttons.add(Box.createRigidArea(new Dimension(7, 0)));
134 buttons.add(btnCopyPath);
135
136 JPanel bottomPane = new JPanel(new GridBagLayout());
137 GridBagConstraints gc = new GridBagConstraints();
138 gc.gridx = 0;
139 gc.gridy = 0;
140 gc.anchor = GridBagConstraints.CENTER;
141 gc.weightx = 1;
142 bottomPane.add(buttons, gc);
143
144 gc.gridx = 1;
145 gc.gridy = 0;
146 gc.anchor = GridBagConstraints.PAGE_END;
147 gc.weightx = 0;
148 bottomPane.add(btnCollapse, gc);
149
150 content.add(bottomPane, BorderLayout.SOUTH);
151
152 createLayout(content, false, null);
153 }
154
155 @Override
156 public void destroy() {
157 MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
158 MainApplication.getLayerManager().removeLayerChangeListener(this);
159 // Manually destroy actions until JButtons are replaced by standard SideButtons
160 imageFirstAction.destroy();
161 imageLastAction.destroy();
162 imagePreviousAction.destroy();
163 imageNextAction.destroy();
164 imageCenterViewAction.destroy();
165 imageCollapseAction.destroy();
166 imageCopyPathAction.destroy();
167 imageRemoveAction.destroy();
168 imageRemoveFromDiskAction.destroy();
169 imageZoomAction.destroy();
170 super.destroy();
171 dialog = null;
172 }
173
174 private class ImageNextAction extends JosmAction {
175 ImageNextAction() {
176 super(null, new ImageProvider("dialogs", "next"), tr("Next"), Shortcut.registerShortcut(
177 "geoimage:next", tr("Geoimage: {0}", tr("Show next Image")), KeyEvent.VK_PAGE_DOWN, Shortcut.DIRECT),
178 false, null, false);
179 }
180
181 @Override
182 public void actionPerformed(ActionEvent e) {
183 if (currentLayer != null) {
184 currentLayer.showNextPhoto();
185 }
186 }
187 }
188
189 private class ImagePreviousAction extends JosmAction {
190 ImagePreviousAction() {
191 super(null, new ImageProvider("dialogs", "previous"), tr("Previous"), Shortcut.registerShortcut(
192 "geoimage:previous", tr("Geoimage: {0}", tr("Show previous Image")), KeyEvent.VK_PAGE_UP, Shortcut.DIRECT),
193 false, null, false);
194 }
195
196 @Override
197 public void actionPerformed(ActionEvent e) {
198 if (currentLayer != null) {
199 currentLayer.showPreviousPhoto();
200 }
201 }
202 }
203
204 private class ImageFirstAction extends JosmAction {
205 ImageFirstAction() {
206 super(null, (ImageProvider) null, null, Shortcut.registerShortcut(
207 "geoimage:first", tr("Geoimage: {0}", tr("Show first Image")), KeyEvent.VK_HOME, Shortcut.DIRECT),
208 false, null, false);
209 }
210
211 @Override
212 public void actionPerformed(ActionEvent e) {
213 if (currentLayer != null) {
214 currentLayer.showFirstPhoto();
215 }
216 }
217 }
218
219 private class ImageLastAction extends JosmAction {
220 ImageLastAction() {
221 super(null, (ImageProvider) null, null, Shortcut.registerShortcut(
222 "geoimage:last", tr("Geoimage: {0}", tr("Show last Image")), KeyEvent.VK_END, Shortcut.DIRECT),
223 false, null, false);
224 }
225
226 @Override
227 public void actionPerformed(ActionEvent e) {
228 if (currentLayer != null) {
229 currentLayer.showLastPhoto();
230 }
231 }
232 }
233
234 private class ImageCenterViewAction extends JosmAction {
235 ImageCenterViewAction() {
236 super(null, new ImageProvider("dialogs", "centreview"), tr("Center view"), null,
237 false, null, false);
238 }
239
240 @Override
241 public void actionPerformed(ActionEvent e) {
242 final JToggleButton button = (JToggleButton) e.getSource();
243 centerView = button.isEnabled() && button.isSelected();
244 if (centerView && currentEntry != null && currentEntry.getPos() != null) {
245 MainApplication.getMap().mapView.zoomTo(currentEntry.getPos());
246 }
247 }
248 }
249
250 private class ImageZoomAction extends JosmAction {
251 ImageZoomAction() {
252 super(null, new ImageProvider("dialogs", "zoom-best-fit"), tr("Zoom best fit and 1:1"), null,
253 false, null, false);
254 }
255
256 @Override
257 public void actionPerformed(ActionEvent e) {
258 imgDisplay.zoomBestFitOrOne();
259 }
260 }
261
262 private class ImageRemoveAction extends JosmAction {
263 ImageRemoveAction() {
264 super(null, new ImageProvider("dialogs", "delete"), tr("Remove photo from layer"), Shortcut.registerShortcut(
265 "geoimage:deleteimagefromlayer", tr("Geoimage: {0}", tr("Remove photo from layer")), KeyEvent.VK_DELETE, Shortcut.SHIFT),
266 false, null, false);
267 }
268
269 @Override
270 public void actionPerformed(ActionEvent e) {
271 if (currentLayer != null) {
272 currentLayer.removeCurrentPhoto();
273 }
274 }
275 }
276
277 private class ImageRemoveFromDiskAction extends JosmAction {
278 ImageRemoveFromDiskAction() {
279 super(null, new ImageProvider("dialogs", "geoimage/deletefromdisk"), tr("Delete image file from disk"),
280 Shortcut.registerShortcut(
281 "geoimage:deletefilefromdisk", tr("Geoimage: {0}", tr("Delete File from disk")), KeyEvent.VK_DELETE, Shortcut.CTRL_SHIFT),
282 false, null, false);
283 }
284
285 @Override
286 public void actionPerformed(ActionEvent e) {
287 if (currentLayer != null) {
288 currentLayer.removeCurrentPhotoFromDisk();
289 }
290 }
291 }
292
293 private class ImageCopyPathAction extends JosmAction {
294 ImageCopyPathAction() {
295 super(null, new ImageProvider("copy"), tr("Copy image path"), Shortcut.registerShortcut(
296 "geoimage:copypath", tr("Geoimage: {0}", tr("Copy image path")), KeyEvent.VK_C, Shortcut.ALT_CTRL_SHIFT),
297 false, null, false);
298 }
299
300 @Override
301 public void actionPerformed(ActionEvent e) {
302 if (currentLayer != null) {
303 currentLayer.copyCurrentPhotoPath();
304 }
305 }
306 }
307
308 private class ImageCollapseAction extends JosmAction {
309 ImageCollapseAction() {
310 super(null, new ImageProvider("dialogs", "collapse"), tr("Move dialog to the side pane"), null,
311 false, null, false);
312 }
313
314 @Override
315 public void actionPerformed(ActionEvent e) {
316 collapseButtonClicked = true;
317 detachedDialog.getToolkit().getSystemEventQueue().postEvent(new WindowEvent(detachedDialog, WindowEvent.WINDOW_CLOSING));
318 }
319 }
320
321 public static void showImage(GeoImageLayer layer, ImageEntry entry) {
322 getInstance().displayImage(layer, entry);
323 if (layer != null) {
324 layer.checkPreviousNextButtons();
325 } else {
326 setPreviousEnabled(false);
327 setNextEnabled(false);
328 }
329 }
330
331 /**
332 * Enables (or disables) the "Previous" button.
333 * @param value {@code true} to enable the button, {@code false} otherwise
334 */
335 public static void setPreviousEnabled(boolean value) {
336 getInstance().btnPrevious.setEnabled(value);
337 }
338
339 /**
340 * Enables (or disables) the "Next" button.
341 * @param value {@code true} to enable the button, {@code false} otherwise
342 */
343 public static void setNextEnabled(boolean value) {
344 getInstance().btnNext.setEnabled(value);
345 }
346
347 /**
348 * Enables (or disables) the "Center view" button.
349 * @param value {@code true} to enable the button, {@code false} otherwise
350 * @return the old enabled value. Can be used to restore the original enable state
351 */
352 public static synchronized boolean setCentreEnabled(boolean value) {
353 final ImageViewerDialog instance = getInstance();
354 final boolean wasEnabled = instance.tbCentre.isEnabled();
355 instance.tbCentre.setEnabled(value);
356 instance.tbCentre.getAction().actionPerformed(new ActionEvent(instance.tbCentre, 0, null));
357 return wasEnabled;
358 }
359
360 private transient GeoImageLayer currentLayer;
361 private transient ImageEntry currentEntry;
362
363 public void displayImage(GeoImageLayer layer, ImageEntry entry) {
364 boolean imageChanged;
365
366 synchronized (this) {
367 // TODO: pop up image dialog but don't load image again
368
369 imageChanged = currentEntry != entry;
370
371 if (centerView && entry != null && MainApplication.isDisplayingMapView() && entry.getPos() != null) {
372 MainApplication.getMap().mapView.zoomTo(entry.getPos());
373 }
374
375 currentLayer = layer;
376 currentEntry = entry;
377 }
378
379 if (entry != null) {
380 if (imageChanged) {
381 // Set only if the image is new to preserve zoom and position if the same image is redisplayed
382 // (e.g. to update the OSD).
383 imgDisplay.setImage(entry);
384 }
385 setTitle(tr("Geotagged Images") + (entry.getFile() != null ? " - " + entry.getFile().getName() : ""));
386 StringBuilder osd = new StringBuilder(entry.getFile() != null ? entry.getFile().getName() : "");
387 if (entry.getElevation() != null) {
388 osd.append(tr("\nAltitude: {0} m", Math.round(entry.getElevation())));
389 }
390 if (entry.getSpeed() != null) {
391 osd.append(tr("\nSpeed: {0} km/h", Math.round(entry.getSpeed())));
392 }
393 if (entry.getExifImgDir() != null) {
394 osd.append(tr("\nDirection {0}\u00b0", Math.round(entry.getExifImgDir())));
395 }
396 DateFormat dtf = DateUtils.getDateTimeFormat(DateFormat.SHORT, DateFormat.MEDIUM);
397 // Make sure date/time format includes milliseconds
398 if (dtf instanceof SimpleDateFormat) {
399 String pattern = ((SimpleDateFormat) dtf).toPattern();
400 if (!pattern.contains(".SSS")) {
401 dtf = new SimpleDateFormat(pattern.replace(":ss", ":ss.SSS"));
402 }
403 }
404 if (entry.hasExifTime()) {
405 osd.append(tr("\nEXIF time: {0}", dtf.format(entry.getExifTime())));
406 }
407 if (entry.hasGpsTime()) {
408 osd.append(tr("\nGPS time: {0}", dtf.format(entry.getGpsTime())));
409 }
410
411 imgDisplay.setOsdText(osd.toString());
412 } else {
413 // if this method is called to reinitialize dialog content with a blank image,
414 // do not actually show the dialog again with a blank image if currently hidden (fix #10672)
415 setTitle(tr("Geotagged Images"));
416 imgDisplay.setImage(null);
417 imgDisplay.setOsdText("");
418 return;
419 }
420 if (!isDialogShowing()) {
421 setIsDocked(false); // always open a detached window when an image is clicked and dialog is closed
422 showDialog();
423 } else {
424 if (isDocked && isCollapsed) {
425 expand();
426 dialogsPanel.reconstruct(Action.COLLAPSED_TO_DEFAULT, this);
427 }
428 }
429 }
430
431 /**
432 * When an image is closed, really close it and do not pop
433 * up the side dialog.
434 */
435 @Override
436 protected boolean dockWhenClosingDetachedDlg() {
437 if (collapseButtonClicked) {
438 collapseButtonClicked = false;
439 return true;
440 }
441 return false;
442 }
443
444 @Override
445 protected void stateChanged() {
446 super.stateChanged();
447 if (btnCollapse != null) {
448 btnCollapse.setVisible(!isDocked);
449 }
450 }
451
452 /**
453 * Returns whether an image is currently displayed
454 * @return If image is currently displayed
455 */
456 public boolean hasImage() {
457 return currentEntry != null;
458 }
459
460 /**
461 * Returns the currently displayed image.
462 * @return Currently displayed image or {@code null}
463 * @since 6392
464 */
465 public static ImageEntry getCurrentImage() {
466 return getInstance().currentEntry;
467 }
468
469 /**
470 * Returns the layer associated with the image.
471 * @return Layer associated with the image
472 * @since 6392
473 */
474 public static GeoImageLayer getCurrentLayer() {
475 return getInstance().currentLayer;
476 }
477
478 /**
479 * Returns whether the center view is currently active.
480 * @return {@code true} if the center view is active, {@code false} otherwise
481 * @since 9416
482 */
483 public static boolean isCenterView() {
484 return getInstance().centerView;
485 }
486
487 @Override
488 public void layerAdded(LayerAddEvent e) {
489 showLayer(e.getAddedLayer());
490 }
491
492 @Override
493 public void layerRemoving(LayerRemoveEvent e) {
494 // Clear current image and layer if current layer is deleted
495 if (currentLayer != null && currentLayer.equals(e.getRemovedLayer())) {
496 showImage(null, null);
497 }
498 // Check buttons state in case of layer merging
499 if (currentLayer != null && e.getRemovedLayer() instanceof GeoImageLayer) {
500 currentLayer.checkPreviousNextButtons();
501 }
502 }
503
504 @Override
505 public void layerOrderChanged(LayerOrderChangeEvent e) {
506 // ignored
507 }
508
509 @Override
510 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
511 showLayer(e.getSource().getActiveLayer());
512 }
513
514 private void showLayer(Layer newLayer) {
515 if (currentLayer == null && newLayer instanceof GeoImageLayer) {
516 ((GeoImageLayer) newLayer).showFirstPhoto();
517 }
518 }
519}
Note: See TracBrowser for help on using the repository browser.