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

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