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