source: josm/trunk/src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java@ 2649

Last change on this file since 2649 was 2649, checked in by stoecker, 14 years ago

silence warnings

  • Property svn:eol-style set to native
File size: 45.2 KB
Line 
1// License: GPL. See LICENSE file for details.
2// Copyright 2007 by Christian Gallioz (aka khris78)
3// Parts of code from Geotagged plugin (by Rob Neild)
4// and the core JOSM source code (by Immanuel Scholz and others)
5
6package org.openstreetmap.josm.gui.layer.geoimage;
7
8import static org.openstreetmap.josm.tools.I18n.tr;
9
10import java.awt.BorderLayout;
11import java.awt.Cursor;
12import java.awt.Dimension;
13import java.awt.FlowLayout;
14import java.awt.GridBagConstraints;
15import java.awt.GridBagLayout;
16import java.awt.event.ActionEvent;
17import java.awt.event.ActionListener;
18import java.io.File;
19import java.io.FileInputStream;
20import java.io.IOException;
21import java.io.InputStream;
22import java.text.ParseException;
23import java.text.SimpleDateFormat;
24import java.util.ArrayList;
25import java.util.Collection;
26import java.util.Collections;
27import java.util.Comparator;
28import java.util.Date;
29import java.util.Hashtable;
30import java.util.Iterator;
31import java.util.List;
32import java.util.TimeZone;
33import java.util.Vector;
34import java.util.zip.GZIPInputStream;
35
36import javax.swing.AbstractListModel;
37import javax.swing.ButtonGroup;
38import javax.swing.JButton;
39import javax.swing.JCheckBox;
40import javax.swing.JComboBox;
41import javax.swing.JFileChooser;
42import javax.swing.JLabel;
43import javax.swing.JList;
44import javax.swing.JOptionPane;
45import javax.swing.JPanel;
46import javax.swing.JRadioButton;
47import javax.swing.JScrollPane;
48import javax.swing.JSlider;
49import javax.swing.JTextField;
50import javax.swing.ListSelectionModel;
51import javax.swing.event.ChangeEvent;
52import javax.swing.event.ChangeListener;
53import javax.swing.event.ListSelectionEvent;
54import javax.swing.event.ListSelectionListener;
55import javax.swing.filechooser.FileFilter;
56
57import org.openstreetmap.josm.Main;
58import org.openstreetmap.josm.data.gpx.GpxData;
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.ExtendedDialog;
63import org.openstreetmap.josm.gui.layer.GpxLayer;
64import org.openstreetmap.josm.gui.layer.Layer;
65import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer.ImageEntry;
66import org.openstreetmap.josm.io.GpxReader;
67import org.openstreetmap.josm.tools.ExifReader;
68import org.openstreetmap.josm.tools.GBC;
69import org.openstreetmap.josm.tools.ImageProvider;
70import org.openstreetmap.josm.tools.PrimaryDateParser;
71import org.xml.sax.SAXException;
72
73
74/** This class displays the window to select the GPX file and the offset (timezone + delta).
75 * Then it correlates the images of the layer with that GPX file.
76 */
77public class CorrelateGpxWithImages implements ActionListener {
78
79 private static List<GpxData> loadedGpxData = new ArrayList<GpxData>();
80
81 public static class CorrelateParameters {
82 GpxData gpxData;
83 float timezone;
84 long offset;
85 }
86
87 GeoImageLayer yLayer = null;
88
89 private static class GpxDataWrapper {
90 String name;
91 GpxData data;
92 File file;
93
94 public GpxDataWrapper(String name, GpxData data, File file) {
95 this.name = name;
96 this.data = data;
97 this.file = file;
98 }
99
100 @Override
101 public String toString() {
102 return name;
103 }
104 }
105
106 Vector<GpxDataWrapper> gpxLst = new Vector<GpxDataWrapper>();
107 JPanel panel = null;
108 JComboBox cbGpx = null;
109 JTextField tfTimezone = null;
110 JTextField tfOffset = null;
111 JRadioButton rbAllImg = null;
112 JRadioButton rbUntaggedImg = null;
113 JRadioButton rbNoExifImg = null;
114
115 /** This class is called when the user doesn't find the GPX file he needs in the files that have
116 * been loaded yet. It displays a FileChooser dialog to select the GPX file to be loaded.
117 */
118 private class LoadGpxDataActionListener implements ActionListener {
119
120 public void actionPerformed(ActionEvent arg0) {
121 JFileChooser fc = new JFileChooser(Main.pref.get("lastDirectory"));
122 fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
123 fc.setAcceptAllFileFilterUsed(false);
124 fc.setMultiSelectionEnabled(false);
125 fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
126 fc.setFileFilter(new FileFilter(){
127 @Override public boolean accept(File f) {
128 return (f.isDirectory()
129 || f .getName().toLowerCase().endsWith(".gpx")
130 || f.getName().toLowerCase().endsWith(".gpx.gz"));
131 }
132 @Override public String getDescription() {
133 return tr("GPX Files (*.gpx *.gpx.gz)");
134 }
135 });
136 fc.showOpenDialog(Main.parent);
137 File sel = fc.getSelectedFile();
138 if (sel == null)
139 return;
140
141 try {
142 panel.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
143
144 Main.pref.put("lastDirectory", sel.getPath());
145
146 for (int i = gpxLst.size() - 1 ; i >= 0 ; i--) {
147 GpxDataWrapper wrapper = gpxLst.get(i);
148 if (wrapper.file != null && sel.equals(wrapper.file)) {
149 cbGpx.setSelectedIndex(i);
150 if (!sel.getName().equals(wrapper.name)) {
151 JOptionPane.showMessageDialog(
152 Main.parent,
153 tr("File {0} is loaded yet under the name \"{1}\"", sel.getName(), wrapper.name),
154 tr("Error"),
155 JOptionPane.ERROR_MESSAGE
156 );
157 }
158 return;
159 }
160 }
161 GpxData data = null;
162 try {
163 InputStream iStream;
164 if (sel.getName().toLowerCase().endsWith(".gpx.gz")) {
165 iStream = new GZIPInputStream(new FileInputStream(sel));
166 } else {
167 iStream = new FileInputStream(sel);
168 }
169 data = new GpxReader(iStream, sel).data;
170 data.storageFile = sel;
171
172 } catch (SAXException x) {
173 x.printStackTrace();
174 JOptionPane.showMessageDialog(
175 Main.parent,
176 tr("Error while parsing {0}",sel.getName())+": "+x.getMessage(),
177 tr("Error"),
178 JOptionPane.ERROR_MESSAGE
179 );
180 return;
181 } catch (IOException x) {
182 x.printStackTrace();
183 JOptionPane.showMessageDialog(
184 Main.parent,
185 tr("Could not read \"{0}\"",sel.getName())+"\n"+x.getMessage(),
186 tr("Error"),
187 JOptionPane.ERROR_MESSAGE
188 );
189 return;
190 }
191
192 loadedGpxData.add(data);
193 if (gpxLst.get(0).file == null) {
194 gpxLst.remove(0);
195 }
196 gpxLst.add(new GpxDataWrapper(sel.getName(), data, sel));
197 cbGpx.setSelectedIndex(cbGpx.getItemCount() - 1);
198 } finally {
199 panel.setCursor(Cursor.getDefaultCursor());
200 }
201 }
202 }
203
204 /** This action listener is called when the user has a photo of the time of his GPS receiver. It
205 * displays the list of photos of the layer, and upon selection displays the selected photo.
206 * From that photo, the user can key in the time of the GPS.
207 * Then values of timezone and delta are set.
208 * @author chris
209 *
210 */
211 private class SetOffsetActionListener implements ActionListener {
212 JPanel panel;
213 JLabel lbExifTime;
214 JTextField tfGpsTime;
215 JComboBox cbTimezones;
216 ImageDisplay imgDisp;
217 JList imgList;
218
219 public void actionPerformed(ActionEvent arg0) {
220 SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
221
222 panel = new JPanel();
223 panel.setLayout(new BorderLayout());
224 panel.add(new JLabel(tr("<html>Take a photo of your GPS receiver while it displays the time.<br>"
225 + "Display that photo here.<br>"
226 + "And then, simply capture the time you read on the photo and select a timezone<hr></html>")),
227 BorderLayout.NORTH);
228
229 imgDisp = new ImageDisplay();
230 imgDisp.setPreferredSize(new Dimension(300, 225));
231 panel.add(imgDisp, BorderLayout.CENTER);
232
233 JPanel panelTf = new JPanel();
234 panelTf.setLayout(new GridBagLayout());
235
236 GridBagConstraints gc = new GridBagConstraints();
237 gc.gridx = gc.gridy = 0;
238 gc.gridwidth = gc.gridheight = 1;
239 gc.weightx = gc.weighty = 0.0;
240 gc.fill = GridBagConstraints.NONE;
241 gc.anchor = GridBagConstraints.WEST;
242 panelTf.add(new JLabel(tr("Photo time (from exif):")), gc);
243
244 lbExifTime = new JLabel();
245 gc.gridx = 1;
246 gc.weightx = 1.0;
247 gc.fill = GridBagConstraints.HORIZONTAL;
248 gc.gridwidth = 2;
249 panelTf.add(lbExifTime, gc);
250
251 gc.gridx = 0;
252 gc.gridy = 1;
253 gc.gridwidth = gc.gridheight = 1;
254 gc.weightx = gc.weighty = 0.0;
255 gc.fill = GridBagConstraints.NONE;
256 gc.anchor = GridBagConstraints.WEST;
257 panelTf.add(new JLabel(tr("Gps time (read from the above photo): ")), gc);
258
259 tfGpsTime = new JTextField();
260 tfGpsTime.setEnabled(false);
261 tfGpsTime.setMinimumSize(new Dimension(150, tfGpsTime.getMinimumSize().height));
262 gc.gridx = 1;
263 gc.weightx = 1.0;
264 gc.fill = GridBagConstraints.HORIZONTAL;
265 panelTf.add(tfGpsTime, gc);
266
267 gc.gridx = 2;
268 gc.weightx = 0.2;
269 panelTf.add(new JLabel(tr(" [dd/mm/yyyy hh:mm:ss]")), gc);
270
271 gc.gridx = 0;
272 gc.gridy = 2;
273 gc.gridwidth = gc.gridheight = 1;
274 gc.weightx = gc.weighty = 0.0;
275 gc.fill = GridBagConstraints.NONE;
276 gc.anchor = GridBagConstraints.WEST;
277 panelTf.add(new JLabel(tr("I'm in the timezone of: ")), gc);
278
279 Vector<String> vtTimezones = new Vector<String>();
280 String[] tmp = TimeZone.getAvailableIDs();
281
282 for (String tzStr : tmp) {
283 TimeZone tz = TimeZone.getTimeZone(tzStr);
284
285 String tzDesc = new StringBuffer(tzStr).append(" (")
286 .append(formatTimezone(tz.getRawOffset() / 3600000.0))
287 .append(')').toString();
288 vtTimezones.add(tzDesc);
289 }
290
291 Collections.sort(vtTimezones);
292
293 cbTimezones = new JComboBox(vtTimezones);
294
295 String tzId = Main.pref.get("geoimage.timezoneid", "");
296 TimeZone defaultTz;
297 if (tzId.length() == 0) {
298 defaultTz = TimeZone.getDefault();
299 } else {
300 defaultTz = TimeZone.getTimeZone(tzId);
301 }
302
303 cbTimezones.setSelectedItem(new StringBuffer(defaultTz.getID()).append(" (")
304 .append(formatTimezone(defaultTz.getRawOffset() / 3600000.0))
305 .append(')').toString());
306
307 gc.gridx = 1;
308 gc.weightx = 1.0;
309 gc.gridwidth = 2;
310 gc.fill = GridBagConstraints.HORIZONTAL;
311 panelTf.add(cbTimezones, gc);
312
313 panel.add(panelTf, BorderLayout.SOUTH);
314
315 JPanel panelLst = new JPanel();
316 panelLst.setLayout(new BorderLayout());
317
318 imgList = new JList(new AbstractListModel() {
319 public Object getElementAt(int i) {
320 return yLayer.data.get(i).file.getName();
321 }
322
323 public int getSize() {
324 return yLayer.data.size();
325 }
326 });
327 imgList.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
328 imgList.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
329
330 public void valueChanged(ListSelectionEvent arg0) {
331 int index = imgList.getSelectedIndex();
332 imgDisp.setImage(yLayer.data.get(index).file);
333 Date date = yLayer.data.get(index).time;
334 if (date != null) {
335 lbExifTime.setText(new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(date));
336 tfGpsTime.setText(new SimpleDateFormat("dd/MM/yyyy ").format(date));
337 tfGpsTime.setCaretPosition(tfGpsTime.getText().length());
338 tfGpsTime.setEnabled(true);
339 } else {
340 lbExifTime.setText(tr("No date"));
341 tfGpsTime.setText("");
342 tfGpsTime.setEnabled(false);
343 }
344 }
345
346 });
347 panelLst.add(new JScrollPane(imgList), BorderLayout.CENTER);
348
349 JButton openButton = new JButton(tr("Open an other photo"));
350 openButton.addActionListener(new ActionListener() {
351
352 public void actionPerformed(ActionEvent arg0) {
353 JFileChooser fc = new JFileChooser(Main.pref.get("geoimage.lastdirectory"));
354 fc.setAcceptAllFileFilterUsed(false);
355 fc.setMultiSelectionEnabled(false);
356 fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
357 fc.setFileFilter(JpegFileFilter.getInstance());
358 fc.showOpenDialog(Main.parent);
359 File sel = fc.getSelectedFile();
360 if (sel == null)
361 return;
362
363 imgDisp.setImage(sel);
364
365 Date date = null;
366 try {
367 date = ExifReader.readTime(sel);
368 } catch (Exception e) {
369 }
370 if (date != null) {
371 lbExifTime.setText(new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(date));
372 tfGpsTime.setText(new SimpleDateFormat("dd/MM/yyyy ").format(date));
373 tfGpsTime.setEnabled(true);
374 } else {
375 lbExifTime.setText(tr("No date"));
376 tfGpsTime.setText("");
377 tfGpsTime.setEnabled(false);
378 }
379 }
380 });
381 panelLst.add(openButton, BorderLayout.PAGE_END);
382
383 panel.add(panelLst, BorderLayout.LINE_START);
384
385 boolean isOk = false;
386 while (! isOk) {
387 int answer = JOptionPane.showConfirmDialog(
388 Main.parent, panel,
389 tr("Synchronize time from a photo of the GPS receiver"),
390 JOptionPane.OK_CANCEL_OPTION,
391 JOptionPane.QUESTION_MESSAGE
392 );
393 if (answer == JOptionPane.CANCEL_OPTION)
394 return;
395
396 long delta;
397
398 try {
399 delta = dateFormat.parse(lbExifTime.getText()).getTime()
400 - dateFormat.parse(tfGpsTime.getText()).getTime();
401 } catch(ParseException e) {
402 JOptionPane.showMessageDialog(Main.parent, tr("Error while parsing the date.\n"
403 + "Please use the requested format"),
404 tr("Invalid date"), JOptionPane.ERROR_MESSAGE );
405 continue;
406 }
407
408 String selectedTz = (String) cbTimezones.getSelectedItem();
409 int pos = selectedTz.lastIndexOf('(');
410 tzId = selectedTz.substring(0, pos - 1);
411 String tzValue = selectedTz.substring(pos + 1, selectedTz.length() - 1);
412
413 Main.pref.put("geoimage.timezoneid", tzId);
414 tfOffset.setText(Long.toString(delta / 1000));
415 tfTimezone.setText(tzValue);
416
417 isOk = true;
418
419 }
420
421 }
422 }
423
424 public CorrelateGpxWithImages(GeoImageLayer layer) {
425 this.yLayer = layer;
426 }
427
428 public void actionPerformed(ActionEvent arg0) {
429 // Construct the list of loaded GPX tracks
430 Collection<Layer> layerLst = Main.main.map.mapView.getAllLayers();
431 GpxDataWrapper defaultItem = null;
432 Iterator<Layer> iterLayer = layerLst.iterator();
433 while (iterLayer.hasNext()) {
434 Layer cur = iterLayer.next();
435 if (cur instanceof GpxLayer) {
436 GpxDataWrapper gdw = new GpxDataWrapper(((GpxLayer) cur).getName(),
437 ((GpxLayer) cur).data,
438 ((GpxLayer) cur).data.storageFile);
439 gpxLst.add(gdw);
440 if (cur == yLayer.gpxLayer) {
441 defaultItem = gdw;
442 }
443 }
444 }
445 for (GpxData data : loadedGpxData) {
446 gpxLst.add(new GpxDataWrapper(data.storageFile.getName(),
447 data,
448 data.storageFile));
449 }
450
451 if (gpxLst.size() == 0) {
452 gpxLst.add(new GpxDataWrapper(tr("<No GPX track loaded yet>"), null, null));
453 }
454
455 JPanel panelCb = new JPanel();
456 panelCb.setLayout(new FlowLayout());
457
458 panelCb.add(new JLabel(tr("GPX track: ")));
459
460 cbGpx = new JComboBox(gpxLst);
461 if (defaultItem != null) {
462 cbGpx.setSelectedItem(defaultItem);
463 }
464 panelCb.add(cbGpx);
465
466 JButton buttonOpen = new JButton(tr("Open another GPX trace"));
467 buttonOpen.setIcon(ImageProvider.get("dialogs/geoimage/geoimage-open"));
468 buttonOpen.addActionListener(new LoadGpxDataActionListener());
469
470 panelCb.add(buttonOpen);
471
472 JPanel panelTf = new JPanel();
473 panelTf.setLayout(new GridBagLayout());
474
475 GridBagConstraints gc = new GridBagConstraints();
476 gc.anchor = GridBagConstraints.WEST;
477
478 gc.gridx = gc.gridy = 0;
479 gc.gridwidth = gc.gridheight = 1;
480 gc.fill = GridBagConstraints.NONE;
481 gc.weightx = gc.weighty = 0.0;
482 panelTf.add(new JLabel(tr("Timezone: ")), gc);
483
484 float gpstimezone = Float.parseFloat(Main.pref.get("geoimage.doublegpstimezone", "0.0"));
485 if (gpstimezone == 0.0) {
486 gpstimezone = - Long.parseLong(Main.pref.get("geoimage.gpstimezone", "0"));
487 }
488 tfTimezone = new JTextField();
489 tfTimezone.setText(formatTimezone(gpstimezone));
490
491 gc.gridx = 1;
492 gc.gridy = 0;
493 gc.gridwidth = gc.gridheight = 1;
494 gc.fill = GridBagConstraints.HORIZONTAL;
495 gc.weightx = 1.0;
496 gc.weighty = 0.0;
497 panelTf.add(tfTimezone, gc);
498
499 gc.gridx = 0;
500 gc.gridy = 1;
501 gc.gridwidth = gc.gridheight = 1;
502 gc.fill = GridBagConstraints.NONE;
503 gc.weightx = gc.weighty = 0.0;
504 panelTf.add(new JLabel(tr("Offset:")), gc);
505
506 long delta = Long.parseLong(Main.pref.get("geoimage.delta", "0")) / 1000;
507 tfOffset = new JTextField();
508 tfOffset.setText(Long.toString(delta));
509 gc.gridx = gc.gridy = 1;
510 gc.gridwidth = gc.gridheight = 1;
511 gc.fill = GridBagConstraints.HORIZONTAL;
512 gc.weightx = 1.0;
513 gc.weighty = 0.0;
514 panelTf.add(tfOffset, gc);
515
516 JButton buttonViewGpsPhoto = new JButton(tr("<html>I can take a picture of my GPS receiver.<br>"
517 + "Can this help?</html>"));
518 buttonViewGpsPhoto.addActionListener(new SetOffsetActionListener());
519 gc.gridx = 2;
520 gc.gridy = 0;
521 gc.gridwidth = 1;
522 gc.gridheight = 2;
523 gc.fill = GridBagConstraints.BOTH;
524 gc.weightx = 0.5;
525 gc.weighty = 1.0;
526 panelTf.add(buttonViewGpsPhoto, gc);
527
528 gc.gridx = 0;
529 gc.gridy = 2;
530 gc.gridwidth = gc.gridheight = 1;
531 gc.fill = GridBagConstraints.NONE;
532 gc.weightx = gc.weighty = 0.0;
533 panelTf.add(new JLabel(tr("Update position for: ")), gc);
534
535 gc.gridx = 1;
536 gc.gridy = 2;
537 gc.gridwidth = 2;
538 gc.gridheight = 1;
539 gc.fill = GridBagConstraints.HORIZONTAL;
540 gc.weightx = 1.0;
541 gc.weighty = 0.0;
542 rbAllImg = new JRadioButton(tr("All images"));
543 panelTf.add(rbAllImg, gc);
544
545 gc.gridx = 1;
546 gc.gridy = 3;
547 gc.gridwidth = 2;
548 gc.gridheight = 1;
549 gc.fill = GridBagConstraints.HORIZONTAL;
550 gc.weightx = 1.0;
551 gc.weighty = 0.0;
552 rbNoExifImg = new JRadioButton(tr("Images with no exif position"));
553 panelTf.add(rbNoExifImg, gc);
554
555 gc.gridx = 1;
556 gc.gridy = 4;
557 gc.gridwidth = 2;
558 gc.gridheight = 1;
559 gc.fill = GridBagConstraints.HORIZONTAL;
560 gc.weightx = 1.0;
561 gc.weighty = 0.0;
562 rbUntaggedImg = new JRadioButton(tr("Not yet tagged images"));
563 panelTf.add(rbUntaggedImg, gc);
564
565 gc.gridx = 0;
566 gc.gridy = 5;
567 gc.gridwidth = 2;
568 gc.gridheight = 1;
569 gc.fill = GridBagConstraints.NONE;
570 gc.weightx = gc.weighty = 0.0;
571 yLayer.useThumbs = Main.pref.getBoolean("geoimage.showThumbs", false);
572 JCheckBox cbShowThumbs = new JCheckBox(tr("Show Thumbnail images on the map"), yLayer.useThumbs);
573 panelTf.add(cbShowThumbs, gc);
574
575 ButtonGroup group = new ButtonGroup();
576 group.add(rbAllImg);
577 group.add(rbNoExifImg);
578 group.add(rbUntaggedImg);
579
580 rbUntaggedImg.setSelected(true);
581
582 panel = new JPanel();
583 panel.setLayout(new BorderLayout());
584
585 panel.add(panelCb, BorderLayout.PAGE_START);
586 panel.add(panelTf, BorderLayout.CENTER);
587
588 boolean isOk = false;
589 GpxDataWrapper selectedGpx = null;
590 while (! isOk) {
591 ExtendedDialog dialog = new ExtendedDialog(
592 Main.parent,
593 tr("Correlate images with GPX track"),
594 new String[] { tr("Correlate"), tr("Auto-Guess"), tr("Cancel") }
595 );
596
597 dialog.setContent(panel);
598 dialog.setButtonIcons(new String[] { "ok.png", "dialogs/geoimage/gpx2imgManual.png", "cancel.png" });
599 dialog.showDialog();
600 int answer = dialog.getValue();
601 if(answer != 1 && answer != 2)
602 return;
603
604 // Check the selected values
605 Object item = cbGpx.getSelectedItem();
606
607 if (item == null || ! (item instanceof GpxDataWrapper)) {
608 JOptionPane.showMessageDialog(Main.parent, tr("You should select a GPX track"),
609 tr("No selected GPX track"), JOptionPane.ERROR_MESSAGE );
610 continue;
611 }
612 selectedGpx = ((GpxDataWrapper) item);
613
614 if (answer == 2) {
615 autoGuess(selectedGpx.data);
616 return;
617 }
618
619 Float timezoneValue = parseTimezone(tfTimezone.getText().trim());
620 if (timezoneValue == null) {
621 JOptionPane.showMessageDialog(Main.parent, tr("Error while parsing timezone.\nExpected format: {0}", "+H:MM"),
622 tr("Invalid timezone"), JOptionPane.ERROR_MESSAGE);
623 continue;
624 }
625 gpstimezone = timezoneValue.floatValue();
626
627 String deltaText = tfOffset.getText().trim();
628 if (deltaText.length() > 0) {
629 try {
630 if(deltaText.startsWith("+")) {
631 deltaText = deltaText.substring(1);
632 }
633 delta = Long.parseLong(deltaText);
634 } catch(NumberFormatException nfe) {
635 JOptionPane.showMessageDialog(Main.parent, tr("Error while parsing offset.\nExpected format: {0}", "number"),
636 tr("Invalid offset"), JOptionPane.ERROR_MESSAGE);
637 continue;
638 }
639 } else {
640 delta = 0;
641 }
642
643 yLayer.useThumbs = cbShowThumbs.isSelected();
644
645 Main.pref.put("geoimage.doublegpstimezone", Double.toString(gpstimezone));
646 Main.pref.put("geoimage.gpstimezone", Long.toString(- ((long) gpstimezone)));
647 Main.pref.put("geoimage.delta", Long.toString(delta * 1000));
648 Main.pref.put("geoimage.showThumbs", yLayer.useThumbs);
649 isOk = true;
650
651 if (yLayer.useThumbs) {
652 yLayer.thumbsloader = new ThumbsLoader(yLayer);
653 Thread t = new Thread(yLayer.thumbsloader);
654 t.setPriority(Thread.MIN_PRIORITY);
655 t.start();
656 }
657
658 }
659
660 // Construct a list of images that have a date, and sort them on the date.
661 ArrayList<ImageEntry> dateImgLst = getSortedImgList(rbAllImg.isSelected(), rbNoExifImg.isSelected());
662
663 int matched = matchGpxTrack(dateImgLst, selectedGpx.data, (long) (gpstimezone * 3600) + delta);
664
665 // Search whether an other layer has yet defined some bounding box.
666 // If none, we'll zoom to the bounding box of the layer with the photos.
667 boolean boundingBoxedLayerFound = false;
668 for (Layer l: Main.map.mapView.getAllLayers()) {
669 if (l != yLayer) {
670 BoundingXYVisitor bbox = new BoundingXYVisitor();
671 l.visitBoundingBox(bbox);
672 if (bbox.getBounds() != null) {
673 boundingBoxedLayerFound = true;
674 break;
675 }
676 }
677 }
678 if (! boundingBoxedLayerFound) {
679 BoundingXYVisitor bbox = new BoundingXYVisitor();
680 yLayer.visitBoundingBox(bbox);
681 Main.map.mapView.recalculateCenterScale(bbox);
682 }
683
684 Main.map.repaint();
685
686 JOptionPane.showMessageDialog(Main.parent, tr("Found {0} matches of {1} in GPX track {2}", matched, dateImgLst.size(), selectedGpx.name),
687 tr("GPX Track loaded"),
688 ((dateImgLst.size() > 0 && matched == 0) ? JOptionPane.WARNING_MESSAGE
689 : JOptionPane.INFORMATION_MESSAGE));
690
691 }
692
693 // These variables all belong to "auto guess" but need to be accessible
694 // from the slider change listener
695 private int dayOffset;
696 private JLabel lblMatches;
697 private JLabel lblOffset;
698 private JLabel lblTimezone;
699 private JLabel lblMinutes;
700 private JLabel lblSeconds;
701 private JSlider sldTimezone;
702 private JSlider sldMinutes;
703 private JSlider sldSeconds;
704 private GpxData autoGpx;
705 private ArrayList<ImageEntry> autoImgs;
706 private long firstGPXDate = -1;
707 private long firstExifDate = -1;
708
709 /**
710 * Tries to automatically match opened photos to a given GPX track. Changes are applied
711 * immediately. Presents dialog with sliders for manual adjust.
712 * @param GpxData The GPX track to match against
713 */
714 private void autoGuess(GpxData gpx) {
715 autoGpx = gpx;
716 autoImgs = getSortedImgList(true, false);
717 PrimaryDateParser dateParser = new PrimaryDateParser();
718
719 // no images found, exit
720 if(autoImgs.size() <= 0) {
721 JOptionPane.showMessageDialog(Main.parent,
722 tr("The selected photos don't contain time information."),
723 tr("Photos don't contain time information"), JOptionPane.WARNING_MESSAGE);
724 return;
725 }
726
727 ImageViewerDialog dialog = ImageViewerDialog.getInstance();
728 dialog.showDialog();
729 // Will show first photo if none is selected yet
730 if(!dialog.hasImage()) {
731 yLayer.showNextPhoto();
732 // FIXME: If the dialog is minimized it will not be maximized. ToggleDialog is
733 // in need of a complete re-write to allow this in a reasonable way.
734 }
735
736 // Init variables
737 firstExifDate = autoImgs.get(0).time.getTime()/1000;
738
739
740 // Finds first GPX point
741 outer: for (GpxTrack trk : gpx.tracks) {
742 for (Collection<WayPoint> segment : trk.trackSegs) {
743 for (WayPoint curWp : segment) {
744 String curDateWpStr = (String) curWp.attr.get("time");
745 if (curDateWpStr == null) {
746 continue;
747 }
748
749 try {
750 firstGPXDate = dateParser.parse(curDateWpStr).getTime()/1000;
751 break outer;
752 } catch(Exception e) {}
753 }
754 }
755 }
756
757 // No GPX timestamps found, exit
758 if(firstGPXDate < 0) {
759 JOptionPane.showMessageDialog(Main.parent,
760 tr("The selected GPX track doesn't contain timestamps. Please select another one."),
761 tr("GPX Track has no time information"), JOptionPane.WARNING_MESSAGE);
762 return;
763 }
764
765 // seconds
766 long diff = (yLayer.hasTimeoffset)
767 ? yLayer.timeoffset
768 : firstExifDate - firstGPXDate;
769 yLayer.timeoffset = diff;
770 yLayer.hasTimeoffset = true;
771
772 double diffInH = (double)diff/(60*60); // hours
773
774 // Find day difference
775 dayOffset = (int)Math.round(diffInH / 24); // days
776 double timezone = diff - dayOffset*24*60*60; // seconds
777
778 // In hours, rounded to two decimal places
779 timezone = (double)Math.round(timezone*100/(60*60)) / 100;
780
781 // Due to imprecise clocks we might get a "+3:28" timezone, which should obviously be 3:30 with
782 // -2 minutes offset. This determines the real timezone and finds offset.
783 double fixTimezone = (double)Math.round(timezone * 2)/2; // hours, rounded to one decimal place
784 int offset = (int)Math.round(diff - fixTimezone*60*60) - dayOffset*24*60*60; // seconds
785
786 /*System.out.println("phto " + firstExifDate);
787 System.out.println("gpx " + firstGPXDate);
788 System.out.println("diff " + diff);
789 System.out.println("difh " + diffInH);
790 System.out.println("days " + dayOffset);
791 System.out.println("time " + timezone);
792 System.out.println("fix " + fixTimezone);
793 System.out.println("offt " + offset);*/
794
795 // This is called whenever one of the sliders is moved.
796 // It updates the labels and also calls the "match photos" code
797 class sliderListener implements ChangeListener {
798 public void stateChanged(ChangeEvent e) {
799 // parse slider position into real timezone
800 double tz = Math.abs(sldTimezone.getValue());
801 String zone = tz % 2 == 0
802 ? (int)Math.floor(tz/2) + ":00"
803 : (int)Math.floor(tz/2) + ":30";
804 if(sldTimezone.getValue() < 0) {
805 zone = "-" + zone;
806 }
807
808 lblTimezone.setText(tr("Timezone: {0}", zone));
809 lblMinutes.setText(tr("Minutes: {0}", sldMinutes.getValue()));
810 lblSeconds.setText(tr("Seconds: {0}", sldSeconds.getValue()));
811
812 float gpstimezone = parseTimezone(zone).floatValue();
813
814 // Reset previous position
815 for(ImageEntry x : autoImgs) {
816 x.pos = null;
817 }
818
819 long timediff = (long) (gpstimezone * 3600)
820 + dayOffset*24*60*60
821 + sldMinutes.getValue()*60
822 + sldSeconds.getValue();
823
824 int matched = matchGpxTrack(autoImgs, autoGpx, timediff);
825
826 lblMatches.setText(
827 tr("Matched {0} of {1} photos to GPX track.", matched, autoImgs.size())
828 + ((Math.abs(dayOffset) == 0)
829 ? ""
830 : " " + tr("(Time difference of {0} days)", Math.abs(dayOffset))
831 )
832 );
833
834 int offset = (int)(firstGPXDate+timediff-firstExifDate);
835 int o = Math.abs(offset);
836 lblOffset.setText(
837 tr("Offset between track and photos: {0}m {1}s",
838 (offset < 0 ? "-" : "") + Long.toString(o/60),
839 Long.toString(o%60)
840 )
841 );
842
843 yLayer.timeoffset = timediff;
844 Main.main.map.repaint();
845 }
846 }
847
848 // Info Labels
849 lblMatches = new JLabel();
850 lblOffset = new JLabel();
851
852 // Timezone Slider
853 // The slider allows to switch timezon from -12:00 to 12:00 in 30 minutes
854 // steps. Therefore the range is -24 to 24.
855 lblTimezone = new JLabel();
856 sldTimezone = new JSlider(-24, 24, 0);
857 sldTimezone.setPaintLabels(true);
858 Hashtable<Integer,JLabel> labelTable = new Hashtable<Integer, JLabel>();
859 labelTable.put(-24, new JLabel("-12:00"));
860 labelTable.put(-12, new JLabel( "-6:00"));
861 labelTable.put( 0, new JLabel( "0:00"));
862 labelTable.put( 12, new JLabel( "6:00"));
863 labelTable.put( 24, new JLabel( "12:00"));
864 sldTimezone.setLabelTable(labelTable);
865
866 // Minutes Slider
867 lblMinutes = new JLabel();
868 sldMinutes = new JSlider(-15, 15, 0);
869 sldMinutes.setPaintLabels(true);
870 sldMinutes.setMajorTickSpacing(5);
871
872 // Seconds slider
873 lblSeconds = new JLabel();
874 sldSeconds = new JSlider(-60, 60, 0);
875 sldSeconds.setPaintLabels(true);
876 sldSeconds.setMajorTickSpacing(30);
877
878 // Put everything together
879 JPanel p = new JPanel(new GridBagLayout());
880 p.setPreferredSize(new Dimension(400, 230));
881 p.add(lblMatches, GBC.eol().fill());
882 p.add(lblOffset, GBC.eol().fill().insets(0, 0, 0, 10));
883 p.add(lblTimezone, GBC.eol().fill());
884 p.add(sldTimezone, GBC.eol().fill().insets(0, 0, 0, 10));
885 p.add(lblMinutes, GBC.eol().fill());
886 p.add(sldMinutes, GBC.eol().fill().insets(0, 0, 0, 10));
887 p.add(lblSeconds, GBC.eol().fill());
888 p.add(sldSeconds, GBC.eol().fill());
889
890 // If there's an error in the calculation the found values
891 // will be off range for the sliders. Catch this error
892 // and inform the user about it.
893 try {
894 sldTimezone.setValue((int)(fixTimezone*2));
895 sldMinutes.setValue(offset/60);
896 sldSeconds.setValue(offset%60);
897 } catch(Exception e) {
898 JOptionPane.showMessageDialog(Main.parent,
899 tr("An error occurred while trying to match the photos to the GPX track."
900 +" You can adjust the sliders to manually match the photos."),
901 tr("Matching photos to track failed"),
902 JOptionPane.WARNING_MESSAGE);
903 }
904
905 // Call the sliderListener once manually so labels get adjusted
906 new sliderListener().stateChanged(null);
907 // Listeners added here, otherwise it tries to match three times
908 // (when setting the default values)
909 sldTimezone.addChangeListener(new sliderListener());
910 sldMinutes.addChangeListener(new sliderListener());
911 sldSeconds.addChangeListener(new sliderListener());
912
913 // There is no way to cancel this dialog, all changes get applied
914 // immediately. Therefore "Close" is marked with an "OK" icon.
915 // Settings are only saved temporarily to the layer.
916 ExtendedDialog d = new ExtendedDialog(Main.parent,
917 tr("Adjust timezone and offset"),
918 new String[] { tr("Close"), tr("Default Values") }
919 );
920
921 d.setContent(p);
922 d.setButtonIcons(new String[] { "ok.png", "dialogs/refresh.png"});
923 d.showDialog();
924 int answer = d.getValue();
925 // User wants default values; discard old result and re-open dialog
926 if(answer == 2) {
927 yLayer.hasTimeoffset = false;
928 autoGuess(gpx);
929 }
930 }
931
932 /**
933 * Returns a list of images that fulfill the given criteria.
934 * Default setting is to return untagged images, but may be overwritten.
935 * @param boolean all -- returns all available images
936 * @param boolean noexif -- returns untagged images without EXIF-GPS coords
937 * @return ArrayList<ImageEntry> matching images
938 */
939 private ArrayList<ImageEntry> getSortedImgList(boolean all, boolean noexif) {
940 ArrayList<ImageEntry> dateImgLst = new ArrayList<ImageEntry>(yLayer.data.size());
941 if (all) {
942 for (ImageEntry e : yLayer.data) {
943 if (e.time != null) {
944 // Reset previous position
945 e.pos = null;
946 dateImgLst.add(e);
947 }
948 }
949
950 } else if (noexif) {
951 for (ImageEntry e : yLayer.data) {
952 if (e.time != null && e.exifCoor == null) {
953 dateImgLst.add(e);
954 }
955 }
956
957 } else {
958 for (ImageEntry e : yLayer.data) {
959 if (e.time != null && e.pos == null) {
960 dateImgLst.add(e);
961 }
962 }
963 }
964
965 Collections.sort(dateImgLst, new Comparator<ImageEntry>() {
966 public int compare(ImageEntry arg0, ImageEntry arg1) {
967 return arg0.time.compareTo(arg1.time);
968 }
969 });
970
971 return dateImgLst;
972 }
973
974 private int matchGpxTrack(ArrayList<ImageEntry> dateImgLst, GpxData selectedGpx, long offset) {
975 int ret = 0;
976
977 PrimaryDateParser dateParser = new PrimaryDateParser();
978
979 for (GpxTrack trk : selectedGpx.tracks) {
980 for (Collection<WayPoint> segment : trk.trackSegs) {
981
982 long prevDateWp = 0;
983 WayPoint prevWp = null;
984
985 for (WayPoint curWp : segment) {
986
987 String curDateWpStr = (String) curWp.attr.get("time");
988 if (curDateWpStr != null) {
989
990 try {
991 long curDateWp = dateParser.parse(curDateWpStr).getTime()/1000 + offset;
992 ret += matchPoints(dateImgLst, prevWp, prevDateWp, curWp, curDateWp);
993
994 prevWp = curWp;
995 prevDateWp = curDateWp;
996
997 } catch(ParseException e) {
998 System.err.println("Error while parsing date \"" + curDateWpStr + '"');
999 e.printStackTrace();
1000 prevWp = null;
1001 prevDateWp = 0;
1002 }
1003 } else {
1004 prevWp = null;
1005 prevDateWp = 0;
1006 }
1007 }
1008 }
1009 }
1010 return ret;
1011 }
1012
1013 private int matchPoints(ArrayList<ImageEntry> dateImgLst, WayPoint prevWp, long prevDateWp,
1014 WayPoint curWp, long curDateWp) {
1015 // Time between the track point and the previous one, 5 sec if first point, i.e. photos take
1016 // 5 sec before the first track point can be assumed to be take at the starting position
1017 long interval = prevDateWp > 0 ? ((int)Math.abs(curDateWp - prevDateWp)) : 5;
1018 int ret = 0;
1019
1020 // i is the index of the timewise last photo that has the same or earlier EXIF time
1021 int i = getLastIndexOfListBefore(dateImgLst, curDateWp);
1022
1023 // no photos match
1024 if (i < 0)
1025 return 0;
1026
1027 Double speed = null;
1028 Double prevElevation = null;
1029 Double curElevation = null;
1030
1031 if (prevWp != null) {
1032 double distance = prevWp.getCoor().greatCircleDistance(curWp.getCoor());
1033 // This is in km/h, 3.6 * m/s
1034 if (curDateWp > prevDateWp) {
1035 speed = 3.6 * distance / (curDateWp - prevDateWp);
1036 }
1037 try {
1038 prevElevation = new Double((String) prevWp.attr.get("ele"));
1039 } catch(Exception e) {}
1040 }
1041
1042 try {
1043 curElevation = new Double((String) curWp.attr.get("ele"));
1044 } catch (Exception e) {}
1045
1046 // First trackpoint, then interval is set to five seconds, i.e. photos up to five seconds
1047 // before the first point will be geotagged with the starting point
1048 if(prevDateWp == 0 || curDateWp <= prevDateWp) {
1049 while(i >= 0 && (dateImgLst.get(i).time.getTime()/1000) <= curDateWp
1050 && (dateImgLst.get(i).time.getTime()/1000) >= (curDateWp - interval)) {
1051 if(dateImgLst.get(i).pos == null) {
1052 dateImgLst.get(i).setCoor(curWp.getCoor());
1053 dateImgLst.get(i).speed = speed;
1054 dateImgLst.get(i).elevation = curElevation;
1055 ret++;
1056 }
1057 i--;
1058 }
1059 return ret;
1060 }
1061
1062 // This code gives a simple linear interpolation of the coordinates between current and
1063 // previous track point assuming a constant speed in between
1064 long imgDate;
1065 while(i >= 0 && (imgDate = dateImgLst.get(i).time.getTime()/1000) >= prevDateWp) {
1066
1067 if(dateImgLst.get(i).pos == null) {
1068 // The values of timeDiff are between 0 and 1, it is not seconds but a dimensionless
1069 // variable
1070 double timeDiff = (double)(imgDate - prevDateWp) / interval;
1071 dateImgLst.get(i).setCoor(prevWp.getCoor().interpolate(curWp.getCoor(), timeDiff));
1072 dateImgLst.get(i).speed = speed;
1073
1074 if (curElevation != null && prevElevation != null) {
1075 dateImgLst.get(i).elevation = prevElevation + (curElevation - prevElevation) * timeDiff;
1076 }
1077
1078 ret++;
1079 }
1080 i--;
1081 }
1082 return ret;
1083 }
1084
1085 private int getLastIndexOfListBefore(ArrayList<ImageEntry> dateImgLst, long searchedDate) {
1086 int lstSize= dateImgLst.size();
1087
1088 // No photos or the first photo taken is later than the search period
1089 if(lstSize == 0 || searchedDate < dateImgLst.get(0).time.getTime()/1000)
1090 return -1;
1091
1092 // The search period is later than the last photo
1093 if (searchedDate > dateImgLst.get(lstSize - 1).time.getTime() / 1000)
1094 return lstSize-1;
1095
1096 // The searched index is somewhere in the middle, do a binary search from the beginning
1097 int curIndex= 0;
1098 int startIndex= 0;
1099 int endIndex= lstSize-1;
1100 while (endIndex - startIndex > 1) {
1101 curIndex= (int) Math.round((double)(endIndex + startIndex)/2);
1102 if (searchedDate > dateImgLst.get(curIndex).time.getTime()/1000) {
1103 startIndex= curIndex;
1104 } else {
1105 endIndex= curIndex;
1106 }
1107 }
1108 if (searchedDate < dateImgLst.get(endIndex).time.getTime()/1000)
1109 return startIndex;
1110
1111 // This final loop is to check if photos with the exact same EXIF time follows
1112 while ((endIndex < (lstSize-1)) && (dateImgLst.get(endIndex).time.getTime()
1113 == dateImgLst.get(endIndex + 1).time.getTime())) {
1114 endIndex++;
1115 }
1116 return endIndex;
1117 }
1118
1119
1120 private String formatTimezone(double timezone) {
1121 StringBuffer ret = new StringBuffer();
1122
1123 if (timezone < 0) {
1124 ret.append('-');
1125 timezone = -timezone;
1126 } else {
1127 ret.append('+');
1128 }
1129 ret.append((long) timezone).append(':');
1130 int minutes = (int) ((timezone % 1) * 60);
1131 if (minutes < 10) {
1132 ret.append('0');
1133 }
1134 ret.append(minutes);
1135
1136 return ret.toString();
1137 }
1138
1139 private Float parseTimezone(String timezone) {
1140 if (timezone.length() == 0)
1141 return new Float(0);
1142
1143 char sgnTimezone = '+';
1144 StringBuffer hTimezone = new StringBuffer();
1145 StringBuffer mTimezone = new StringBuffer();
1146 int state = 1; // 1=start/sign, 2=hours, 3=minutes.
1147 for (int i = 0; i < timezone.length(); i++) {
1148 char c = timezone.charAt(i);
1149 switch (c) {
1150 case ' ' :
1151 if (state != 2 || hTimezone.length() != 0)
1152 return null;
1153 break;
1154 case '+' :
1155 case '-' :
1156 if (state == 1) {
1157 sgnTimezone = c;
1158 state = 2;
1159 } else
1160 return null;
1161 break;
1162 case ':' :
1163 case '.' :
1164 if (state == 2) {
1165 state = 3;
1166 } else
1167 return null;
1168 break;
1169 case '0' : case '1' : case '2' : case '3' : case '4' :
1170 case '5' : case '6' : case '7' : case '8' : case '9' :
1171 switch(state) {
1172 case 1 :
1173 case 2 :
1174 state = 2;
1175 hTimezone.append(c);
1176 break;
1177 case 3 :
1178 mTimezone.append(c);
1179 break;
1180 default :
1181 return null;
1182 }
1183 break;
1184 default :
1185 return null;
1186 }
1187 }
1188
1189 int h = 0;
1190 int m = 0;
1191 try {
1192 h = Integer.parseInt(hTimezone.toString());
1193 if (mTimezone.length() > 0) {
1194 m = Integer.parseInt(mTimezone.toString());
1195 }
1196 } catch (NumberFormatException nfe) {
1197 // Invalid timezone
1198 return null;
1199 }
1200
1201 if (h > 12 || m > 59 )
1202 return null;
1203 else
1204 return new Float((h + m / 60.0) * (sgnTimezone == '-' ? -1 : 1));
1205 }
1206}
Note: See TracBrowser for help on using the repository browser.