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

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

fixed #2572 - patch by Gubaer - fixed illegal access on nonexisting files

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