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

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

enable PMD rule OptimizableToArrayCall

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