001 // License: GPL. See LICENSE file for details.
002 // Copyright 2007 by Christian Gallioz (aka khris78)
003 // Parts of code from Geotagged plugin (by Rob Neild)
004 // and the core JOSM source code (by Immanuel Scholz and others)
005
006 package org.openstreetmap.josm.gui.layer.geoimage;
007
008 import static org.openstreetmap.josm.tools.I18n.tr;
009
010 import java.awt.BorderLayout;
011 import java.awt.Component;
012 import java.awt.Dimension;
013 import java.awt.GridBagConstraints;
014 import java.awt.GridBagLayout;
015 import java.awt.event.ActionEvent;
016 import java.awt.event.KeyEvent;
017 import java.awt.event.WindowEvent;
018 import java.text.DateFormat;
019
020 import javax.swing.AbstractAction;
021 import javax.swing.Box;
022 import javax.swing.ImageIcon;
023 import javax.swing.JButton;
024 import javax.swing.JComponent;
025 import javax.swing.JPanel;
026 import javax.swing.JToggleButton;
027
028 import org.openstreetmap.josm.Main;
029 import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
030 import org.openstreetmap.josm.gui.dialogs.DialogsPanel.Action;
031 import org.openstreetmap.josm.tools.ImageProvider;
032 import org.openstreetmap.josm.tools.Shortcut;
033
034 public class ImageViewerDialog extends ToggleDialog {
035
036 private static final String COMMAND_ZOOM = "zoom";
037 private static final String COMMAND_CENTERVIEW = "centre";
038 private static final String COMMAND_NEXT = "next";
039 private static final String COMMAND_REMOVE = "remove";
040 private static final String COMMAND_REMOVE_FROM_DISK = "removefromdisk";
041 private static final String COMMAND_PREVIOUS = "previous";
042 private static final String COMMAND_COLLAPSE = "collapse";
043
044 private ImageDisplay imgDisplay = new ImageDisplay();
045 private boolean centerView = false;
046
047 // Only one instance of that class is present at one time
048 private static ImageViewerDialog dialog;
049
050 private boolean collapseButtonClicked = false;
051
052 static void newInstance() {
053 dialog = new ImageViewerDialog();
054 }
055
056 public static ImageViewerDialog getInstance() {
057 if (dialog == null)
058 throw new AssertionError(); // a new instance needs to be created first
059 return dialog;
060 }
061
062 private JButton btnNext;
063 private JButton btnPrevious;
064 private JButton btnCollapse;
065
066 private ImageViewerDialog() {
067 super(tr("Geotagged Images"), "geoimage", tr("Display geotagged images"), Shortcut.registerShortcut("tools:geotagged",
068 tr("Tool: {0}", tr("Display geotagged images")), KeyEvent.VK_Y, Shortcut.DIRECT), 200);
069
070 /* Don't show a detached dialog right from the start. */
071 if (isShowing && !isDocked) {
072 setIsShowing(false);
073 }
074
075 JPanel content = new JPanel();
076 content.setLayout(new BorderLayout());
077
078 content.add(imgDisplay, BorderLayout.CENTER);
079
080 Dimension buttonDim = new Dimension(26,26);
081
082 ImageAction prevAction = new ImageAction(COMMAND_PREVIOUS, ImageProvider.get("dialogs", "previous"), tr("Previous"));
083 btnPrevious = new JButton(prevAction);
084 btnPrevious.setPreferredSize(buttonDim);
085 Shortcut scPrev = Shortcut.registerShortcut(
086 "geoimage:previous", tr("Geoimage: {0}", tr("Show previous Image")), KeyEvent.VK_PAGE_UP, Shortcut.DIRECT);
087 final String APREVIOUS = "Previous Image";
088 Main.registerActionShortcut(prevAction, scPrev);
089 btnPrevious.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scPrev.getKeyStroke(), APREVIOUS);
090 btnPrevious.getActionMap().put(APREVIOUS, prevAction);
091
092 final String DELETE_TEXT = tr("Remove photo from layer");
093 ImageAction delAction = new ImageAction(COMMAND_REMOVE, ImageProvider.get("dialogs", "delete"), DELETE_TEXT);
094 JButton btnDelete = new JButton(delAction);
095 btnDelete.setPreferredSize(buttonDim);
096 Shortcut scDelete = Shortcut.registerShortcut(
097 "geoimage:deleteimagefromlayer", tr("Geoimage: {0}", tr("Remove photo from layer")), KeyEvent.VK_DELETE, Shortcut.SHIFT);
098 Main.registerActionShortcut(delAction, scDelete);
099 btnDelete.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scDelete.getKeyStroke(), DELETE_TEXT);
100 btnDelete.getActionMap().put(DELETE_TEXT, delAction);
101
102 ImageAction delFromDiskAction = new ImageAction(COMMAND_REMOVE_FROM_DISK, ImageProvider.get("dialogs", "geoimage/deletefromdisk"), tr("Delete image file from disk"));
103 JButton btnDeleteFromDisk = new JButton(delFromDiskAction);
104 btnDeleteFromDisk.setPreferredSize(buttonDim);
105 Shortcut scDeleteFromDisk = Shortcut.registerShortcut(
106 "geoimage:deletefilefromdisk", tr("Geoimage: {0}", tr("Delete File from disk")), KeyEvent.VK_DELETE, Shortcut.CTRL_SHIFT);
107 final String ADELFROMDISK = "Delete image file from disk";
108 Main.registerActionShortcut(delFromDiskAction, scDeleteFromDisk);
109 btnDeleteFromDisk.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scDeleteFromDisk.getKeyStroke(), ADELFROMDISK);
110 btnDeleteFromDisk.getActionMap().put(ADELFROMDISK, delFromDiskAction);
111
112 ImageAction nextAction = new ImageAction(COMMAND_NEXT, ImageProvider.get("dialogs", "next"), tr("Next"));
113 btnNext = new JButton(nextAction);
114 btnNext.setPreferredSize(buttonDim);
115 Shortcut scNext = Shortcut.registerShortcut(
116 "geoimage:next", tr("Geoimage: {0}", tr("Show next Image")), KeyEvent.VK_PAGE_DOWN, Shortcut.DIRECT);
117 final String ANEXT = "Next Image";
118 Main.registerActionShortcut(nextAction, scNext);
119 btnNext.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scNext.getKeyStroke(), ANEXT);
120 btnNext.getActionMap().put(ANEXT, nextAction);
121
122 JToggleButton tbCentre = new JToggleButton(new ImageAction(COMMAND_CENTERVIEW, ImageProvider.get("dialogs", "centreview"), tr("Center view")));
123 tbCentre.setPreferredSize(buttonDim);
124
125 JButton btnZoomBestFit = new JButton(new ImageAction(COMMAND_ZOOM, ImageProvider.get("dialogs", "zoom-best-fit"), tr("Zoom best fit and 1:1")));
126 btnZoomBestFit.setPreferredSize(buttonDim);
127
128 btnCollapse = new JButton(new ImageAction(COMMAND_COLLAPSE, ImageProvider.get("dialogs", "collapse"), tr("Move dialog to the side pane")));
129 btnCollapse.setPreferredSize(new Dimension(20,20));
130 btnCollapse.setAlignmentY(Component.TOP_ALIGNMENT);
131
132 JPanel buttons = new JPanel();
133 buttons.add(btnPrevious);
134 buttons.add(btnNext);
135 buttons.add(Box.createRigidArea(new Dimension(14, 0)));
136 buttons.add(tbCentre);
137 buttons.add(btnZoomBestFit);
138 buttons.add(Box.createRigidArea(new Dimension(14, 0)));
139 buttons.add(btnDelete);
140 buttons.add(btnDeleteFromDisk);
141
142 JPanel bottomPane = new JPanel();
143 bottomPane.setLayout(new GridBagLayout());
144 GridBagConstraints gc = new GridBagConstraints();
145 gc.gridx = 0;
146 gc.gridy = 0;
147 gc.anchor = GridBagConstraints.CENTER;
148 gc.weightx = 1;
149 bottomPane.add(buttons, gc);
150
151 gc.gridx = 1;
152 gc.gridy = 0;
153 gc.anchor = GridBagConstraints.PAGE_END;
154 gc.weightx = 0;
155 bottomPane.add(btnCollapse, gc);
156
157 content.add(bottomPane, BorderLayout.SOUTH);
158
159 add(content, BorderLayout.CENTER);
160 }
161
162 class ImageAction extends AbstractAction {
163 private final String action;
164 public ImageAction(String action, ImageIcon icon, String toolTipText) {
165 this.action = action;
166 putValue(SHORT_DESCRIPTION, toolTipText);
167 putValue(SMALL_ICON, icon);
168 }
169
170 public void actionPerformed(ActionEvent e) {
171 if (COMMAND_NEXT.equals(action)) {
172 if (currentLayer != null) {
173 currentLayer.showNextPhoto();
174 }
175 } else if (COMMAND_PREVIOUS.equals(action)) {
176 if (currentLayer != null) {
177 currentLayer.showPreviousPhoto();
178 }
179
180 } else if (COMMAND_CENTERVIEW.equals(action)) {
181 centerView = ((JToggleButton) e.getSource()).isSelected();
182 if (centerView && currentEntry != null && currentEntry.getPos() != null) {
183 Main.map.mapView.zoomTo(currentEntry.getPos());
184 }
185
186 } else if (COMMAND_ZOOM.equals(action)) {
187 imgDisplay.zoomBestFitOrOne();
188
189 } else if (COMMAND_REMOVE.equals(action)) {
190 if (currentLayer != null) {
191 currentLayer.removeCurrentPhoto();
192 }
193 } else if (COMMAND_REMOVE_FROM_DISK.equals(action)) {
194 if (currentLayer != null) {
195 currentLayer.removeCurrentPhotoFromDisk();
196 }
197 } else if (COMMAND_COLLAPSE.equals(action)) {
198 collapseButtonClicked = true;
199 detachedDialog.getToolkit().getSystemEventQueue().postEvent(new WindowEvent(detachedDialog, WindowEvent.WINDOW_CLOSING));
200 }
201 }
202 }
203
204 public static void showImage(GeoImageLayer layer, ImageEntry entry) {
205 getInstance().displayImage(layer, entry);
206 layer.checkPreviousNextButtons();
207 }
208 public static void setPreviousEnabled(Boolean value) {
209 getInstance().btnPrevious.setEnabled(value);
210 }
211 public static void setNextEnabled(Boolean value) {
212 getInstance().btnNext.setEnabled(value);
213 }
214
215 private GeoImageLayer currentLayer = null;
216 private ImageEntry currentEntry = null;
217
218 public void displayImage(GeoImageLayer layer, ImageEntry entry) {
219 synchronized(this) {
220 // if (currentLayer == layer && currentEntry == entry) {
221 // repaint();
222 // return;
223 // } TODO: pop up image dialog but don't load image again
224
225 if (centerView && Main.isDisplayingMapView() && entry != null && entry.getPos() != null) {
226 Main.map.mapView.zoomTo(entry.getPos());
227 }
228
229 currentLayer = layer;
230 currentEntry = entry;
231 }
232
233 if (entry != null) {
234 imgDisplay.setImage(entry.getFile(), entry.getExifOrientation());
235 setTitle("Geotagged Images" + (entry.getFile() != null ? " - " + entry.getFile().getName() : ""));
236 StringBuffer osd = new StringBuffer(entry.getFile() != null ? entry.getFile().getName() : "");
237 if (entry.getElevation() != null) {
238 osd.append(tr("\nAltitude: {0} m", entry.getElevation().longValue()));
239 }
240 if (entry.getSpeed() != null) {
241 osd.append(tr("\n{0} km/h", Math.round(entry.getSpeed())));
242 }
243 if (entry.getExifImgDir() != null) {
244 osd.append(tr("\nDirection {0}\u00b0", Math.round(entry.getExifImgDir())));
245 }
246 //if (entry.getPos() != null) {
247 // osd.append(tr("\nlat: {0}, lon: {1}", Double.toString(entry.getPos().lat()), Double.toString(entry.getPos().lon())));
248 //}
249 //osd.append(tr("\nfile mtime: {0}", Long.toString(entry.getFile().lastModified())));
250 DateFormat dtf = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
251 if (entry.getExifTime() != null) {
252 osd.append(tr("\nEXIF time: {0}", dtf.format(entry.getExifTime())));
253 }
254 if (entry.getGpsTime() != null) {
255 osd.append(tr("\nGPS time: {0}", dtf.format(entry.getGpsTime())));
256 }
257
258 imgDisplay.setOsdText(osd.toString());
259 } else {
260 imgDisplay.setImage(null, null);
261 imgDisplay.setOsdText("");
262 }
263 if (! isDialogShowing()) {
264 setIsDocked(false); // always open a detached window when an image is clicked and dialog is closed
265 showDialog();
266 } else {
267 if (isDocked && isCollapsed) {
268 expand();
269 dialogsPanel.reconstruct(Action.COLLAPSED_TO_DEFAULT, this);
270 }
271 }
272
273 }
274
275 /**
276 * When pressing the Toggle button always show the docked dialog.
277 */
278 @Override
279 protected void toggleButtonHook() {
280 if (! isShowing) {
281 setIsDocked(true);
282 setIsCollapsed(false);
283 }
284 }
285
286 /**
287 * When an image is closed, really close it and do not pop
288 * up the side dialog.
289 */
290 @Override
291 protected boolean dockWhenClosingDetachedDlg() {
292 if (collapseButtonClicked) {
293 collapseButtonClicked = false;
294 return true;
295 }
296 return false;
297 }
298
299 @Override
300 protected void stateChanged() {
301 super.stateChanged();
302 if (btnCollapse != null) {
303 btnCollapse.setVisible(!isDocked);
304 }
305 }
306
307 /**
308 * Returns whether an image is currently displayed
309 * @return If image is currently displayed
310 */
311 public boolean hasImage() {
312 return currentEntry != null;
313 }
314 }