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

Last change on this file since 5891 was 5891, checked in by stoecker, 11 years ago

fix javadoc

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