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

Last change on this file since 2907 was 2907, checked in by jttt, 14 years ago

Gpx refactoring - GpxTrack and GpxTrackSegment is now interface, implementations for specific use can be provided (currently JOSM supports immutable gpx track, livegps plugin supports append only track).

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