source: josm/trunk/src/org/openstreetmap/josm/gui/layer/GeoImageLayer.java@ 321

Last change on this file since 321 was 319, checked in by imi, 18 years ago
  • removed MinML2 dependency (use javax.xml)
  • fixed reorder action (thanks Robert)
  • added backup files before saving (thanks Dave)
  • added search for last modifying user (thanks Dave)
  • fixed import of plugin list and added author field (thanks Shaun)
File size: 17.1 KB
Line 
1//License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.gui.layer;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.BorderLayout;
8import java.awt.Color;
9import java.awt.Component;
10import java.awt.Cursor;
11import java.awt.Graphics;
12import java.awt.GridBagLayout;
13import java.awt.Image;
14import java.awt.Insets;
15import java.awt.Point;
16import java.awt.Rectangle;
17import java.awt.event.ActionEvent;
18import java.awt.event.ActionListener;
19import java.awt.event.MouseAdapter;
20import java.awt.event.MouseEvent;
21import java.awt.event.KeyEvent;
22import java.io.File;
23import java.io.IOException;
24import java.text.ParseException;
25import java.text.SimpleDateFormat;
26import java.util.ArrayList;
27import java.util.Collection;
28import java.util.Collections;
29import java.util.Date;
30import java.util.LinkedList;
31
32import javax.swing.BorderFactory;
33import javax.swing.DefaultListCellRenderer;
34import javax.swing.Icon;
35import javax.swing.ImageIcon;
36import javax.swing.JDialog;
37import javax.swing.JFileChooser;
38import javax.swing.JLabel;
39import javax.swing.JList;
40import javax.swing.JMenuItem;
41import javax.swing.JOptionPane;
42import javax.swing.JPanel;
43import javax.swing.JScrollPane;
44import javax.swing.JSeparator;
45import javax.swing.JTextField;
46import javax.swing.JToggleButton;
47import javax.swing.JButton;
48import javax.swing.JViewport;
49import javax.swing.border.BevelBorder;
50import javax.swing.border.Border;
51import javax.swing.filechooser.FileFilter;
52
53import org.openstreetmap.josm.Main;
54import org.openstreetmap.josm.actions.RenameLayerAction;
55import org.openstreetmap.josm.data.coor.EastNorth;
56import org.openstreetmap.josm.data.coor.LatLon;
57import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
58import org.openstreetmap.josm.gui.MapView;
59import org.openstreetmap.josm.gui.PleaseWaitRunnable;
60import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
61import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
62import org.openstreetmap.josm.gui.layer.RawGpsLayer.GpsPoint;
63import org.openstreetmap.josm.tools.DateParser;
64import org.openstreetmap.josm.tools.ExifReader;
65import org.openstreetmap.josm.tools.GBC;
66import org.openstreetmap.josm.tools.ImageProvider;
67
68/**
69 * A layer which imports several photos from disk and read EXIF time information from them.
70 *
71 * @author Imi
72 */
73public class GeoImageLayer extends Layer {
74
75 private static final class ImageEntry implements Comparable<ImageEntry> {
76 File image;
77 Date time;
78 LatLon coor;
79 EastNorth pos;
80 Icon icon;
81 public int compareTo(ImageEntry image) {
82 return time.compareTo(image.time);
83 }
84 }
85
86 private static final class Loader extends PleaseWaitRunnable {
87 boolean cancelled = false;
88 private GeoImageLayer layer;
89 private final Collection<File> files;
90 private final RawGpsLayer gpsLayer;
91 public Loader(Collection<File> files, RawGpsLayer gpsLayer) {
92 super(tr("Images for {0}", gpsLayer.name));
93 this.files = files;
94 this.gpsLayer = gpsLayer;
95 }
96 @Override protected void realRun() throws IOException {
97 Main.pleaseWaitDlg.currentAction.setText(tr("Read GPS..."));
98 LinkedList<TimedPoint> gps = new LinkedList<TimedPoint>();
99
100 // Extract dates and locations from GPS input
101 for (Collection<GpsPoint> c : gpsLayer.data) {
102 for (GpsPoint p : c) {
103 if (p.time == null)
104 throw new IOException(tr("No time for point {0} x {1}",p.latlon.lat(),p.latlon.lon()));
105 Date d = null;
106 try {
107 d = DateParser.parse(p.time);
108 } catch (ParseException e) {
109 throw new IOException(tr("Cannot read time \"{0}\" from point {1} x {2}",p.time,p.latlon.lat(),p.latlon.lon()));
110 }
111 gps.add(new TimedPoint(d, p.eastNorth));
112 }
113 }
114
115 if (gps.isEmpty()) {
116 errorMessage = tr("No images with readable timestamps found.");
117 return;
118 }
119
120 // read the image files
121 ArrayList<ImageEntry> data = new ArrayList<ImageEntry>(files.size());
122 int i = 0;
123 Main.pleaseWaitDlg.progress.setMaximum(files.size());
124 for (File f : files) {
125 if (cancelled)
126 break;
127 Main.pleaseWaitDlg.currentAction.setText(tr("Reading {0}...",f.getName()));
128 Main.pleaseWaitDlg.progress.setValue(i++);
129
130 ImageEntry e = new ImageEntry();
131 try {
132 e.time = ExifReader.readTime(f);
133 } catch (ParseException e1) {
134 continue;
135 }
136 if (e.time == null)
137 continue;
138 e.image = f;
139 e.icon = loadScaledImage(f, 16);
140
141 data.add(e);
142 }
143 layer = new GeoImageLayer(data, gps);
144 layer.calculatePosition();
145 }
146 @Override protected void finish() {
147 if (layer != null)
148 Main.main.addLayer(layer);
149 }
150 @Override protected void cancel() {cancelled = true;}
151 }
152
153 public ArrayList<ImageEntry> data;
154 private LinkedList<TimedPoint> gps = new LinkedList<TimedPoint>();
155
156 /**
157 * The delta added to all timestamps in files from the camera
158 * to match to the timestamp from the gps receivers tracklog.
159 */
160 private long delta = Long.parseLong(Main.pref.get("tagimages.delta", "0"));
161 private long gpstimezone = Long.parseLong(Main.pref.get("tagimages.gpstimezone", "0"))*60*60*1000;
162 private boolean mousePressed = false;
163 private static final SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
164 private MouseAdapter mouseAdapter;
165 private int currentImage;
166
167 public static final class GpsTimeIncorrect extends Exception {
168 public GpsTimeIncorrect(String message, Throwable cause) {
169 super(message, cause);
170 }
171 public GpsTimeIncorrect(String message) {
172 super(message);
173 }
174 }
175
176 private static final class TimedPoint implements Comparable<TimedPoint> {
177 Date time;
178 EastNorth pos;
179 public TimedPoint(Date time, EastNorth pos) {
180 this.time = time;
181 this.pos = pos;
182 }
183 public int compareTo(TimedPoint point) {
184 return time.compareTo(point.time);
185 }
186 }
187
188 public static void create(Collection<File> files, RawGpsLayer gpsLayer) {
189 Loader loader = new Loader(files, gpsLayer);
190 Main.worker.execute(loader);
191 }
192
193 private GeoImageLayer(final ArrayList<ImageEntry> data, LinkedList<TimedPoint> gps) {
194 super(tr("Geotagged Images"));
195 Collections.sort(data);
196 Collections.sort(gps);
197 this.data = data;
198 this.gps = gps;
199 mouseAdapter = new MouseAdapter(){
200 @Override public void mousePressed(MouseEvent e) {
201 if (e.getButton() != MouseEvent.BUTTON1)
202 return;
203 mousePressed = true;
204 if (visible)
205 Main.map.mapView.repaint();
206 }
207 @Override public void mouseReleased(MouseEvent ev) {
208 if (ev.getButton() != MouseEvent.BUTTON1)
209 return;
210 mousePressed = false;
211 if (!visible)
212 return;
213 for (int i = data.size(); i > 0; --i) {
214 ImageEntry e = data.get(i-1);
215 if (e.pos == null)
216 continue;
217 Point p = Main.map.mapView.getPoint(e.pos);
218 Rectangle r = new Rectangle(p.x-e.icon.getIconWidth()/2, p.y-e.icon.getIconHeight()/2, e.icon.getIconWidth(), e.icon.getIconHeight());
219 if (r.contains(ev.getPoint())) {
220 showImage(i-1);
221 break;
222 }
223 }
224 Main.map.mapView.repaint();
225 }
226 };
227 Main.map.mapView.addMouseListener(mouseAdapter);
228 Layer.listeners.add(new LayerChangeListener(){
229 public void activeLayerChange(Layer oldLayer, Layer newLayer) {}
230 public void layerAdded(Layer newLayer) {}
231 public void layerRemoved(Layer oldLayer) {
232 Main.map.mapView.removeMouseListener(mouseAdapter);
233 }
234 });
235 }
236
237 private void showImage(int i) {
238 currentImage = i;
239 final JPanel p = new JPanel(new BorderLayout());
240 final ImageEntry e = data.get(currentImage);
241 final JScrollPane scroll = new JScrollPane(new JLabel(loadScaledImage(e.image, 580)));
242 final JViewport vp = scroll.getViewport();
243 p.add(scroll, BorderLayout.CENTER);
244
245 final JToggleButton scale = new JToggleButton(ImageProvider.get("dialogs", "zoom-best-fit"));
246 final JButton next = new JButton(ImageProvider.get("dialogs", "next"));
247 final JButton prev = new JButton(ImageProvider.get("dialogs", "previous"));
248 final JToggleButton cent = new JToggleButton(ImageProvider.get("dialogs", "centreview"));
249
250 JPanel p2 = new JPanel();
251 p2.add(prev);
252 p2.add(scale);
253 p2.add(cent);
254 p2.add(next);
255 prev.setEnabled(currentImage>0?true:false);
256 next.setEnabled(currentImage<data.size()-1?true:false);
257 p.add(p2, BorderLayout.SOUTH);
258 final JOptionPane pane = new JOptionPane(p, JOptionPane.PLAIN_MESSAGE);
259 final JDialog dlg = pane.createDialog(Main.parent, e.image+" ("+e.coor.toDisplayString()+")");
260 scale.addActionListener(new ActionListener(){
261 public void actionPerformed(ActionEvent ev) {
262 p.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
263 if (scale.getModel().isSelected())
264 ((JLabel)vp.getView()).setIcon(loadScaledImage(e.image, Math.max(vp.getWidth(), vp.getHeight())));
265 else
266 ((JLabel)vp.getView()).setIcon(new ImageIcon(e.image.getPath()));
267 p.setCursor(Cursor.getDefaultCursor());
268 }
269 });
270 scale.setSelected(true);
271 cent.addActionListener(new ActionListener(){
272 public void actionPerformed(ActionEvent ev) {
273 final ImageEntry e = data.get(currentImage);
274 if (cent.getModel().isSelected())
275 Main.map.mapView.zoomTo(e.pos, Main.map.mapView.getScale());
276 }
277 });
278
279 ActionListener nextprevAction = new ActionListener(){
280 public void actionPerformed(ActionEvent ev) {
281 p.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
282 if (ev.getActionCommand().equals("Next")) {
283 currentImage++;
284 if(currentImage>=data.size()-1) next.setEnabled(false);
285 prev.setEnabled(true);
286 } else {
287 currentImage--;
288 if(currentImage<=0) prev.setEnabled(false);
289 next.setEnabled(true);
290 }
291
292 final ImageEntry e = data.get(currentImage);
293 if (scale.getModel().isSelected())
294 ((JLabel)vp.getView()).setIcon(loadScaledImage(e.image, Math.max(vp.getWidth(), vp.getHeight())));
295 else
296 ((JLabel)vp.getView()).setIcon(new ImageIcon(e.image.getPath()));
297 dlg.setTitle(e.image+" ("+e.coor.toDisplayString()+")");
298 if (cent.getModel().isSelected())
299 Main.map.mapView.zoomTo(e.pos, Main.map.mapView.getScale());
300 p.setCursor(Cursor.getDefaultCursor());
301 }
302 };
303 next.setActionCommand("Next");
304 prev.setActionCommand("Previous");
305 next.setMnemonic(KeyEvent.VK_RIGHT);
306 prev.setMnemonic(KeyEvent.VK_LEFT);
307 scale.setMnemonic(KeyEvent.VK_F);
308 cent.setMnemonic(KeyEvent.VK_C);
309 next.setToolTipText("Show next image");
310 prev.setToolTipText("Show previous image");
311 cent.setToolTipText("Centre image location in main display");
312 scale.setToolTipText("Scale image to fit");
313
314 prev.addActionListener(nextprevAction);
315 next.addActionListener(nextprevAction);
316 cent.setSelected(false);
317
318 dlg.setModal(false);
319 dlg.setVisible(true);
320 }
321
322 @Override public Icon getIcon() {
323 return ImageProvider.get("layer", "tagimages_small");
324 }
325
326 @Override public Object getInfoComponent() {
327 JPanel p = new JPanel(new GridBagLayout());
328 p.add(new JLabel(getToolTipText()), GBC.eop());
329
330 p.add(new JLabel(tr("GPS start: {0}",dateFormat.format(gps.getFirst().time))), GBC.eol());
331 p.add(new JLabel(tr("GPS end: {0}",dateFormat.format(gps.getLast().time))), GBC.eop());
332
333 p.add(new JLabel(tr("current delta: {0}s",(delta/1000.0))), GBC.eol());
334 p.add(new JLabel(tr("timezone difference: ")+(gpstimezone>0?"+":"")+(gpstimezone/1000/60/60)), GBC.eop());
335
336 JList img = new JList(data.toArray());
337 img.setCellRenderer(new DefaultListCellRenderer(){
338 @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
339 super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
340 ImageEntry e = (ImageEntry)value;
341 setIcon(e.icon);
342 setText(e.image.getName()+" ("+dateFormat.format(new Date(e.time.getTime()+(delta+gpstimezone)))+")");
343 if (e.pos == null)
344 setForeground(Color.red);
345 return this;
346 }
347 });
348 img.setVisibleRowCount(5);
349 p.add(new JScrollPane(img), GBC.eop().fill(GBC.BOTH));
350 return p;
351 }
352
353 @Override public String getToolTipText() {
354 int i = 0;
355 for (ImageEntry e : data)
356 if (e.pos != null)
357 i++;
358 return data.size()+" "+trn("image","images",data.size())+". "+tr("{0} within the track.",i);
359 }
360
361 @Override public boolean isMergable(Layer other) {
362 return other instanceof GeoImageLayer;
363 }
364
365 @Override public void mergeFrom(Layer from) {
366 GeoImageLayer l = (GeoImageLayer)from;
367 data.addAll(l.data);
368 }
369
370 @Override public void paint(Graphics g, MapView mv) {
371 boolean clickedFound = false;
372 for (ImageEntry e : data) {
373 if (e.pos != null) {
374 Point p = mv.getPoint(e.pos);
375 Rectangle r = new Rectangle(p.x-e.icon.getIconWidth()/2, p.y-e.icon.getIconHeight()/2, e.icon.getIconWidth(), e.icon.getIconHeight());
376 e.icon.paintIcon(mv, g, r.x, r.y);
377 Border b = null;
378 Point mousePosition = mv.getMousePosition();
379 if (mousePosition == null)
380 continue; // mouse outside the whole window
381 if (!clickedFound && mousePressed && r.contains(mousePosition)) {
382 b = BorderFactory.createBevelBorder(BevelBorder.LOWERED);
383 clickedFound = true;
384 } else
385 b = BorderFactory.createBevelBorder(BevelBorder.RAISED);
386 Insets inset = b.getBorderInsets(mv);
387 r.grow((inset.top+inset.bottom)/2, (inset.left+inset.right)/2);
388 b.paintBorder(mv, g, r.x, r.y, r.width, r.height);
389 }
390 }
391 }
392
393 @Override public void visitBoundingBox(BoundingXYVisitor v) {
394 for (ImageEntry e : data)
395 v.visit(e.pos);
396 }
397
398 @Override public Component[] getMenuEntries() {
399 JMenuItem sync = new JMenuItem(tr("Sync clock"), ImageProvider.get("clock"));
400 sync.addActionListener(new ActionListener(){
401 public void actionPerformed(ActionEvent e) {
402 JFileChooser fc = new JFileChooser(Main.pref.get("tagimages.lastdirectory"));
403 fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
404 fc.setAcceptAllFileFilterUsed(false);
405 fc.setFileFilter(new FileFilter(){
406 @Override public boolean accept(File f) {
407 return f.isDirectory() || f.getName().toLowerCase().endsWith(".jpg");
408 }
409 @Override public String getDescription() {
410 return tr("JPEG images (*.jpg)");
411 }
412 });
413 fc.showOpenDialog(Main.parent);
414 File sel = fc.getSelectedFile();
415 if (sel == null)
416 return;
417 Main.pref.put("tagimages.lastdirectory", sel.getPath());
418 sync(sel);
419 Main.map.repaint();
420 }
421 });
422 return new Component[]{
423 new JMenuItem(new LayerListDialog.ShowHideLayerAction(this)),
424 new JMenuItem(new LayerListDialog.DeleteLayerAction(this)),
425 new JSeparator(),
426 sync,
427 new JSeparator(),
428 new JMenuItem(new RenameLayerAction(null, this)),
429 new JSeparator(),
430 new JMenuItem(new LayerListPopup.InfoAction(this))};
431 }
432
433 private void calculatePosition() {
434 for (ImageEntry e : data) {
435 TimedPoint lastTP = null;
436 for (TimedPoint tp : gps) {
437 Date time = new Date(tp.time.getTime() - (delta+gpstimezone));
438 if (time.after(e.time) && lastTP != null) {
439 double x = (lastTP.pos.east()+tp.pos.east())/2;
440 double y = (lastTP.pos.north()+tp.pos.north())/2;
441 e.pos = new EastNorth(x,y);
442 break;
443 }
444 lastTP = tp;
445 }
446 if (e.pos != null)
447 e.coor = Main.proj.eastNorth2latlon(e.pos);
448 }
449 }
450
451 private void sync(File f) {
452 Date exifDate;
453 try {
454 exifDate = ExifReader.readTime(f);
455 } catch (ParseException e) {
456 JOptionPane.showMessageDialog(Main.parent, tr("The date in file \"{0}\" could not be parsed.", f.getName()));
457 return;
458 }
459 if (exifDate == null) {
460 JOptionPane.showMessageDialog(Main.parent, tr("There is no EXIF time within the file \"{0}\".", f.getName()));
461 return;
462 }
463 JPanel p = new JPanel(new GridBagLayout());
464 p.add(new JLabel(tr("Image")), GBC.eol());
465 p.add(new JLabel(loadScaledImage(f, 300)), GBC.eop());
466 p.add(new JLabel(tr("Enter shown date (mm/dd/yyyy HH:MM:SS)")), GBC.eol());
467 JTextField gpsText = new JTextField(dateFormat.format(new Date(exifDate.getTime()+delta)));
468 p.add(gpsText, GBC.eol().fill(GBC.HORIZONTAL));
469 p.add(new JLabel(tr("GPS unit timezome (difference to photo)")), GBC.eol());
470 String t = Main.pref.get("tagimages.gpstimezone", "0");
471 if (t.charAt(0) != '-')
472 t = "+"+t;
473 JTextField gpsTimezone = new JTextField(t);
474 p.add(gpsTimezone, GBC.eol().fill(GBC.HORIZONTAL));
475
476 while (true) {
477 int answer = JOptionPane.showConfirmDialog(Main.parent, p, tr("Syncronize Time with GPS Unit"), JOptionPane.OK_CANCEL_OPTION);
478 if (answer != JOptionPane.OK_OPTION || gpsText.getText().equals(""))
479 return;
480 try {
481 delta = DateParser.parse(gpsText.getText()).getTime() - exifDate.getTime();
482 String time = gpsTimezone.getText();
483 if (!time.equals("") && time.charAt(0) == '+')
484 time = time.substring(1);
485 if (time.equals(""))
486 time = "0";
487 gpstimezone = Long.valueOf(time)*60*60*1000;
488 Main.pref.put("tagimages.delta", ""+delta);
489 Main.pref.put("tagimages.gpstimezone", time);
490 calculatePosition();
491 return;
492 } catch (NumberFormatException x) {
493 JOptionPane.showMessageDialog(Main.parent, tr("Time entered could not be parsed."));
494 } catch (ParseException x) {
495 JOptionPane.showMessageDialog(Main.parent, tr("Time entered could not be parsed."));
496 }
497 }
498 }
499
500 private static Icon loadScaledImage(File f, int maxSize) {
501 Image img = new ImageIcon(f.getPath()).getImage();
502 int w = img.getWidth(null);
503 int h = img.getHeight(null);
504 if (w>h) {
505 h = Math.round(maxSize*((float)h/w));
506 w = maxSize;
507 } else {
508 w = Math.round(maxSize*((float)w/h));
509 h = maxSize;
510 }
511 return new ImageIcon(img.getScaledInstance(w, h, Image.SCALE_SMOOTH));
512 }
513}
Note: See TracBrowser for help on using the repository browser.