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

Last change on this file since 6318 was 6318, checked in by bastiK, 11 years ago

fixed #9177 - combobox not correctly constructed (patch by AlfonZ)

  • Property svn:eol-style set to native
File size: 52.4 KB
Line 
1// License: GPL. See LICENSE file for details.
2package org.openstreetmap.josm.gui.layer.geoimage;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.BorderLayout;
8import java.awt.Cursor;
9import java.awt.Dimension;
10import java.awt.FlowLayout;
11import java.awt.GridBagConstraints;
12import java.awt.GridBagLayout;
13import java.awt.event.ActionEvent;
14import java.awt.event.ActionListener;
15import java.awt.event.FocusEvent;
16import java.awt.event.FocusListener;
17import java.awt.event.ItemEvent;
18import java.awt.event.ItemListener;
19import java.awt.event.WindowAdapter;
20import java.awt.event.WindowEvent;
21import java.io.File;
22import java.io.FileInputStream;
23import java.io.IOException;
24import java.io.InputStream;
25import java.text.ParseException;
26import java.text.SimpleDateFormat;
27import java.util.ArrayList;
28import java.util.Collection;
29import java.util.Collections;
30import java.util.Comparator;
31import java.util.Date;
32import java.util.Dictionary;
33import java.util.Hashtable;
34import java.util.List;
35import java.util.TimeZone;
36import java.util.zip.GZIPInputStream;
37
38import javax.swing.AbstractAction;
39import javax.swing.AbstractListModel;
40import javax.swing.BorderFactory;
41import javax.swing.JButton;
42import javax.swing.JCheckBox;
43import javax.swing.JFileChooser;
44import javax.swing.JLabel;
45import javax.swing.JList;
46import javax.swing.JOptionPane;
47import javax.swing.JPanel;
48import javax.swing.JScrollPane;
49import javax.swing.JSeparator;
50import javax.swing.JSlider;
51import javax.swing.ListSelectionModel;
52import javax.swing.SwingConstants;
53import javax.swing.event.ChangeEvent;
54import javax.swing.event.ChangeListener;
55import javax.swing.event.DocumentEvent;
56import javax.swing.event.DocumentListener;
57import javax.swing.event.ListSelectionEvent;
58import javax.swing.event.ListSelectionListener;
59import javax.swing.filechooser.FileFilter;
60
61import org.openstreetmap.josm.Main;
62import org.openstreetmap.josm.actions.DiskAccessAction;
63import org.openstreetmap.josm.data.gpx.GpxData;
64import org.openstreetmap.josm.data.gpx.GpxTrack;
65import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
66import org.openstreetmap.josm.data.gpx.WayPoint;
67import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
68import org.openstreetmap.josm.gui.ExtendedDialog;
69import org.openstreetmap.josm.gui.layer.GpxLayer;
70import org.openstreetmap.josm.gui.layer.Layer;
71import org.openstreetmap.josm.gui.widgets.JosmComboBox;
72import org.openstreetmap.josm.gui.widgets.JosmTextField;
73import org.openstreetmap.josm.io.GpxReader;
74import org.openstreetmap.josm.tools.ExifReader;
75import org.openstreetmap.josm.tools.GBC;
76import org.openstreetmap.josm.tools.ImageProvider;
77import org.openstreetmap.josm.tools.PrimaryDateParser;
78import org.xml.sax.SAXException;
79
80/**
81 * This class displays the window to select the GPX file and the offset (timezone + delta).
82 * Then it correlates the images of the layer with that GPX file.
83 */
84public class CorrelateGpxWithImages extends AbstractAction {
85
86 private static List<GpxData> loadedGpxData = new ArrayList<GpxData>();
87
88 GeoImageLayer yLayer = null;
89 double timezone;
90 long delta;
91
92 public CorrelateGpxWithImages(GeoImageLayer layer) {
93 super(tr("Correlate to GPX"), ImageProvider.get("dialogs/geoimage/gpx2img"));
94 this.yLayer = layer;
95 }
96
97 private static class GpxDataWrapper {
98 String name;
99 GpxData data;
100 File file;
101
102 public GpxDataWrapper(String name, GpxData data, File file) {
103 this.name = name;
104 this.data = data;
105 this.file = file;
106 }
107
108 @Override
109 public String toString() {
110 return name;
111 }
112 }
113
114 ExtendedDialog syncDialog;
115 List<GpxDataWrapper> gpxLst = new ArrayList<GpxDataWrapper>();
116 JPanel outerPanel;
117 JosmComboBox cbGpx;
118 JosmTextField tfTimezone;
119 JosmTextField tfOffset;
120 JCheckBox cbExifImg;
121 JCheckBox cbTaggedImg;
122 JCheckBox cbShowThumbs;
123 JLabel statusBarText;
124
125 // remember the last number of matched photos
126 int lastNumMatched = 0;
127
128 /** This class is called when the user doesn't find the GPX file he needs in the files that have
129 * been loaded yet. It displays a FileChooser dialog to select the GPX file to be loaded.
130 */
131 private class LoadGpxDataActionListener implements ActionListener {
132
133 @Override
134 public void actionPerformed(ActionEvent arg0) {
135 FileFilter filter = new FileFilter(){
136 @Override public boolean accept(File f) {
137 return (f.isDirectory()
138 || f .getName().toLowerCase().endsWith(".gpx")
139 || f.getName().toLowerCase().endsWith(".gpx.gz"));
140 }
141 @Override public String getDescription() {
142 return tr("GPX Files (*.gpx *.gpx.gz)");
143 }
144 };
145 JFileChooser fc = DiskAccessAction.createAndOpenFileChooser(true, false, null, filter, JFileChooser.FILES_ONLY, null);
146 if (fc == null)
147 return;
148 File sel = fc.getSelectedFile();
149
150 try {
151 outerPanel.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
152
153 for (int i = gpxLst.size() - 1 ; i >= 0 ; i--) {
154 GpxDataWrapper wrapper = gpxLst.get(i);
155 if (wrapper.file != null && sel.equals(wrapper.file)) {
156 cbGpx.setSelectedIndex(i);
157 if (!sel.getName().equals(wrapper.name)) {
158 JOptionPane.showMessageDialog(
159 Main.parent,
160 tr("File {0} is loaded yet under the name \"{1}\"", sel.getName(), wrapper.name),
161 tr("Error"),
162 JOptionPane.ERROR_MESSAGE
163 );
164 }
165 return;
166 }
167 }
168 GpxData data = null;
169 try {
170 InputStream iStream;
171 if (sel.getName().toLowerCase().endsWith(".gpx.gz")) {
172 iStream = new GZIPInputStream(new FileInputStream(sel));
173 } else {
174 iStream = new FileInputStream(sel);
175 }
176 GpxReader reader = new GpxReader(iStream);
177 reader.parse(false);
178 data = reader.getGpxData();
179 data.storageFile = sel;
180
181 } catch (SAXException x) {
182 x.printStackTrace();
183 JOptionPane.showMessageDialog(
184 Main.parent,
185 tr("Error while parsing {0}",sel.getName())+": "+x.getMessage(),
186 tr("Error"),
187 JOptionPane.ERROR_MESSAGE
188 );
189 return;
190 } catch (IOException x) {
191 x.printStackTrace();
192 JOptionPane.showMessageDialog(
193 Main.parent,
194 tr("Could not read \"{0}\"",sel.getName())+"\n"+x.getMessage(),
195 tr("Error"),
196 JOptionPane.ERROR_MESSAGE
197 );
198 return;
199 }
200
201 loadedGpxData.add(data);
202 if (gpxLst.get(0).file == null) {
203 gpxLst.remove(0);
204 }
205 gpxLst.add(new GpxDataWrapper(sel.getName(), data, sel));
206 cbGpx.setSelectedIndex(cbGpx.getItemCount() - 1);
207 } finally {
208 outerPanel.setCursor(Cursor.getDefaultCursor());
209 }
210 }
211 }
212
213 /**
214 * This action listener is called when the user has a photo of the time of his GPS receiver. It
215 * displays the list of photos of the layer, and upon selection displays the selected photo.
216 * From that photo, the user can key in the time of the GPS.
217 * Then values of timezone and delta are set.
218 * @author chris
219 *
220 */
221 private class SetOffsetActionListener implements ActionListener {
222 JPanel panel;
223 JLabel lbExifTime;
224 JosmTextField tfGpsTime;
225 JosmComboBox cbTimezones;
226 ImageDisplay imgDisp;
227 JList imgList;
228
229 @Override
230 public void actionPerformed(ActionEvent arg0) {
231 SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
232
233 panel = new JPanel();
234 panel.setLayout(new BorderLayout());
235 panel.add(new JLabel(tr("<html>Take a photo of your GPS receiver while it displays the time.<br>"
236 + "Display that photo here.<br>"
237 + "And then, simply capture the time you read on the photo and select a timezone<hr></html>")),
238 BorderLayout.NORTH);
239
240 imgDisp = new ImageDisplay();
241 imgDisp.setPreferredSize(new Dimension(300, 225));
242 panel.add(imgDisp, BorderLayout.CENTER);
243
244 JPanel panelTf = new JPanel();
245 panelTf.setLayout(new GridBagLayout());
246
247 GridBagConstraints gc = new GridBagConstraints();
248 gc.gridx = gc.gridy = 0;
249 gc.gridwidth = gc.gridheight = 1;
250 gc.weightx = gc.weighty = 0.0;
251 gc.fill = GridBagConstraints.NONE;
252 gc.anchor = GridBagConstraints.WEST;
253 panelTf.add(new JLabel(tr("Photo time (from exif):")), gc);
254
255 lbExifTime = new JLabel();
256 gc.gridx = 1;
257 gc.weightx = 1.0;
258 gc.fill = GridBagConstraints.HORIZONTAL;
259 gc.gridwidth = 2;
260 panelTf.add(lbExifTime, gc);
261
262 gc.gridx = 0;
263 gc.gridy = 1;
264 gc.gridwidth = gc.gridheight = 1;
265 gc.weightx = gc.weighty = 0.0;
266 gc.fill = GridBagConstraints.NONE;
267 gc.anchor = GridBagConstraints.WEST;
268 panelTf.add(new JLabel(tr("Gps time (read from the above photo): ")), gc);
269
270 tfGpsTime = new JosmTextField(12);
271 tfGpsTime.setEnabled(false);
272 tfGpsTime.setMinimumSize(new Dimension(155, tfGpsTime.getMinimumSize().height));
273 gc.gridx = 1;
274 gc.weightx = 1.0;
275 gc.fill = GridBagConstraints.HORIZONTAL;
276 panelTf.add(tfGpsTime, gc);
277
278 gc.gridx = 2;
279 gc.weightx = 0.2;
280 panelTf.add(new JLabel(tr(" [dd/mm/yyyy hh:mm:ss]")), gc);
281
282 gc.gridx = 0;
283 gc.gridy = 2;
284 gc.gridwidth = gc.gridheight = 1;
285 gc.weightx = gc.weighty = 0.0;
286 gc.fill = GridBagConstraints.NONE;
287 gc.anchor = GridBagConstraints.WEST;
288 panelTf.add(new JLabel(tr("I am in the timezone of: ")), gc);
289
290 String[] tmp = TimeZone.getAvailableIDs();
291 List<String> vtTimezones = new ArrayList<String>(tmp.length);
292
293 for (String tzStr : tmp) {
294 TimeZone tz = TimeZone.getTimeZone(tzStr);
295
296 String tzDesc = new StringBuffer(tzStr).append(" (")
297 .append(formatTimezone(tz.getRawOffset() / 3600000.0))
298 .append(')').toString();
299 vtTimezones.add(tzDesc);
300 }
301
302 Collections.sort(vtTimezones);
303
304 cbTimezones = new JosmComboBox(vtTimezones.toArray());
305
306 String tzId = Main.pref.get("geoimage.timezoneid", "");
307 TimeZone defaultTz;
308 if (tzId.length() == 0) {
309 defaultTz = TimeZone.getDefault();
310 } else {
311 defaultTz = TimeZone.getTimeZone(tzId);
312 }
313
314 cbTimezones.setSelectedItem(new StringBuffer(defaultTz.getID()).append(" (")
315 .append(formatTimezone(defaultTz.getRawOffset() / 3600000.0))
316 .append(')').toString());
317
318 gc.gridx = 1;
319 gc.weightx = 1.0;
320 gc.gridwidth = 2;
321 gc.fill = GridBagConstraints.HORIZONTAL;
322 panelTf.add(cbTimezones, gc);
323
324 panel.add(panelTf, BorderLayout.SOUTH);
325
326 JPanel panelLst = new JPanel();
327 panelLst.setLayout(new BorderLayout());
328
329 imgList = new JList(new AbstractListModel() {
330 @Override
331 public Object getElementAt(int i) {
332 return yLayer.data.get(i).getFile().getName();
333 }
334
335 @Override
336 public int getSize() {
337 return yLayer.data.size();
338 }
339 });
340 imgList.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
341 imgList.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
342
343 @Override
344 public void valueChanged(ListSelectionEvent arg0) {
345 int index = imgList.getSelectedIndex();
346 Integer orientation = null;
347 try {
348 orientation = ExifReader.readOrientation(yLayer.data.get(index).getFile());
349 } catch (Exception e) {
350 Main.warn(e);
351 }
352 imgDisp.setImage(yLayer.data.get(index).getFile(), orientation);
353 Date date = yLayer.data.get(index).getExifTime();
354 if (date != null) {
355 lbExifTime.setText(new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(date));
356 tfGpsTime.setText(new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(date));
357 tfGpsTime.setCaretPosition(tfGpsTime.getText().length());
358 tfGpsTime.setEnabled(true);
359 tfGpsTime.requestFocus();
360 } else {
361 lbExifTime.setText(tr("No date"));
362 tfGpsTime.setText("");
363 tfGpsTime.setEnabled(false);
364 }
365 }
366
367 });
368 panelLst.add(new JScrollPane(imgList), BorderLayout.CENTER);
369
370 JButton openButton = new JButton(tr("Open another photo"));
371 openButton.addActionListener(new ActionListener() {
372
373 @Override
374 public void actionPerformed(ActionEvent arg0) {
375 JFileChooser fc = DiskAccessAction.createAndOpenFileChooser(true, false, null, JpegFileFilter.getInstance(), JFileChooser.FILES_ONLY, "geoimage.lastdirectory");
376 if (fc == null)
377 return;
378 File sel = fc.getSelectedFile();
379
380 Integer orientation = null;
381 try {
382 orientation = ExifReader.readOrientation(sel);
383 } catch (Exception e) {
384 Main.warn(e);
385 }
386 imgDisp.setImage(sel, orientation);
387
388 Date date = null;
389 try {
390 date = ExifReader.readTime(sel);
391 } catch (Exception e) {
392 Main.warn(e);
393 }
394 if (date != null) {
395 lbExifTime.setText(new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(date));
396 tfGpsTime.setText(new SimpleDateFormat("dd/MM/yyyy ").format(date));
397 tfGpsTime.setEnabled(true);
398 } else {
399 lbExifTime.setText(tr("No date"));
400 tfGpsTime.setText("");
401 tfGpsTime.setEnabled(false);
402 }
403 }
404 });
405 panelLst.add(openButton, BorderLayout.PAGE_END);
406
407 panel.add(panelLst, BorderLayout.LINE_START);
408
409 boolean isOk = false;
410 while (! isOk) {
411 int answer = JOptionPane.showConfirmDialog(
412 Main.parent, panel,
413 tr("Synchronize time from a photo of the GPS receiver"),
414 JOptionPane.OK_CANCEL_OPTION,
415 JOptionPane.QUESTION_MESSAGE
416 );
417 if (answer == JOptionPane.CANCEL_OPTION)
418 return;
419
420 long delta;
421
422 try {
423 delta = dateFormat.parse(lbExifTime.getText()).getTime()
424 - dateFormat.parse(tfGpsTime.getText()).getTime();
425 } catch(ParseException e) {
426 JOptionPane.showMessageDialog(Main.parent, tr("Error while parsing the date.\n"
427 + "Please use the requested format"),
428 tr("Invalid date"), JOptionPane.ERROR_MESSAGE );
429 continue;
430 }
431
432 String selectedTz = (String) cbTimezones.getSelectedItem();
433 int pos = selectedTz.lastIndexOf('(');
434 tzId = selectedTz.substring(0, pos - 1);
435 String tzValue = selectedTz.substring(pos + 1, selectedTz.length() - 1);
436
437 Main.pref.put("geoimage.timezoneid", tzId);
438 tfOffset.setText(Long.toString(delta / 1000));
439 tfTimezone.setText(tzValue);
440
441 isOk = true;
442
443 }
444 statusBarUpdater.updateStatusBar();
445 yLayer.updateBufferAndRepaint();
446 }
447 }
448
449 @Override
450 public void actionPerformed(ActionEvent arg0) {
451 // Construct the list of loaded GPX tracks
452 Collection<Layer> layerLst = Main.map.mapView.getAllLayers();
453 GpxDataWrapper defaultItem = null;
454 for (Layer cur : layerLst) {
455 if (cur instanceof GpxLayer) {
456 GpxLayer curGpx = (GpxLayer) cur;
457 GpxDataWrapper gdw = new GpxDataWrapper(curGpx.getName(), curGpx.data, curGpx.data.storageFile);
458 gpxLst.add(gdw);
459 if (cur == yLayer.gpxLayer) {
460 defaultItem = gdw;
461 }
462 }
463 }
464 for (GpxData data : loadedGpxData) {
465 gpxLst.add(new GpxDataWrapper(data.storageFile.getName(),
466 data,
467 data.storageFile));
468 }
469
470 if (gpxLst.isEmpty()) {
471 gpxLst.add(new GpxDataWrapper(tr("<No GPX track loaded yet>"), null, null));
472 }
473
474 JPanel panelCb = new JPanel();
475
476 panelCb.add(new JLabel(tr("GPX track: ")));
477
478 cbGpx = new JosmComboBox(gpxLst.toArray());
479 if (defaultItem != null) {
480 cbGpx.setSelectedItem(defaultItem);
481 }
482 cbGpx.addActionListener(statusBarUpdaterWithRepaint);
483 panelCb.add(cbGpx);
484
485 JButton buttonOpen = new JButton(tr("Open another GPX trace"));
486 buttonOpen.addActionListener(new LoadGpxDataActionListener());
487 panelCb.add(buttonOpen);
488
489 JPanel panelTf = new JPanel();
490 panelTf.setLayout(new GridBagLayout());
491
492 String prefTimezone = Main.pref.get("geoimage.timezone", "0:00");
493 if (prefTimezone == null) {
494 prefTimezone = "0:00";
495 }
496 try {
497 timezone = parseTimezone(prefTimezone);
498 } catch (ParseException e) {
499 timezone = 0;
500 }
501
502 tfTimezone = new JosmTextField(10);
503 tfTimezone.setText(formatTimezone(timezone));
504
505 try {
506 delta = parseOffset(Main.pref.get("geoimage.delta", "0"));
507 } catch (ParseException e) {
508 delta = 0;
509 }
510 delta = delta / 1000; // milliseconds -> seconds
511
512 tfOffset = new JosmTextField(10);
513 tfOffset.setText(Long.toString(delta));
514
515 JButton buttonViewGpsPhoto = new JButton(tr("<html>Use photo of an accurate clock,<br>"
516 + "e.g. GPS receiver display</html>"));
517 buttonViewGpsPhoto.setIcon(ImageProvider.get("clock"));
518 buttonViewGpsPhoto.addActionListener(new SetOffsetActionListener());
519
520 JButton buttonAutoGuess = new JButton(tr("Auto-Guess"));
521 buttonAutoGuess.setToolTipText(tr("Matches first photo with first gpx point"));
522 buttonAutoGuess.addActionListener(new AutoGuessActionListener());
523
524 JButton buttonAdjust = new JButton(tr("Manual adjust"));
525 buttonAdjust.addActionListener(new AdjustActionListener());
526
527 JLabel labelPosition = new JLabel(tr("Override position for: "));
528
529 int numAll = getSortedImgList(true, true).size();
530 int numExif = numAll - getSortedImgList(false, true).size();
531 int numTagged = numAll - getSortedImgList(true, false).size();
532
533 cbExifImg = new JCheckBox(tr("Images with geo location in exif data ({0}/{1})", numExif, numAll));
534 cbExifImg.setEnabled(numExif != 0);
535
536 cbTaggedImg = new JCheckBox(tr("Images that are already tagged ({0}/{1})", numTagged, numAll), true);
537 cbTaggedImg.setEnabled(numTagged != 0);
538
539 labelPosition.setEnabled(cbExifImg.isEnabled() || cbTaggedImg.isEnabled());
540
541 boolean ticked = yLayer.thumbsLoaded || Main.pref.getBoolean("geoimage.showThumbs", false);
542 cbShowThumbs = new JCheckBox(tr("Show Thumbnail images on the map"), ticked);
543 cbShowThumbs.setEnabled(!yLayer.thumbsLoaded);
544
545 int y=0;
546 GBC gbc = GBC.eol();
547 gbc.gridx = 0;
548 gbc.gridy = y++;
549 panelTf.add(panelCb, gbc);
550
551 gbc = GBC.eol().fill(GBC.HORIZONTAL).insets(0,0,0,12);
552 gbc.gridx = 0;
553 gbc.gridy = y++;
554 panelTf.add(new JSeparator(SwingConstants.HORIZONTAL), gbc);
555
556 gbc = GBC.std();
557 gbc.gridx = 0;
558 gbc.gridy = y;
559 panelTf.add(new JLabel(tr("Timezone: ")), gbc);
560
561 gbc = GBC.std().fill(GBC.HORIZONTAL);
562 gbc.gridx = 1;
563 gbc.gridy = y++;
564 gbc.weightx = 1.;
565 panelTf.add(tfTimezone, gbc);
566
567 gbc = GBC.std();
568 gbc.gridx = 0;
569 gbc.gridy = y;
570 panelTf.add(new JLabel(tr("Offset:")), gbc);
571
572 gbc = GBC.std().fill(GBC.HORIZONTAL);
573 gbc.gridx = 1;
574 gbc.gridy = y++;
575 gbc.weightx = 1.;
576 panelTf.add(tfOffset, gbc);
577
578 gbc = GBC.std().insets(5,5,5,5);
579 gbc.gridx = 2;
580 gbc.gridy = y-2;
581 gbc.gridheight = 2;
582 gbc.gridwidth = 2;
583 gbc.fill = GridBagConstraints.BOTH;
584 gbc.weightx = 0.5;
585 panelTf.add(buttonViewGpsPhoto, gbc);
586
587 gbc = GBC.std().fill(GBC.BOTH).insets(5,5,5,5);
588 gbc.gridx = 2;
589 gbc.gridy = y++;
590 gbc.weightx = 0.5;
591 panelTf.add(buttonAutoGuess, gbc);
592
593 gbc.gridx = 3;
594 panelTf.add(buttonAdjust, gbc);
595
596 gbc = GBC.eol().fill(GBC.HORIZONTAL).insets(0,12,0,0);
597 gbc.gridx = 0;
598 gbc.gridy = y++;
599 panelTf.add(new JSeparator(SwingConstants.HORIZONTAL), gbc);
600
601 gbc = GBC.eol();
602 gbc.gridx = 0;
603 gbc.gridy = y++;
604 panelTf.add(labelPosition, gbc);
605
606 gbc = GBC.eol();
607 gbc.gridx = 1;
608 gbc.gridy = y++;
609 panelTf.add(cbExifImg, gbc);
610
611 gbc = GBC.eol();
612 gbc.gridx = 1;
613 gbc.gridy = y++;
614 panelTf.add(cbTaggedImg, gbc);
615
616 gbc = GBC.eol();
617 gbc.gridx = 0;
618 gbc.gridy = y++;
619 panelTf.add(cbShowThumbs, gbc);
620
621 final JPanel statusBar = new JPanel();
622 statusBar.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
623 statusBar.setBorder(BorderFactory.createLoweredBevelBorder());
624 statusBarText = new JLabel(" ");
625 statusBarText.setFont(statusBarText.getFont().deriveFont(8));
626 statusBar.add(statusBarText);
627
628 tfTimezone.addFocusListener(repaintTheMap);
629 tfOffset.addFocusListener(repaintTheMap);
630
631 tfTimezone.getDocument().addDocumentListener(statusBarUpdater);
632 tfOffset.getDocument().addDocumentListener(statusBarUpdater);
633 cbExifImg.addItemListener(statusBarUpdaterWithRepaint);
634 cbTaggedImg.addItemListener(statusBarUpdaterWithRepaint);
635
636 statusBarUpdater.updateStatusBar();
637
638 outerPanel = new JPanel();
639 outerPanel.setLayout(new BorderLayout());
640 outerPanel.add(statusBar, BorderLayout.PAGE_END);
641
642 syncDialog = new ExtendedDialog(
643 Main.parent,
644 tr("Correlate images with GPX track"),
645 new String[] { tr("Correlate"), tr("Cancel") },
646 false
647 );
648 syncDialog.setContent(panelTf, false);
649 syncDialog.setButtonIcons(new String[] { "ok.png", "cancel.png" });
650 syncDialog.setupDialog();
651 outerPanel.add(syncDialog.getContentPane(), BorderLayout.PAGE_START);
652 syncDialog.setContentPane(outerPanel);
653 syncDialog.pack();
654 syncDialog.addWindowListener(new WindowAdapter() {
655 final static int CANCEL = -1;
656 final static int DONE = 0;
657 final static int AGAIN = 1;
658 final static int NOTHING = 2;
659 private int checkAndSave() {
660 if (syncDialog.isVisible())
661 // nothing happened: JOSM was minimized or similar
662 return NOTHING;
663 int answer = syncDialog.getValue();
664 if(answer != 1)
665 return CANCEL;
666
667 // Parse values again, to display an error if the format is not recognized
668 try {
669 timezone = parseTimezone(tfTimezone.getText().trim());
670 } catch (ParseException e) {
671 JOptionPane.showMessageDialog(Main.parent, e.getMessage(),
672 tr("Invalid timezone"), JOptionPane.ERROR_MESSAGE);
673 return AGAIN;
674 }
675
676 try {
677 delta = parseOffset(tfOffset.getText().trim());
678 } catch (ParseException e) {
679 JOptionPane.showMessageDialog(Main.parent, e.getMessage(),
680 tr("Invalid offset"), JOptionPane.ERROR_MESSAGE);
681 return AGAIN;
682 }
683
684 if (lastNumMatched == 0) {
685 if (new ExtendedDialog(
686 Main.parent,
687 tr("Correlate images with GPX track"),
688 new String[] { tr("OK"), tr("Try Again") }).
689 setContent(tr("No images could be matched!")).
690 setButtonIcons(new String[] { "ok.png", "dialogs/refresh.png"}).
691 showDialog().getValue() == 2)
692 return AGAIN;
693 }
694 return DONE;
695 }
696
697 @Override
698 public void windowDeactivated(WindowEvent e) {
699 int result = checkAndSave();
700 switch (result) {
701 case NOTHING:
702 break;
703 case CANCEL:
704 {
705 if (yLayer != null) {
706 for (ImageEntry ie : yLayer.data) {
707 ie.tmp = null;
708 }
709 yLayer.updateBufferAndRepaint();
710 }
711 break;
712 }
713 case AGAIN:
714 actionPerformed(null);
715 break;
716 case DONE:
717 {
718 Main.pref.put("geoimage.timezone", formatTimezone(timezone));
719 Main.pref.put("geoimage.delta", Long.toString(delta * 1000));
720 Main.pref.put("geoimage.showThumbs", yLayer.useThumbs);
721
722 yLayer.useThumbs = cbShowThumbs.isSelected();
723 yLayer.loadThumbs();
724
725 // Search whether an other layer has yet defined some bounding box.
726 // If none, we'll zoom to the bounding box of the layer with the photos.
727 boolean boundingBoxedLayerFound = false;
728 for (Layer l: Main.map.mapView.getAllLayers()) {
729 if (l != yLayer) {
730 BoundingXYVisitor bbox = new BoundingXYVisitor();
731 l.visitBoundingBox(bbox);
732 if (bbox.getBounds() != null) {
733 boundingBoxedLayerFound = true;
734 break;
735 }
736 }
737 }
738 if (! boundingBoxedLayerFound) {
739 BoundingXYVisitor bbox = new BoundingXYVisitor();
740 yLayer.visitBoundingBox(bbox);
741 Main.map.mapView.recalculateCenterScale(bbox);
742 }
743
744 for (ImageEntry ie : yLayer.data) {
745 ie.applyTmp();
746 }
747
748 yLayer.updateBufferAndRepaint();
749
750 break;
751 }
752 default:
753 throw new IllegalStateException();
754 }
755 }
756 });
757 syncDialog.showDialog();
758 }
759
760 StatusBarUpdater statusBarUpdater = new StatusBarUpdater(false);
761 StatusBarUpdater statusBarUpdaterWithRepaint = new StatusBarUpdater(true);
762
763 private class StatusBarUpdater implements DocumentListener, ItemListener, ActionListener {
764 private boolean doRepaint;
765
766 public StatusBarUpdater(boolean doRepaint) {
767 this.doRepaint = doRepaint;
768 }
769
770 @Override
771 public void insertUpdate(DocumentEvent ev) {
772 updateStatusBar();
773 }
774 @Override
775 public void removeUpdate(DocumentEvent ev) {
776 updateStatusBar();
777 }
778 @Override
779 public void changedUpdate(DocumentEvent ev) {
780 }
781 @Override
782 public void itemStateChanged(ItemEvent e) {
783 updateStatusBar();
784 }
785 @Override
786 public void actionPerformed(ActionEvent e) {
787 updateStatusBar();
788 }
789
790 public void updateStatusBar() {
791 statusBarText.setText(statusText());
792 if (doRepaint) {
793 yLayer.updateBufferAndRepaint();
794 }
795 }
796
797 private String statusText() {
798 try {
799 timezone = parseTimezone(tfTimezone.getText().trim());
800 delta = parseOffset(tfOffset.getText().trim());
801 } catch (ParseException e) {
802 return e.getMessage();
803 }
804
805 // The selection of images we are about to correlate may have changed.
806 // So reset all images.
807 for (ImageEntry ie: yLayer.data) {
808 ie.tmp = null;
809 }
810
811 // Construct a list of images that have a date, and sort them on the date.
812 List<ImageEntry> dateImgLst = getSortedImgList();
813 // Create a temporary copy for each image
814 for (ImageEntry ie : dateImgLst) {
815 ie.cleanTmp();
816 }
817
818 GpxDataWrapper selGpx = selectedGPX(false);
819 if (selGpx == null)
820 return tr("No gpx selected");
821
822 final long offset_ms = ((long) (timezone * 3600) + delta) * 1000; // in milliseconds
823 lastNumMatched = matchGpxTrack(dateImgLst, selGpx.data, offset_ms);
824
825 return trn("<html>Matched <b>{0}</b> of <b>{1}</b> photo to GPX track.</html>",
826 "<html>Matched <b>{0}</b> of <b>{1}</b> photos to GPX track.</html>",
827 dateImgLst.size(), lastNumMatched, dateImgLst.size());
828 }
829 }
830
831 RepaintTheMapListener repaintTheMap = new RepaintTheMapListener();
832 private class RepaintTheMapListener implements FocusListener {
833 @Override
834 public void focusGained(FocusEvent e) { // do nothing
835 }
836
837 @Override
838 public void focusLost(FocusEvent e) {
839 yLayer.updateBufferAndRepaint();
840 }
841 }
842
843 /**
844 * Presents dialog with sliders for manual adjust.
845 */
846 private class AdjustActionListener implements ActionListener {
847
848 @Override
849 public void actionPerformed(ActionEvent arg0) {
850
851 long diff = delta + Math.round(timezone*60*60);
852
853 double diffInH = (double)diff/(60*60); // hours
854
855 // Find day difference
856 final int dayOffset = (int)Math.round(diffInH / 24); // days
857 double tmz = diff - dayOffset*24*60*60L; // seconds
858
859 // In hours, rounded to two decimal places
860 tmz = (double)Math.round(tmz*100/(60*60)) / 100;
861
862 // Due to imprecise clocks we might get a "+3:28" timezone, which should obviously be 3:30 with
863 // -2 minutes offset. This determines the real timezone and finds offset.
864 double fixTimezone = (double)Math.round(tmz * 2)/2; // hours, rounded to one decimal place
865 int offset = (int)Math.round(diff - fixTimezone*60*60) - dayOffset*24*60*60; // seconds
866
867 // Info Labels
868 final JLabel lblMatches = new JLabel();
869
870 // Timezone Slider
871 // The slider allows to switch timezon from -12:00 to 12:00 in 30 minutes
872 // steps. Therefore the range is -24 to 24.
873 final JLabel lblTimezone = new JLabel();
874 final JSlider sldTimezone = new JSlider(-24, 24, 0);
875 sldTimezone.setPaintLabels(true);
876 Dictionary<Integer,JLabel> labelTable = new Hashtable<Integer, JLabel>();
877 labelTable.put(-24, new JLabel("-12:00"));
878 labelTable.put(-12, new JLabel( "-6:00"));
879 labelTable.put( 0, new JLabel( "0:00"));
880 labelTable.put( 12, new JLabel( "6:00"));
881 labelTable.put( 24, new JLabel( "12:00"));
882 sldTimezone.setLabelTable(labelTable);
883
884 // Minutes Slider
885 final JLabel lblMinutes = new JLabel();
886 final JSlider sldMinutes = new JSlider(-15, 15, 0);
887 sldMinutes.setPaintLabels(true);
888 sldMinutes.setMajorTickSpacing(5);
889
890 // Seconds slider
891 final JLabel lblSeconds = new JLabel();
892 final JSlider sldSeconds = new JSlider(-60, 60, 0);
893 sldSeconds.setPaintLabels(true);
894 sldSeconds.setMajorTickSpacing(30);
895
896 // This is called whenever one of the sliders is moved.
897 // It updates the labels and also calls the "match photos" code
898 class SliderListener implements ChangeListener {
899 @Override
900 public void stateChanged(ChangeEvent e) {
901 // parse slider position into real timezone
902 double tz = Math.abs(sldTimezone.getValue());
903 String zone = tz % 2 == 0
904 ? (int)Math.floor(tz/2) + ":00"
905 : (int)Math.floor(tz/2) + ":30";
906 if(sldTimezone.getValue() < 0) {
907 zone = "-" + zone;
908 }
909
910 lblTimezone.setText(tr("Timezone: {0}", zone));
911 lblMinutes.setText(tr("Minutes: {0}", sldMinutes.getValue()));
912 lblSeconds.setText(tr("Seconds: {0}", sldSeconds.getValue()));
913
914 try {
915 timezone = parseTimezone(zone);
916 } catch (ParseException pe) {
917 throw new RuntimeException();
918 }
919 delta = sldMinutes.getValue()*60 + sldSeconds.getValue();
920
921 tfTimezone.getDocument().removeDocumentListener(statusBarUpdater);
922 tfOffset.getDocument().removeDocumentListener(statusBarUpdater);
923
924 tfTimezone.setText(formatTimezone(timezone));
925 tfOffset.setText(Long.toString(delta + 24*60*60L*dayOffset)); // add the day offset to the offset field
926
927 tfTimezone.getDocument().addDocumentListener(statusBarUpdater);
928 tfOffset.getDocument().addDocumentListener(statusBarUpdater);
929
930 lblMatches.setText(statusBarText.getText() + "<br>" + trn("(Time difference of {0} day)", "Time difference of {0} days", Math.abs(dayOffset), Math.abs(dayOffset)));
931
932 statusBarUpdater.updateStatusBar();
933 yLayer.updateBufferAndRepaint();
934 }
935 }
936
937 // Put everything together
938 JPanel p = new JPanel(new GridBagLayout());
939 p.setPreferredSize(new Dimension(400, 230));
940 p.add(lblMatches, GBC.eol().fill());
941 p.add(lblTimezone, GBC.eol().fill());
942 p.add(sldTimezone, GBC.eol().fill().insets(0, 0, 0, 10));
943 p.add(lblMinutes, GBC.eol().fill());
944 p.add(sldMinutes, GBC.eol().fill().insets(0, 0, 0, 10));
945 p.add(lblSeconds, GBC.eol().fill());
946 p.add(sldSeconds, GBC.eol().fill());
947
948 // If there's an error in the calculation the found values
949 // will be off range for the sliders. Catch this error
950 // and inform the user about it.
951 try {
952 sldTimezone.setValue((int)(fixTimezone*2));
953 sldMinutes.setValue(offset/60);
954 sldSeconds.setValue(offset%60);
955 } catch(Exception e) {
956 JOptionPane.showMessageDialog(Main.parent,
957 tr("An error occurred while trying to match the photos to the GPX track."
958 +" You can adjust the sliders to manually match the photos."),
959 tr("Matching photos to track failed"),
960 JOptionPane.WARNING_MESSAGE);
961 }
962
963 // Call the sliderListener once manually so labels get adjusted
964 new SliderListener().stateChanged(null);
965 // Listeners added here, otherwise it tries to match three times
966 // (when setting the default values)
967 sldTimezone.addChangeListener(new SliderListener());
968 sldMinutes.addChangeListener(new SliderListener());
969 sldSeconds.addChangeListener(new SliderListener());
970
971 // There is no way to cancel this dialog, all changes get applied
972 // immediately. Therefore "Close" is marked with an "OK" icon.
973 // Settings are only saved temporarily to the layer.
974 new ExtendedDialog(Main.parent,
975 tr("Adjust timezone and offset"),
976 new String[] { tr("Close")}).
977 setContent(p).setButtonIcons(new String[] {"ok.png"}).showDialog();
978 }
979 }
980
981 private class AutoGuessActionListener implements ActionListener {
982
983 @Override
984 public void actionPerformed(ActionEvent arg0) {
985 GpxDataWrapper gpxW = selectedGPX(true);
986 if (gpxW == null)
987 return;
988 GpxData gpx = gpxW.data;
989
990 List<ImageEntry> imgs = getSortedImgList();
991 PrimaryDateParser dateParser = new PrimaryDateParser();
992
993 // no images found, exit
994 if(imgs.size() <= 0) {
995 JOptionPane.showMessageDialog(Main.parent,
996 tr("The selected photos do not contain time information."),
997 tr("Photos do not contain time information"), JOptionPane.WARNING_MESSAGE);
998 return;
999 }
1000
1001 // Init variables
1002 long firstExifDate = imgs.get(0).getExifTime().getTime()/1000;
1003
1004 long firstGPXDate = -1;
1005 // Finds first GPX point
1006 outer: for (GpxTrack trk : gpx.tracks) {
1007 for (GpxTrackSegment segment : trk.getSegments()) {
1008 for (WayPoint curWp : segment.getWayPoints()) {
1009 String curDateWpStr = (String) curWp.attr.get("time");
1010 if (curDateWpStr == null) {
1011 continue;
1012 }
1013
1014 try {
1015 firstGPXDate = dateParser.parse(curDateWpStr).getTime()/1000;
1016 break outer;
1017 } catch(Exception e) {}
1018 }
1019 }
1020 }
1021
1022 // No GPX timestamps found, exit
1023 if(firstGPXDate < 0) {
1024 JOptionPane.showMessageDialog(Main.parent,
1025 tr("The selected GPX track does not contain timestamps. Please select another one."),
1026 tr("GPX Track has no time information"), JOptionPane.WARNING_MESSAGE);
1027 return;
1028 }
1029
1030 // seconds
1031 long diff = firstExifDate - firstGPXDate;
1032
1033 double diffInH = (double)diff/(60*60); // hours
1034
1035 // Find day difference
1036 int dayOffset = (int)Math.round(diffInH / 24); // days
1037 double tz = diff - dayOffset*24*60*60L; // seconds
1038
1039 // In hours, rounded to two decimal places
1040 tz = (double)Math.round(tz*100/(60*60)) / 100;
1041
1042 // Due to imprecise clocks we might get a "+3:28" timezone, which should obviously be 3:30 with
1043 // -2 minutes offset. This determines the real timezone and finds offset.
1044 timezone = (double)Math.round(tz * 2)/2; // hours, rounded to one decimal place
1045 delta = Math.round(diff - timezone*60*60); // seconds
1046
1047 tfTimezone.getDocument().removeDocumentListener(statusBarUpdater);
1048 tfOffset.getDocument().removeDocumentListener(statusBarUpdater);
1049
1050 tfTimezone.setText(formatTimezone(timezone));
1051 tfOffset.setText(Long.toString(delta));
1052 tfOffset.requestFocus();
1053
1054 tfTimezone.getDocument().addDocumentListener(statusBarUpdater);
1055 tfOffset.getDocument().addDocumentListener(statusBarUpdater);
1056
1057 statusBarUpdater.updateStatusBar();
1058 yLayer.updateBufferAndRepaint();
1059 }
1060 }
1061
1062 private List<ImageEntry> getSortedImgList() {
1063 return getSortedImgList(cbExifImg.isSelected(), cbTaggedImg.isSelected());
1064 }
1065
1066 /**
1067 * Returns a list of images that fulfill the given criteria.
1068 * Default setting is to return untagged images, but may be overwritten.
1069 * @param exif also returns images with exif-gps info
1070 * @param tagged also returns tagged images
1071 * @return matching images
1072 */
1073 private List<ImageEntry> getSortedImgList(boolean exif, boolean tagged) {
1074 List<ImageEntry> dateImgLst = new ArrayList<ImageEntry>(yLayer.data.size());
1075 for (ImageEntry e : yLayer.data) {
1076 if (e.getExifTime() == null) {
1077 continue;
1078 }
1079
1080 if (e.getExifCoor() != null) {
1081 if (!exif) {
1082 continue;
1083 }
1084 }
1085
1086 if (e.isTagged() && e.getExifCoor() == null) {
1087 if (!tagged) {
1088 continue;
1089 }
1090 }
1091
1092 dateImgLst.add(e);
1093 }
1094
1095 Collections.sort(dateImgLst, new Comparator<ImageEntry>() {
1096 @Override
1097 public int compare(ImageEntry arg0, ImageEntry arg1) {
1098 return arg0.getExifTime().compareTo(arg1.getExifTime());
1099 }
1100 });
1101
1102 return dateImgLst;
1103 }
1104
1105 private GpxDataWrapper selectedGPX(boolean complain) {
1106 Object item = cbGpx.getSelectedItem();
1107
1108 if (item == null || ((GpxDataWrapper) item).file == null) {
1109 if (complain) {
1110 JOptionPane.showMessageDialog(Main.parent, tr("You should select a GPX track"),
1111 tr("No selected GPX track"), JOptionPane.ERROR_MESSAGE );
1112 }
1113 return null;
1114 }
1115 return (GpxDataWrapper) item;
1116 }
1117
1118 /**
1119 * Match a list of photos to a gpx track with a given offset.
1120 * All images need a exifTime attribute and the List must be sorted according to these times.
1121 */
1122 private int matchGpxTrack(List<ImageEntry> images, GpxData selectedGpx, long offset) {
1123 int ret = 0;
1124
1125 PrimaryDateParser dateParser = new PrimaryDateParser();
1126
1127 for (GpxTrack trk : selectedGpx.tracks) {
1128 for (GpxTrackSegment segment : trk.getSegments()) {
1129
1130 long prevWpTime = 0;
1131 WayPoint prevWp = null;
1132
1133 for (WayPoint curWp : segment.getWayPoints()) {
1134
1135 String curWpTimeStr = (String) curWp.attr.get("time");
1136 if (curWpTimeStr != null) {
1137
1138 try {
1139 long curWpTime = dateParser.parse(curWpTimeStr).getTime() + offset;
1140 ret += matchPoints(images, prevWp, prevWpTime, curWp, curWpTime, offset);
1141
1142 prevWp = curWp;
1143 prevWpTime = curWpTime;
1144
1145 } catch(ParseException e) {
1146 Main.error("Error while parsing date \"" + curWpTimeStr + '"');
1147 e.printStackTrace();
1148 prevWp = null;
1149 prevWpTime = 0;
1150 }
1151 } else {
1152 prevWp = null;
1153 prevWpTime = 0;
1154 }
1155 }
1156 }
1157 }
1158 return ret;
1159 }
1160
1161 private int matchPoints(List<ImageEntry> images, WayPoint prevWp, long prevWpTime,
1162 WayPoint curWp, long curWpTime, long offset) {
1163 // Time between the track point and the previous one, 5 sec if first point, i.e. photos take
1164 // 5 sec before the first track point can be assumed to be take at the starting position
1165 long interval = prevWpTime > 0 ? ((long)Math.abs(curWpTime - prevWpTime)) : 5*1000;
1166 int ret = 0;
1167
1168 // i is the index of the timewise last photo that has the same or earlier EXIF time
1169 int i = getLastIndexOfListBefore(images, curWpTime);
1170
1171 // no photos match
1172 if (i < 0)
1173 return 0;
1174
1175 Double speed = null;
1176 Double prevElevation = null;
1177 Double curElevation = null;
1178
1179 if (prevWp != null) {
1180 double distance = prevWp.getCoor().greatCircleDistance(curWp.getCoor());
1181 // This is in km/h, 3.6 * m/s
1182 if (curWpTime > prevWpTime) {
1183 speed = 3600 * distance / (curWpTime - prevWpTime);
1184 }
1185 try {
1186 prevElevation = new Double((String) prevWp.attr.get("ele"));
1187 } catch(Exception e) {}
1188 }
1189
1190 try {
1191 curElevation = new Double((String) curWp.attr.get("ele"));
1192 } catch (Exception e) {}
1193
1194 // First trackpoint, then interval is set to five seconds, i.e. photos up to five seconds
1195 // before the first point will be geotagged with the starting point
1196 if(prevWpTime == 0 || curWpTime <= prevWpTime) {
1197 while (true) {
1198 if (i < 0) {
1199 break;
1200 }
1201 final ImageEntry curImg = images.get(i);
1202 if (curImg.getExifTime().getTime() > curWpTime
1203 || curImg.getExifTime().getTime() < curWpTime - interval) {
1204 break;
1205 }
1206 if(curImg.tmp.getPos() == null) {
1207 curImg.tmp.setPos(curWp.getCoor());
1208 curImg.tmp.setSpeed(speed);
1209 curImg.tmp.setElevation(curElevation);
1210 curImg.tmp.setGpsTime(new Date(curImg.getExifTime().getTime() - offset));
1211 ret++;
1212 }
1213 i--;
1214 }
1215 return ret;
1216 }
1217
1218 // This code gives a simple linear interpolation of the coordinates between current and
1219 // previous track point assuming a constant speed in between
1220 while (true) {
1221 if (i < 0) {
1222 break;
1223 }
1224 ImageEntry curImg = images.get(i);
1225 long imgTime = curImg.getExifTime().getTime();
1226 if (imgTime < prevWpTime) {
1227 break;
1228 }
1229
1230 if(curImg.tmp.getPos() == null) {
1231 // The values of timeDiff are between 0 and 1, it is not seconds but a dimensionless
1232 // variable
1233 double timeDiff = (double)(imgTime - prevWpTime) / interval;
1234 curImg.tmp.setPos(prevWp.getCoor().interpolate(curWp.getCoor(), timeDiff));
1235 curImg.tmp.setSpeed(speed);
1236 if (curElevation != null && prevElevation != null) {
1237 curImg.tmp.setElevation(prevElevation + (curElevation - prevElevation) * timeDiff);
1238 }
1239 curImg.tmp.setGpsTime(new Date(curImg.getExifTime().getTime() - offset));
1240
1241 ret++;
1242 }
1243 i--;
1244 }
1245 return ret;
1246 }
1247
1248 private int getLastIndexOfListBefore(List<ImageEntry> images, long searchedTime) {
1249 int lstSize= images.size();
1250
1251 // No photos or the first photo taken is later than the search period
1252 if(lstSize == 0 || searchedTime < images.get(0).getExifTime().getTime())
1253 return -1;
1254
1255 // The search period is later than the last photo
1256 if (searchedTime > images.get(lstSize - 1).getExifTime().getTime())
1257 return lstSize-1;
1258
1259 // The searched index is somewhere in the middle, do a binary search from the beginning
1260 int curIndex= 0;
1261 int startIndex= 0;
1262 int endIndex= lstSize-1;
1263 while (endIndex - startIndex > 1) {
1264 curIndex= (endIndex + startIndex) / 2;
1265 if (searchedTime > images.get(curIndex).getExifTime().getTime()) {
1266 startIndex= curIndex;
1267 } else {
1268 endIndex= curIndex;
1269 }
1270 }
1271 if (searchedTime < images.get(endIndex).getExifTime().getTime())
1272 return startIndex;
1273
1274 // This final loop is to check if photos with the exact same EXIF time follows
1275 while ((endIndex < (lstSize-1)) && (images.get(endIndex).getExifTime().getTime()
1276 == images.get(endIndex + 1).getExifTime().getTime())) {
1277 endIndex++;
1278 }
1279 return endIndex;
1280 }
1281
1282 private String formatTimezone(double timezone) {
1283 StringBuffer ret = new StringBuffer();
1284
1285 if (timezone < 0) {
1286 ret.append('-');
1287 timezone = -timezone;
1288 } else {
1289 ret.append('+');
1290 }
1291 ret.append((long) timezone).append(':');
1292 int minutes = (int) ((timezone % 1) * 60);
1293 if (minutes < 10) {
1294 ret.append('0');
1295 }
1296 ret.append(minutes);
1297
1298 return ret.toString();
1299 }
1300
1301 private double parseTimezone(String timezone) throws ParseException {
1302
1303 String error = tr("Error while parsing timezone.\nExpected format: {0}", "+H:MM");
1304
1305 if (timezone.length() == 0)
1306 return 0;
1307
1308 char sgnTimezone = '+';
1309 StringBuffer hTimezone = new StringBuffer();
1310 StringBuffer mTimezone = new StringBuffer();
1311 int state = 1; // 1=start/sign, 2=hours, 3=minutes.
1312 for (int i = 0; i < timezone.length(); i++) {
1313 char c = timezone.charAt(i);
1314 switch (c) {
1315 case ' ' :
1316 if (state != 2 || hTimezone.length() != 0)
1317 throw new ParseException(error,0);
1318 break;
1319 case '+' :
1320 case '-' :
1321 if (state == 1) {
1322 sgnTimezone = c;
1323 state = 2;
1324 } else
1325 throw new ParseException(error,0);
1326 break;
1327 case ':' :
1328 case '.' :
1329 if (state == 2) {
1330 state = 3;
1331 } else
1332 throw new ParseException(error,0);
1333 break;
1334 case '0' : case '1' : case '2' : case '3' : case '4' :
1335 case '5' : case '6' : case '7' : case '8' : case '9' :
1336 switch(state) {
1337 case 1 :
1338 case 2 :
1339 state = 2;
1340 hTimezone.append(c);
1341 break;
1342 case 3 :
1343 mTimezone.append(c);
1344 break;
1345 default :
1346 throw new ParseException(error,0);
1347 }
1348 break;
1349 default :
1350 throw new ParseException(error,0);
1351 }
1352 }
1353
1354 int h = 0;
1355 int m = 0;
1356 try {
1357 h = Integer.parseInt(hTimezone.toString());
1358 if (mTimezone.length() > 0) {
1359 m = Integer.parseInt(mTimezone.toString());
1360 }
1361 } catch (NumberFormatException nfe) {
1362 // Invalid timezone
1363 throw new ParseException(error,0);
1364 }
1365
1366 if (h > 12 || m > 59 )
1367 throw new ParseException(error,0);
1368 else
1369 return (h + m / 60.0) * (sgnTimezone == '-' ? -1 : 1);
1370 }
1371
1372 private long parseOffset(String offset) throws ParseException {
1373 String error = tr("Error while parsing offset.\nExpected format: {0}", "number");
1374
1375 if (offset.length() > 0) {
1376 try {
1377 if(offset.startsWith("+")) {
1378 offset = offset.substring(1);
1379 }
1380 return Long.parseLong(offset);
1381 } catch(NumberFormatException nfe) {
1382 throw new ParseException(error,0);
1383 }
1384 } else
1385 return 0;
1386 }
1387}
Note: See TracBrowser for help on using the repository browser.