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

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

fix #10024 - Add an option in Preferences/Look-and-Feel to use native file-choosing dialogs.
They look nicer but they do not support file filters, so we cannot use them (yet) as default.
Based on patch by Lesath and code review by simon04.
The native dialogs are not used if selection mode is not supported ("files and directories" on all platforms, "directories" on systems other than OS X)

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