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