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

Last change on this file since 100 was 100, checked in by imi, 18 years ago
  • fixed JOSM crash when importing GeoImages on layers without timestamp
  • fixed merging: incomplete segments do not overwrite complete on ways
  • fixed focus when entering the popups from PropertyDialog
  • fixed broken "draw lines between gps points"
  • added doubleclick on bookmarklist
  • added background color configuration
  • added GpxImport to import 1.0 and 1.1 GPX files

This is release JOSM 1.3

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