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

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

fix #12209 - CorrelateGpxWithImages refactor (patch by Bjoeni)

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