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

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