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

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

fix #15488 - display SubSecTimeOriginal on picture's information overlay

  • 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.AbstractAction;
18import javax.swing.Box;
19import javax.swing.ImageIcon;
20import javax.swing.JButton;
21import javax.swing.JComponent;
22import javax.swing.JPanel;
23import javax.swing.JToggleButton;
24
25import org.openstreetmap.josm.gui.MainApplication;
26import org.openstreetmap.josm.gui.dialogs.DialogsPanel.Action;
27import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
28import org.openstreetmap.josm.gui.layer.Layer;
29import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
30import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
31import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
32import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
33import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
34import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
35import org.openstreetmap.josm.tools.ImageProvider;
36import org.openstreetmap.josm.tools.Shortcut;
37import org.openstreetmap.josm.tools.date.DateUtils;
38
39/**
40 * Dialog to view and manipulate geo-tagged images from a {@link GeoImageLayer}.
41 */
42public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener {
43
44 private static final String COMMAND_ZOOM = "zoom";
45 private static final String COMMAND_CENTERVIEW = "centre";
46 private static final String COMMAND_NEXT = "next";
47 private static final String COMMAND_REMOVE = "remove";
48 private static final String COMMAND_REMOVE_FROM_DISK = "removefromdisk";
49 private static final String COMMAND_PREVIOUS = "previous";
50 private static final String COMMAND_COLLAPSE = "collapse";
51 private static final String COMMAND_FIRST = "first";
52 private static final String COMMAND_LAST = "last";
53 private static final String COMMAND_COPY_PATH = "copypath";
54
55 private final ImageDisplay imgDisplay = new ImageDisplay();
56 private boolean centerView;
57
58 // Only one instance of that class is present at one time
59 private static volatile ImageViewerDialog dialog;
60
61 private boolean collapseButtonClicked;
62
63 static void newInstance() {
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 ImageAction prevAction = new ImageAction(COMMAND_PREVIOUS, ImageProvider.get("dialogs", "previous"), tr("Previous"));
98 btnPrevious = new JButton(prevAction);
99 btnPrevious.setPreferredSize(buttonDim);
100 Shortcut scPrev = Shortcut.registerShortcut(
101 "geoimage:previous", tr("Geoimage: {0}", tr("Show previous Image")), KeyEvent.VK_PAGE_UP, Shortcut.DIRECT);
102 final String previousImage = "Previous Image";
103 MainApplication.registerActionShortcut(prevAction, scPrev);
104 btnPrevious.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scPrev.getKeyStroke(), previousImage);
105 btnPrevious.getActionMap().put(previousImage, prevAction);
106 btnPrevious.setEnabled(false);
107
108 final String removePhoto = tr("Remove photo from layer");
109 ImageAction delAction = new ImageAction(COMMAND_REMOVE, ImageProvider.get("dialogs", "delete"), removePhoto);
110 JButton btnDelete = new JButton(delAction);
111 btnDelete.setPreferredSize(buttonDim);
112 Shortcut scDelete = Shortcut.registerShortcut(
113 "geoimage:deleteimagefromlayer", tr("Geoimage: {0}", tr("Remove photo from layer")), KeyEvent.VK_DELETE, Shortcut.SHIFT);
114 MainApplication.registerActionShortcut(delAction, scDelete);
115 btnDelete.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scDelete.getKeyStroke(), removePhoto);
116 btnDelete.getActionMap().put(removePhoto, delAction);
117
118 ImageAction delFromDiskAction = new ImageAction(COMMAND_REMOVE_FROM_DISK,
119 ImageProvider.get("dialogs", "geoimage/deletefromdisk"), tr("Delete image file from disk"));
120 JButton btnDeleteFromDisk = new JButton(delFromDiskAction);
121 btnDeleteFromDisk.setPreferredSize(buttonDim);
122 Shortcut scDeleteFromDisk = Shortcut.registerShortcut(
123 "geoimage:deletefilefromdisk", tr("Geoimage: {0}", tr("Delete File from disk")), KeyEvent.VK_DELETE, Shortcut.CTRL_SHIFT);
124 final String deleteImage = "Delete image file from disk";
125 MainApplication.registerActionShortcut(delFromDiskAction, scDeleteFromDisk);
126 btnDeleteFromDisk.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scDeleteFromDisk.getKeyStroke(), deleteImage);
127 btnDeleteFromDisk.getActionMap().put(deleteImage, delFromDiskAction);
128
129 ImageAction copyPathAction = new ImageAction(COMMAND_COPY_PATH, ImageProvider.get("copy"), tr("Copy image path"));
130 JButton btnCopyPath = new JButton(copyPathAction);
131 btnCopyPath.setPreferredSize(buttonDim);
132 Shortcut scCopyPath = Shortcut.registerShortcut(
133 "geoimage:copypath", tr("Geoimage: {0}", tr("Copy image path")), KeyEvent.VK_C, Shortcut.ALT_CTRL_SHIFT);
134 final String copyImage = "Copy image path";
135 MainApplication.registerActionShortcut(copyPathAction, scCopyPath);
136 btnCopyPath.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scCopyPath.getKeyStroke(), copyImage);
137 btnCopyPath.getActionMap().put(copyImage, copyPathAction);
138
139 ImageAction nextAction = new ImageAction(COMMAND_NEXT, ImageProvider.get("dialogs", "next"), tr("Next"));
140 btnNext = new JButton(nextAction);
141 btnNext.setPreferredSize(buttonDim);
142 Shortcut scNext = Shortcut.registerShortcut(
143 "geoimage:next", tr("Geoimage: {0}", tr("Show next Image")), KeyEvent.VK_PAGE_DOWN, Shortcut.DIRECT);
144 final String nextImage = "Next Image";
145 MainApplication.registerActionShortcut(nextAction, scNext);
146 btnNext.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scNext.getKeyStroke(), nextImage);
147 btnNext.getActionMap().put(nextImage, nextAction);
148 btnNext.setEnabled(false);
149
150 MainApplication.registerActionShortcut(
151 new ImageAction(COMMAND_FIRST, null, null),
152 Shortcut.registerShortcut(
153 "geoimage:first", tr("Geoimage: {0}", tr("Show first Image")), KeyEvent.VK_HOME, Shortcut.DIRECT)
154 );
155 MainApplication.registerActionShortcut(
156 new ImageAction(COMMAND_LAST, null, null),
157 Shortcut.registerShortcut(
158 "geoimage:last", tr("Geoimage: {0}", tr("Show last Image")), KeyEvent.VK_END, Shortcut.DIRECT)
159 );
160
161 tbCentre = new JToggleButton(new ImageAction(COMMAND_CENTERVIEW,
162 ImageProvider.get("dialogs", "centreview"), tr("Center view")));
163 tbCentre.setPreferredSize(buttonDim);
164
165 JButton btnZoomBestFit = new JButton(new ImageAction(COMMAND_ZOOM,
166 ImageProvider.get("dialogs", "zoom-best-fit"), tr("Zoom best fit and 1:1")));
167 btnZoomBestFit.setPreferredSize(buttonDim);
168
169 btnCollapse = new JButton(new ImageAction(COMMAND_COLLAPSE,
170 ImageProvider.get("dialogs", "collapse"), tr("Move dialog to the side pane")));
171 btnCollapse.setPreferredSize(new Dimension(20, 20));
172 btnCollapse.setAlignmentY(Component.TOP_ALIGNMENT);
173
174 JPanel buttons = new JPanel();
175 buttons.add(btnPrevious);
176 buttons.add(btnNext);
177 buttons.add(Box.createRigidArea(new Dimension(7, 0)));
178 buttons.add(tbCentre);
179 buttons.add(btnZoomBestFit);
180 buttons.add(Box.createRigidArea(new Dimension(7, 0)));
181 buttons.add(btnDelete);
182 buttons.add(btnDeleteFromDisk);
183 buttons.add(Box.createRigidArea(new Dimension(7, 0)));
184 buttons.add(btnCopyPath);
185
186 JPanel bottomPane = new JPanel(new GridBagLayout());
187 GridBagConstraints gc = new GridBagConstraints();
188 gc.gridx = 0;
189 gc.gridy = 0;
190 gc.anchor = GridBagConstraints.CENTER;
191 gc.weightx = 1;
192 bottomPane.add(buttons, gc);
193
194 gc.gridx = 1;
195 gc.gridy = 0;
196 gc.anchor = GridBagConstraints.PAGE_END;
197 gc.weightx = 0;
198 bottomPane.add(btnCollapse, gc);
199
200 content.add(bottomPane, BorderLayout.SOUTH);
201
202 createLayout(content, false, null);
203 }
204
205 @Override
206 public void destroy() {
207 MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
208 MainApplication.getLayerManager().removeLayerChangeListener(this);
209 super.destroy();
210 }
211
212 class ImageAction extends AbstractAction {
213 private final String action;
214
215 ImageAction(String action, ImageIcon icon, String toolTipText) {
216 this.action = action;
217 putValue(SHORT_DESCRIPTION, toolTipText);
218 putValue(SMALL_ICON, icon);
219 }
220
221 @Override
222 public void actionPerformed(ActionEvent e) {
223 if (COMMAND_NEXT.equals(action)) {
224 if (currentLayer != null) {
225 currentLayer.showNextPhoto();
226 }
227 } else if (COMMAND_PREVIOUS.equals(action)) {
228 if (currentLayer != null) {
229 currentLayer.showPreviousPhoto();
230 }
231 } else if (COMMAND_FIRST.equals(action) && currentLayer != null) {
232 currentLayer.showFirstPhoto();
233 } else if (COMMAND_LAST.equals(action) && currentLayer != null) {
234 currentLayer.showLastPhoto();
235 } else if (COMMAND_CENTERVIEW.equals(action)) {
236 final JToggleButton button = (JToggleButton) e.getSource();
237 centerView = button.isEnabled() && button.isSelected();
238 if (centerView && currentEntry != null && currentEntry.getPos() != null) {
239 MainApplication.getMap().mapView.zoomTo(currentEntry.getPos());
240 }
241 } else if (COMMAND_ZOOM.equals(action)) {
242 imgDisplay.zoomBestFitOrOne();
243 } else if (COMMAND_REMOVE.equals(action)) {
244 if (currentLayer != null) {
245 currentLayer.removeCurrentPhoto();
246 }
247 } else if (COMMAND_REMOVE_FROM_DISK.equals(action)) {
248 if (currentLayer != null) {
249 currentLayer.removeCurrentPhotoFromDisk();
250 }
251 } else if (COMMAND_COPY_PATH.equals(action)) {
252 if (currentLayer != null) {
253 currentLayer.copyCurrentPhotoPath();
254 }
255 } else if (COMMAND_COLLAPSE.equals(action)) {
256 collapseButtonClicked = true;
257 detachedDialog.getToolkit().getSystemEventQueue().postEvent(new WindowEvent(detachedDialog, WindowEvent.WINDOW_CLOSING));
258 }
259 }
260 }
261
262 public static void showImage(GeoImageLayer layer, ImageEntry entry) {
263 getInstance().displayImage(layer, entry);
264 if (layer != null) {
265 layer.checkPreviousNextButtons();
266 } else {
267 setPreviousEnabled(false);
268 setNextEnabled(false);
269 }
270 }
271
272 /**
273 * Enables (or disables) the "Previous" button.
274 * @param value {@code true} to enable the button, {@code false} otherwise
275 */
276 public static void setPreviousEnabled(boolean value) {
277 getInstance().btnPrevious.setEnabled(value);
278 }
279
280 /**
281 * Enables (or disables) the "Next" button.
282 * @param value {@code true} to enable the button, {@code false} otherwise
283 */
284 public static void setNextEnabled(boolean value) {
285 getInstance().btnNext.setEnabled(value);
286 }
287
288 /**
289 * Enables (or disables) the "Center view" button.
290 * @param value {@code true} to enable the button, {@code false} otherwise
291 * @return the old enabled value. Can be used to restore the original enable state
292 */
293 public static synchronized boolean setCentreEnabled(boolean value) {
294 final ImageViewerDialog instance = getInstance();
295 final boolean wasEnabled = instance.tbCentre.isEnabled();
296 instance.tbCentre.setEnabled(value);
297 instance.tbCentre.getAction().actionPerformed(new ActionEvent(instance.tbCentre, 0, null));
298 return wasEnabled;
299 }
300
301 private transient GeoImageLayer currentLayer;
302 private transient ImageEntry currentEntry;
303
304 public void displayImage(GeoImageLayer layer, ImageEntry entry) {
305 boolean imageChanged;
306
307 synchronized (this) {
308 // TODO: pop up image dialog but don't load image again
309
310 imageChanged = currentEntry != entry;
311
312 if (centerView && entry != null && MainApplication.isDisplayingMapView() && entry.getPos() != null) {
313 MainApplication.getMap().mapView.zoomTo(entry.getPos());
314 }
315
316 currentLayer = layer;
317 currentEntry = entry;
318 }
319
320 if (entry != null) {
321 if (imageChanged) {
322 // Set only if the image is new to preserve zoom and position if the same image is redisplayed
323 // (e.g. to update the OSD).
324 imgDisplay.setImage(entry.getFile(), entry.getExifOrientation());
325 }
326 setTitle(tr("Geotagged Images") + (entry.getFile() != null ? " - " + entry.getFile().getName() : ""));
327 StringBuilder osd = new StringBuilder(entry.getFile() != null ? entry.getFile().getName() : "");
328 if (entry.getElevation() != null) {
329 osd.append(tr("\nAltitude: {0} m", Math.round(entry.getElevation())));
330 }
331 if (entry.getSpeed() != null) {
332 osd.append(tr("\nSpeed: {0} km/h", Math.round(entry.getSpeed())));
333 }
334 if (entry.getExifImgDir() != null) {
335 osd.append(tr("\nDirection {0}\u00b0", Math.round(entry.getExifImgDir())));
336 }
337 DateFormat dtf = DateUtils.getDateTimeFormat(DateFormat.SHORT, DateFormat.MEDIUM);
338 // Make sure date/time format includes milliseconds
339 if (dtf instanceof SimpleDateFormat) {
340 String pattern = ((SimpleDateFormat) dtf).toPattern();
341 if (!pattern.contains(".SSS")) {
342 dtf = new SimpleDateFormat(pattern.replace(":ss", ":ss.SSS"));
343 }
344 }
345 if (entry.hasExifTime()) {
346 osd.append(tr("\nEXIF time: {0}", dtf.format(entry.getExifTime())));
347 }
348 if (entry.hasGpsTime()) {
349 osd.append(tr("\nGPS time: {0}", dtf.format(entry.getGpsTime())));
350 }
351
352 imgDisplay.setOsdText(osd.toString());
353 } else {
354 // if this method is called to reinitialize dialog content with a blank image,
355 // do not actually show the dialog again with a blank image if currently hidden (fix #10672)
356 setTitle(tr("Geotagged Images"));
357 imgDisplay.setImage(null, null);
358 imgDisplay.setOsdText("");
359 return;
360 }
361 if (!isDialogShowing()) {
362 setIsDocked(false); // always open a detached window when an image is clicked and dialog is closed
363 showDialog();
364 } else {
365 if (isDocked && isCollapsed) {
366 expand();
367 dialogsPanel.reconstruct(Action.COLLAPSED_TO_DEFAULT, this);
368 }
369 }
370 }
371
372 /**
373 * When an image is closed, really close it and do not pop
374 * up the side dialog.
375 */
376 @Override
377 protected boolean dockWhenClosingDetachedDlg() {
378 if (collapseButtonClicked) {
379 collapseButtonClicked = false;
380 return true;
381 }
382 return false;
383 }
384
385 @Override
386 protected void stateChanged() {
387 super.stateChanged();
388 if (btnCollapse != null) {
389 btnCollapse.setVisible(!isDocked);
390 }
391 }
392
393 /**
394 * Returns whether an image is currently displayed
395 * @return If image is currently displayed
396 */
397 public boolean hasImage() {
398 return currentEntry != null;
399 }
400
401 /**
402 * Returns the currently displayed image.
403 * @return Currently displayed image or {@code null}
404 * @since 6392
405 */
406 public static ImageEntry getCurrentImage() {
407 return getInstance().currentEntry;
408 }
409
410 /**
411 * Returns the layer associated with the image.
412 * @return Layer associated with the image
413 * @since 6392
414 */
415 public static GeoImageLayer getCurrentLayer() {
416 return getInstance().currentLayer;
417 }
418
419 /**
420 * Returns whether the center view is currently active.
421 * @return {@code true} if the center view is active, {@code false} otherwise
422 * @since 9416
423 */
424 public static boolean isCenterView() {
425 return getInstance().centerView;
426 }
427
428 @Override
429 public void layerAdded(LayerAddEvent e) {
430 showLayer(e.getAddedLayer());
431 }
432
433 @Override
434 public void layerRemoving(LayerRemoveEvent e) {
435 // Clear current image and layer if current layer is deleted
436 if (currentLayer != null && currentLayer.equals(e.getRemovedLayer())) {
437 showImage(null, null);
438 }
439 // Check buttons state in case of layer merging
440 if (currentLayer != null && e.getRemovedLayer() instanceof GeoImageLayer) {
441 currentLayer.checkPreviousNextButtons();
442 }
443 }
444
445 @Override
446 public void layerOrderChanged(LayerOrderChangeEvent e) {
447 // ignored
448 }
449
450 @Override
451 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
452 showLayer(e.getSource().getActiveLayer());
453 }
454
455 private void showLayer(Layer newLayer) {
456 if (currentLayer == null && newLayer instanceof GeoImageLayer) {
457 ((GeoImageLayer) newLayer).showFirstPhoto();
458 }
459 }
460
461}
Note: See TracBrowser for help on using the repository browser.