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

Last change on this file since 1116 was 1116, checked in by stoecker, 15 years ago

fixed 2 typos. closes #1821

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