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

Last change on this file since 2577 was 2566, checked in by bastiK, 14 years ago

Moved the code from agpifoj plugin to JOSM core. Thanks to Christian Gallioz for this great (ex-)plugin.
In the current state it might be a little unstable.

  • Did a view modification so it fits in better.
  • Added the Thumbnail feature, but still work to be done.

New in JOSM core: Possibility to add toggle dialogs not only on startup, but also later.

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