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

Last change on this file since 7005 was 7005, checked in by Don-vip, 10 years ago

see #8465 - use diamond operator where applicable

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