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

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