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

Last change on this file since 1152 was 1152, checked in by stoecker, 16 years ago

close bug #1178

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