Ticket #16681: GPXCorrelate.patch
File GPXCorrelate.patch, 35.3 KB (added by , 6 years ago) |
---|
-
src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java
5 5 import static org.openstreetmap.josm.tools.I18n.trn; 6 6 7 7 import java.awt.BorderLayout; 8 import java.awt.Component; 8 9 import java.awt.Cursor; 9 10 import java.awt.Dimension; 10 11 import java.awt.FlowLayout; … … 43 44 import javax.swing.BorderFactory; 44 45 import javax.swing.JButton; 45 46 import javax.swing.JCheckBox; 47 import javax.swing.JComponent; 46 48 import javax.swing.JFileChooser; 47 49 import javax.swing.JLabel; 48 50 import javax.swing.JList; … … 51 53 import javax.swing.JScrollPane; 52 54 import javax.swing.JSeparator; 53 55 import javax.swing.JSlider; 56 import javax.swing.JSpinner; 54 57 import javax.swing.ListSelectionModel; 55 58 import javax.swing.MutableComboBoxModel; 59 import javax.swing.SpinnerNumberModel; 56 60 import javax.swing.SwingConstants; 61 import javax.swing.border.Border; 57 62 import javax.swing.event.ChangeEvent; 58 63 import javax.swing.event.ChangeListener; 59 64 import javax.swing.event.DocumentEvent; … … 74 79 import org.openstreetmap.josm.gui.io.importexport.NMEAImporter; 75 80 import org.openstreetmap.josm.gui.layer.GpxLayer; 76 81 import org.openstreetmap.josm.gui.layer.Layer; 82 import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; 83 import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; 84 import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; 85 import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; 77 86 import org.openstreetmap.josm.gui.widgets.AbstractFileChooser; 78 87 import org.openstreetmap.josm.gui.widgets.FileChooserManager; 79 88 import org.openstreetmap.josm.gui.widgets.JosmComboBox; … … 83 92 import org.openstreetmap.josm.io.IGpxReader; 84 93 import org.openstreetmap.josm.io.nmea.NmeaReader; 85 94 import org.openstreetmap.josm.spi.preferences.Config; 95 import org.openstreetmap.josm.spi.preferences.IPreferences; 86 96 import org.openstreetmap.josm.tools.GBC; 87 97 import org.openstreetmap.josm.tools.ImageProvider; 88 98 import org.openstreetmap.josm.tools.JosmRuntimeException; … … 102 112 private final transient GeoImageLayer yLayer; 103 113 private transient Timezone timezone; 104 114 private transient Offset delta; 115 private static boolean forceTags = false; 105 116 106 117 /** 107 118 * Constructs a new {@code CorrelateGpxWithImages} action. … … 111 122 super(tr("Correlate to GPX")); 112 123 new ImageProvider("dialogs/geoimage/gpx2img").getResource().attachImageIcon(this, true); 113 124 this.yLayer = layer; 125 MainApplication.getLayerManager().addLayerChangeListener(new GpxLayerAddedListener()); 114 126 } 115 127 116 128 private final class SyncDialogWindowListener extends WindowAdapter { … … 322 334 } 323 335 } 324 336 337 private class AdvancedSettingsActionListener implements ActionListener { 338 339 private class CheckBoxActionListener implements ActionListener { 340 private JComponent[] comps; 341 342 CheckBoxActionListener(JComponent... c) { 343 comps = c; 344 } 345 346 @Override 347 public void actionPerformed(ActionEvent arg0) { 348 setEnabled((JCheckBox) arg0.getSource()); 349 } 350 351 public void setEnabled(JCheckBox cb) { 352 for (JComponent comp : comps) { 353 if (comp instanceof JSpinner) { 354 comp.setEnabled(cb.isSelected()); 355 } else if (comp instanceof JPanel) { 356 boolean en = cb.isSelected(); 357 for (Component c : comp.getComponents()) { 358 if (c instanceof JSpinner) { 359 c.setEnabled(en); 360 } else { 361 c.setEnabled(cb.isSelected()); 362 if (en && c instanceof JCheckBox) { 363 en = ((JCheckBox) c).isSelected(); 364 } 365 } 366 } 367 } 368 } 369 370 } 371 } 372 373 private void addCheckBoxActionListener(JCheckBox cb, JComponent... c) { 374 CheckBoxActionListener listener = new CheckBoxActionListener(c); 375 cb.addActionListener(listener); 376 listener.setEnabled(cb); 377 } 378 379 @Override 380 public void actionPerformed(ActionEvent arg0) { 381 382 IPreferences s = Config.getPref(); 383 JPanel p = new JPanel(new GridBagLayout()); 384 385 Border border1 = BorderFactory.createEmptyBorder(0, 20, 0, 0); 386 Border border2 = BorderFactory.createEmptyBorder(10, 0, 5, 0); 387 Border border = BorderFactory.createEmptyBorder(0, 40, 0, 0); 388 FlowLayout layout = new FlowLayout(); 389 390 JLabel l = new JLabel(tr("Segment settings")); 391 l.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0)); 392 p.add(l, GBC.eol()); 393 JCheckBox cInterpolSeg = new JCheckBox(tr("Interpolate between segments"), s.getBoolean("geoimage.seg.int", true)); 394 cInterpolSeg.setBorder(border1); 395 p.add(cInterpolSeg, GBC.eol()); 396 397 JCheckBox cInterpolSegTime = new JCheckBox(tr("only when the segments are less than"), s.getBoolean("geoimage.seg.int.time", true)); 398 JSpinner sInterpolSegTime = new JSpinner(new SpinnerNumberModel(s.getInt("geoimage.seg.int.time.val", 60), 0, Integer.MAX_VALUE, 1)); 399 ((JSpinner.DefaultEditor) sInterpolSegTime.getEditor()).getTextField().setColumns(3); 400 JLabel lInterpolSegTime = new JLabel(tr("minutes apart")); 401 JPanel pInterpolSegTime = new JPanel(layout); 402 pInterpolSegTime.add(cInterpolSegTime); 403 pInterpolSegTime.add(sInterpolSegTime); 404 pInterpolSegTime.add(lInterpolSegTime); 405 pInterpolSegTime.setBorder(border); 406 p.add(pInterpolSegTime, GBC.eol()); 407 408 JCheckBox cInterpolSegDist = new JCheckBox(tr("only when the segments are less than"), s.getBoolean("geoimage.seg.int.dist", true)); 409 JSpinner sInterpolSegDist = new JSpinner(new SpinnerNumberModel(s.getInt("geoimage.seg.int.dist.val", 50), 0, Integer.MAX_VALUE, 1)); 410 ((JSpinner.DefaultEditor) sInterpolSegDist.getEditor()).getTextField().setColumns(3); 411 JLabel lInterpolSegDist = new JLabel(tr("metres apart")); 412 JPanel pInterpolSegDist = new JPanel(layout); 413 pInterpolSegDist.add(cInterpolSegDist); 414 pInterpolSegDist.add(sInterpolSegDist); 415 pInterpolSegDist.add(lInterpolSegDist); 416 pInterpolSegDist.setBorder(border); 417 p.add(pInterpolSegDist, GBC.eol()); 418 419 JCheckBox cTagSeg = new JCheckBox(tr("Tag images at the closest end of a segment, when not interpolated"), s.getBoolean("geoimage.seg.tag", true)); 420 cTagSeg.setBorder(border1); 421 p.add(cTagSeg, GBC.eol()); 422 423 JCheckBox cTagSegTime = new JCheckBox(tr("only within"), s.getBoolean("geoimage.seg.tag.time", true)); 424 JSpinner sTagSegTime = new JSpinner(new SpinnerNumberModel(s.getInt("geoimage.seg.tag.time.val", 2), 0, Integer.MAX_VALUE, 1)); 425 ((JSpinner.DefaultEditor) sTagSegTime.getEditor()).getTextField().setColumns(3); 426 JLabel lTagSegTime = new JLabel(tr("minutes of the closest trackpoint")); 427 JPanel pTagSegTime = new JPanel(layout); 428 pTagSegTime.add(cTagSegTime); 429 pTagSegTime.add(sTagSegTime); 430 pTagSegTime.add(lTagSegTime); 431 pTagSegTime.setBorder(border); 432 p.add(pTagSegTime, GBC.eol()); 433 434 l = new JLabel(tr("Track settings (note that multiple tracks can be in one GPX file)")); 435 l.setBorder(border2); 436 p.add(l, GBC.eol()); 437 JCheckBox cInterpolTrack = new JCheckBox(tr("Interpolate between tracks"), s.getBoolean("geoimage.trk.int", false)); 438 cInterpolTrack.setBorder(border1); 439 p.add(cInterpolTrack, GBC.eol()); 440 441 JCheckBox cInterpolTrackTime = new JCheckBox(tr("only when the tracks are less than"), s.getBoolean("geoimage.trk.int.time", false)); 442 JSpinner sInterpolTrackTime = new JSpinner(new SpinnerNumberModel(s.getInt("geoimage.trk.int.time.val", 60), 0, Integer.MAX_VALUE, 1)); 443 ((JSpinner.DefaultEditor) sInterpolTrackTime.getEditor()).getTextField().setColumns(3); 444 JLabel lInterpolTrackTime = new JLabel(tr("minutes apart")); 445 JPanel pInterpolTrackTime = new JPanel(layout); 446 pInterpolTrackTime.add(cInterpolTrackTime); 447 pInterpolTrackTime.add(sInterpolTrackTime); 448 pInterpolTrackTime.add(lInterpolTrackTime); 449 pInterpolTrackTime.setBorder(border); 450 p.add(pInterpolTrackTime, GBC.eol()); 451 452 JCheckBox cInterpolTrackDist = new JCheckBox(tr("only when the tracks are less than"), s.getBoolean("geoimage.trk.int.dist", false)); 453 JSpinner sInterpolTrackDist = new JSpinner(new SpinnerNumberModel(s.getInt("geoimage.trk.int.dist.val", 50), 0, Integer.MAX_VALUE, 1)); 454 ((JSpinner.DefaultEditor) sInterpolTrackDist.getEditor()).getTextField().setColumns(3); 455 JLabel lInterpolTrackDist = new JLabel(tr("metres apart")); 456 JPanel pInterpolTrackDist = new JPanel(layout); 457 pInterpolTrackDist.add(cInterpolTrackDist); 458 pInterpolTrackDist.add(sInterpolTrackDist); 459 pInterpolTrackDist.add(lInterpolTrackDist); 460 pInterpolTrackDist.setBorder(border); 461 p.add(pInterpolTrackDist, GBC.eol()); 462 463 JCheckBox cTagTrack = new JCheckBox(tr("<html>Tag images at the closest end of a track, when not interpolated<br>" + 464 "(also applies before the first and after the last track)</html>"), 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"), s.getBoolean("geoimage.trk.tag.time", true)); 469 JSpinner sTagTrackTime = new JSpinner(new SpinnerNumberModel(s.getInt("geoimage.trk.tag.time.val", 2), 0, Integer.MAX_VALUE, 1)); 470 ((JSpinner.DefaultEditor) sTagTrackTime.getEditor()).getTextField().setColumns(3); 471 JLabel lTagTrackTime = new JLabel(tr("minutes of the closest trackpoint")); 472 JPanel pTagTrackTime = new JPanel(layout); 473 pTagTrackTime.add(cTagTrackTime); 474 pTagTrackTime.add(sTagTrackTime); 475 pTagTrackTime.add(lTagTrackTime); 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(tr("<html>Force tagging of all pictures (temporarily overrides the settings above)<br>This option will not be saved permanently</html>"), forceTags); 483 cForce.setBorder(BorderFactory.createEmptyBorder(0, 20, 10, 0)); 484 p.add(cForce, GBC.eol()); 485 486 addCheckBoxActionListener(cInterpolSegTime, sInterpolSegTime); 487 addCheckBoxActionListener(cInterpolSegDist, sInterpolSegDist); 488 addCheckBoxActionListener(cInterpolSeg, pInterpolSegTime, pInterpolSegDist); 489 490 addCheckBoxActionListener(cTagSegTime, sTagSegTime); 491 addCheckBoxActionListener(cTagSeg, pTagSegTime); 492 493 addCheckBoxActionListener(cInterpolTrackTime, sInterpolTrackTime); 494 addCheckBoxActionListener(cInterpolTrackDist, sInterpolTrackDist); 495 addCheckBoxActionListener(cInterpolTrack, pInterpolTrackTime, pInterpolTrackDist); 496 497 addCheckBoxActionListener(cTagTrackTime, sTagTrackTime); 498 addCheckBoxActionListener(cTagTrack, pTagTrackTime); 499 500 501 ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(), tr("Advanced settings"), tr("OK"), tr("Cancel")) 502 .setButtonIcons("ok", "cancel").setContent(p); 503 if (ed.showDialog().getValue() == 1) { 504 505 s.putBoolean("geoimage.seg.int", cInterpolSeg.isSelected()); 506 s.putBoolean("geoimage.seg.int.dist", cInterpolSegDist.isSelected()); 507 s.putInt("geoimage.seg.int.dist.val", (int) sInterpolSegDist.getValue()); 508 s.putBoolean("geoimage.seg.int.time", cInterpolSegTime.isSelected()); 509 s.putInt("geoimage.seg.int.time.val", (int) sInterpolSegTime.getValue()); 510 s.putBoolean("geoimage.seg.tag", cTagSeg.isSelected()); 511 s.putBoolean("geoimage.seg.tag.time", cTagSegTime.isSelected()); 512 s.putInt("geoimage.seg.tag.time.val", (int) sTagSegTime.getValue()); 513 514 s.putBoolean("geoimage.trk.int", cInterpolTrack.isSelected()); 515 s.putBoolean("geoimage.trk.int.dist", cInterpolTrackDist.isSelected()); 516 s.putInt("geoimage.trk.int.dist.val", (int) sInterpolTrackDist.getValue()); 517 s.putBoolean("geoimage.trk.int.time", cInterpolTrackTime.isSelected()); 518 s.putInt("geoimage.trk.int.time.val", (int) sInterpolTrackTime.getValue()); 519 s.putBoolean("geoimage.trk.tag", cTagTrack.isSelected()); 520 s.putBoolean("geoimage.trk.tag.time", cTagTrackTime.isSelected()); 521 s.putInt("geoimage.trk.tag.time.val", (int) sTagTrackTime.getValue()); 522 523 forceTags = cForce.isSelected(); // This setting is not supposed to be saved permanently 524 525 statusBarUpdater.updateStatusBar(); 526 yLayer.updateBufferAndRepaint(); 527 } 528 } 529 530 } 531 325 532 /** 326 533 * This action listener is called when the user has a photo of the time of his GPS receiver. It 327 534 * displays the list of photos of the layer, and upon selection displays the selected photo. … … 525 732 } 526 733 } 527 734 735 private class GpxLayerAddedListener implements LayerChangeListener { 736 @Override 737 public void layerAdded(LayerAddEvent e) { 738 if (syncDialog != null && syncDialog.isVisible()) { 739 Layer layer = e.getAddedLayer(); 740 if (layer instanceof GpxLayer) { 741 GpxLayer gpx = (GpxLayer) layer; 742 GpxDataWrapper gdw = new GpxDataWrapper(gpx.getName(), gpx.data, gpx.data.storageFile); 743 gpxLst.add(gdw); 744 MutableComboBoxModel<GpxDataWrapper> model = (MutableComboBoxModel<GpxDataWrapper>) cbGpx.getModel(); 745 if (gpxLst.get(0).file == null) { 746 gpxLst.remove(0); 747 model.removeElementAt(0); 748 } 749 model.addElement(gdw); 750 } 751 } 752 } 753 @Override 754 public void layerRemoving(LayerRemoveEvent e) {} 755 @Override 756 public void layerOrderChanged(LayerOrderChangeEvent e) {} 757 } 758 528 759 @Override 529 760 public void actionPerformed(ActionEvent ae) { 530 761 // Construct the list of loaded GPX tracks … … 606 837 JButton buttonAdjust = new JButton(tr("Manual adjust")); 607 838 buttonAdjust.addActionListener(new AdjustActionListener()); 608 839 840 JButton buttonAdvanced = new JButton(tr("Advanced settings...")); 841 buttonAdvanced.addActionListener(new AdvancedSettingsActionListener()); 842 609 843 JLabel labelPosition = new JLabel(tr("Override position for: ")); 610 844 611 845 int numAll = getSortedImgList(true, true).size(); … … 666 900 gbc.weightx = 0.5; 667 901 panelTf.add(buttonViewGpsPhoto, gbc); 668 902 903 669 904 gbc = GBC.std().fill(GBC.BOTH).insets(5, 5, 5, 5); 670 gbc.gridx = 2;905 gbc.gridx = 1; 671 906 gbc.gridy = y++; 672 907 gbc.weightx = 0.5; 908 panelTf.add(buttonAdvanced, gbc); 909 910 gbc.gridx = 2; 673 911 panelTf.add(buttonAutoGuess, gbc); 674 912 675 913 gbc.gridx = 3; … … 715 953 cbTaggedImg.addItemListener(statusBarUpdaterWithRepaint); 716 954 717 955 statusBarUpdater.updateStatusBar(); 956 yLayer.updateBufferAndRepaint(); 718 957 719 958 outerPanel = new JPanel(new BorderLayout()); 720 959 outerPanel.add(statusBar, BorderLayout.PAGE_END); … … 807 1046 if (selGpx == null) 808 1047 return tr("No gpx selected"); 809 1048 810 final long offsetMs = ((long) (timezone.getHours() * TimeUnit.HOURS.toMillis( 1))) + delta.getMilliseconds(); // in milliseconds1049 final long offsetMs = ((long) (timezone.getHours() * TimeUnit.HOURS.toMillis(-1))) + delta.getMilliseconds(); // in milliseconds 811 1050 lastNumMatched = matchGpxTrack(dateImgLst, selGpx.data, offsetMs); 812 1051 813 1052 return trn("<html>Matched <b>{0}</b> of <b>{1}</b> photo to GPX track.</html>", … … 1098 1337 static int matchGpxTrack(List<ImageEntry> images, GpxData selectedGpx, long offset) { 1099 1338 int ret = 0; 1100 1339 1340 long prevWpTime = 0; 1341 WayPoint prevWp = null; 1342 1343 List<List<List<WayPoint>>> trks = new ArrayList<>(); 1344 1101 1345 for (GpxTrack trk : selectedGpx.tracks) { 1102 for (GpxTrackSegment segment : trk.getSegments()) { 1346 List<List<WayPoint>> segs = new ArrayList<>(); 1347 for (GpxTrackSegment seg : trk.getSegments()) { 1348 List<WayPoint> wps = new ArrayList<>(seg.getWayPoints()); 1349 if (!wps.isEmpty()) { 1350 //remove waypoints at the beginning of the track/segment without timestamps 1351 int wp; 1352 for (wp = 0; wp < wps.size(); wp++) { 1353 if (wps.get(wp).setTimeFromAttribute() != null) { 1354 break; 1355 } 1356 } 1357 if (wp == 0) { 1358 segs.add(wps); 1359 } else if (wp < wps.size()) { 1360 segs.add(wps.subList(wp, wps.size())); 1361 } 1362 } 1363 } 1364 //sort segments by first waypoint 1365 if (!segs.isEmpty()) { 1366 segs.sort(new Comparator<List<WayPoint>>() { 1367 @Override 1368 public int compare(List<WayPoint> arg0, List<WayPoint> arg1) { 1369 if (arg0.isEmpty() || arg1.isEmpty() || arg0.get(0).time == arg1.get(0).time) 1370 return 0; 1371 return arg0.get(0).time < arg1.get(0).time ? -1 : 1; 1372 } 1373 }); 1374 trks.add(segs); 1375 } 1376 } 1377 //sort tracks by first waypoint of first segment 1378 trks.sort(new Comparator<List<List<WayPoint>>>() { 1379 @Override 1380 public int compare(List<List<WayPoint>> arg0, List<List<WayPoint>> arg1) { 1381 if (arg0.isEmpty() || arg0.get(0).isEmpty() 1382 || arg1.isEmpty() || arg1.get(0).isEmpty() 1383 || arg0.get(0).get(0).time == arg1.get(0).get(0).time) 1384 return 0; 1385 return arg0.get(0).get(0).time < arg1.get(0).get(0).time ? -1 : 1; 1386 } 1387 }); 1103 1388 1104 long prevWpTime = 0;1105 WayPoint prevWp = null;1389 boolean trkInt, trkTag, segInt, segTag; 1390 int trkTime, trkDist, trkTagTime, segTime, segDist, segTagTime; 1106 1391 1107 for (WayPoint curWp : segment.getWayPoints()) { 1108 final Date parsedTime = curWp.setTimeFromAttribute(); 1109 if (parsedTime != null) { 1110 final long curWpTime = parsedTime.getTime() + offset; 1111 ret += matchPoints(images, prevWp, prevWpTime, curWp, curWpTime, offset); 1392 if (forceTags) { //temporary option to override advanced settings and activate all possible interpolations / tagging methods 1393 trkInt = trkTag = segInt = segTag = true; 1394 trkTime = trkDist = trkTagTime = segTime = segDist = segTagTime = Integer.MAX_VALUE; 1395 } else { 1396 // Load the settings 1397 trkInt = Config.getPref().getBoolean("geoimage.trk.int", false); 1398 trkTime = Config.getPref().getBoolean("geoimage.trk.int.time", false) ? Config.getPref().getInt("geoimage.trk.int.time.val", 60) : Integer.MAX_VALUE; 1399 trkDist = Config.getPref().getBoolean("geoimage.trk.int.dist", false) ? Config.getPref().getInt("geoimage.trk.int.dist.val", 50) : Integer.MAX_VALUE; 1112 1400 1113 prevWp = curWp; 1114 prevWpTime = curWpTime; 1115 continue; 1401 trkTag = Config.getPref().getBoolean("geoimage.trk.tag", true); 1402 trkTagTime = Config.getPref().getBoolean("geoimage.trk.tag.time", true) ? Config.getPref().getInt("geoimage.trk.tag.time.val", 2) : Integer.MAX_VALUE; 1403 1404 segInt = Config.getPref().getBoolean("geoimage.seg.int", true); 1405 segTime = Config.getPref().getBoolean("geoimage.seg.int.time", true) ? Config.getPref().getInt("geoimage.seg.int.time.val", 60) : Integer.MAX_VALUE; 1406 segDist = Config.getPref().getBoolean("geoimage.seg.int.dist", true) ? Config.getPref().getInt("geoimage.seg.int.dist.val", 50) : Integer.MAX_VALUE; 1407 1408 segTag = Config.getPref().getBoolean("geoimage.seg.tag", true); 1409 segTagTime = Config.getPref().getBoolean("geoimage.seg.tag.time", true) ? Config.getPref().getInt("geoimage.seg.tag.time.val", 2) : Integer.MAX_VALUE; 1410 } 1411 boolean isFirst = true; 1412 1413 for (int t = 0; t < trks.size(); t++) { 1414 List<List<WayPoint>> segs = trks.get(t); 1415 for (int s = 0; s < segs.size(); s++) { 1416 List<WayPoint> wps = segs.get(s); 1417 for (int i = 0; i < wps.size(); i++) { 1418 WayPoint curWp = wps.get(i); 1419 Date parsedTime = curWp.setTimeFromAttribute(); 1420 // Interpolate timestamps in the segment, if one or more waypoints miss them 1421 if (parsedTime == null) { 1422 //check if any of the following waypoints has a timestamp... 1423 if (i > 0 && wps.get(i - 1).time != 0) { 1424 long prevWpTimeNoOffset = wps.get(i - 1).getTime().getTime(); 1425 double totalDist = 0; 1426 List<Pair<Double, WayPoint>> nextWps = new ArrayList<>(); 1427 for (int j = i; j < wps.size(); j++) { 1428 totalDist += wps.get(j - 1).getCoor().greatCircleDistance(wps.get(j).getCoor()); 1429 nextWps.add(new Pair<>(totalDist, wps.get(j))); 1430 final Date nextTime = wps.get(j).setTimeFromAttribute(); 1431 if (nextTime != null) { 1432 // ...if yes, interpolate everything in between 1433 long timeDiff = nextTime.getTime() - prevWpTimeNoOffset; 1434 for (Pair<Double, WayPoint> pair : nextWps) { 1435 pair.b.setTime(new Date((long) (prevWpTimeNoOffset + (timeDiff * (pair.a / totalDist))))); 1436 } 1437 break; 1438 } 1439 } 1440 parsedTime = curWp.setTimeFromAttribute(); 1441 if (parsedTime == null) { 1442 break; //It's pointless to continue with this segment, because none of the following waypoints had a timestamp 1443 } 1444 } else { 1445 continue; // Timestamps on waypoints without preceding timestamps in the same segment can not be interpolated, so try next one 1446 } 1116 1447 } 1117 prevWp = null; 1118 prevWpTime = 0; 1448 1449 final long curWpTime = parsedTime.getTime() + offset; 1450 boolean interpolate = true; 1451 int tagTime = 0; 1452 if (i == 0) { 1453 if (s == 0) { //First segment of the track, so apply settings for tracks 1454 if (!trkInt || isFirst || prevWp == null || Math.abs(curWpTime - prevWpTime) > trkTime || prevWp.getCoor().greatCircleDistance(curWp.getCoor()) > trkDist) { 1455 isFirst = false; 1456 interpolate = false; 1457 if (trkTag) { 1458 tagTime = trkTagTime; 1459 } 1460 } 1461 } else { //Apply settings for segments 1462 if (!segInt || prevWp == null || Math.abs(curWpTime - prevWpTime) > segTime || prevWp.getCoor().greatCircleDistance(curWp.getCoor()) > segDist) { 1463 interpolate = false; 1464 if (segTag) { 1465 tagTime = segTagTime; 1466 } 1467 } 1468 } 1469 } 1470 ret += matchPoints(images, prevWp, prevWpTime, curWp, curWpTime, offset, interpolate, tagTime, false); 1471 prevWp = curWp; 1472 prevWpTime = curWpTime; 1119 1473 } 1120 1474 } 1121 1475 } 1476 if (trkTag) { 1477 ret += matchPoints(images, prevWp, prevWpTime, prevWp, prevWpTime, offset, false, trkTagTime, true); 1478 } 1122 1479 return ret; 1123 1480 } 1124 1481 … … 1134 1491 return null; 1135 1492 } 1136 1493 1137 static int matchPoints(List<ImageEntry> images, WayPoint prevWp, long prevWpTime, 1138 WayPoint curWp, long curWpTime, long offset) { 1139 // Time between the track point and the previous one, 5 sec if first point, i.e. photos take 1140 // 5 sec before the first track point can be assumed to be take at the starting position 1141 long interval = prevWpTime > 0 ? Math.abs(curWpTime - prevWpTime) : TimeUnit.SECONDS.toMillis(5); 1494 static int matchPoints(List<ImageEntry> images, WayPoint prevWp, long prevWpTime, WayPoint curWp, long curWpTime, 1495 long offset, boolean interpolate, int tag, boolean isLast) { 1496 1142 1497 int ret = 0; 1143 1498 1144 1499 // i is the index of the timewise last photo that has the same or earlier EXIF time 1145 int i = getLastIndexOfListBefore(images, curWpTime); 1500 int i; 1501 if (isLast) { 1502 i = images.size() - 1; 1503 } else { 1504 i = getLastIndexOfListBefore(images, curWpTime); 1505 } 1146 1506 1147 1507 // no photos match 1148 1508 if (i < 0) … … 1151 1511 Double speed = null; 1152 1512 Double prevElevation = null; 1153 1513 1154 if (prevWp != null ) {1514 if (prevWp != null && interpolate) { 1155 1515 double distance = prevWp.getCoor().greatCircleDistance(curWp.getCoor()); 1156 1516 // This is in km/h, 3.6 * m/s 1157 1517 if (curWpTime > prevWpTime) { … … 1162 1522 1163 1523 Double curElevation = getElevation(curWp); 1164 1524 1165 // First trackpoint, then interval is set to five seconds, i.e. photos up to five seconds 1166 // before the first point will be geotagged with the starting point 1167 if (prevWpTime == 0 || curWpTime <= prevWpTime) { 1525 if (!interpolate || isLast) { 1526 final long half = Math.abs(curWpTime - prevWpTime) / 2; 1168 1527 while (i >= 0) { 1169 1528 final ImageEntry curImg = images.get(i); 1170 long time = curImg.getExifTime().getTime();1171 if ( time > curWpTime || time < curWpTime - interval) {1529 final long time = curImg.getExifTime().getTime(); 1530 if ((!isLast && time > curWpTime) || time < prevWpTime) { 1172 1531 break; 1173 1532 } 1174 if (curImg.tmp.getPos() == null) { 1175 curImg.tmp.setPos(curWp.getCoor()); 1176 curImg.tmp.setSpeed(speed); 1177 curImg.tmp.setElevation(curElevation); 1533 if (curImg.tmp.getPos() == null && Math.abs(time - curWpTime) <= TimeUnit.MINUTES.toMillis(tag)) { 1534 if (prevWp != null && time < curWpTime - half) { 1535 curImg.tmp.setPos(prevWp.getCoor()); 1536 } else { 1537 curImg.tmp.setPos(curWp.getCoor()); 1538 } 1178 1539 curImg.tmp.setGpsTime(new Date(curImg.getExifTime().getTime() - offset)); 1179 1540 curImg.tmp.flagNewGpsData(); 1180 1541 ret++; … … 1181 1542 } 1182 1543 i--; 1183 1544 } 1184 return ret; 1185 } 1545 } else if (prevWp != null) { 1546 // This code gives a simple linear interpolation of the coordinates between current and 1547 // previous track point assuming a constant speed in between 1548 while (i >= 0) { 1549 ImageEntry curImg = images.get(i); 1550 final long imgTime = curImg.getExifTime().getTime(); 1551 if (imgTime < prevWpTime) { 1552 break; 1553 } 1554 if (curImg.tmp.getPos() == null) { 1555 // The values of timeDiff are between 0 and 1, it is not seconds but a dimensionless variable 1556 double timeDiff = (double) (imgTime - prevWpTime) / Math.abs(curWpTime - prevWpTime); 1557 curImg.tmp.setPos(prevWp.getCoor().interpolate(curWp.getCoor(), timeDiff)); 1558 curImg.tmp.setSpeed(speed); 1559 if (curElevation != null && prevElevation != null) { 1560 curImg.tmp.setElevation(prevElevation + (curElevation - prevElevation) * timeDiff); 1561 } 1562 curImg.tmp.setGpsTime(new Date(curImg.getExifTime().getTime() - offset)); 1563 curImg.tmp.flagNewGpsData(); 1186 1564 1187 // This code gives a simple linear interpolation of the coordinates between current and 1188 // previous track point assuming a constant speed in between 1189 while (i >= 0) { 1190 ImageEntry curImg = images.get(i); 1191 long imgTime = curImg.getExifTime().getTime(); 1192 if (imgTime < prevWpTime) { 1193 break; 1194 } 1195 1196 if (prevWp != null && curImg.tmp.getPos() == null) { 1197 // The values of timeDiff are between 0 and 1, it is not seconds but a dimensionless variable 1198 double timeDiff = (double) (imgTime - prevWpTime) / interval; 1199 curImg.tmp.setPos(prevWp.getCoor().interpolate(curWp.getCoor(), timeDiff)); 1200 curImg.tmp.setSpeed(speed); 1201 if (curElevation != null && prevElevation != null) { 1202 curImg.tmp.setElevation(prevElevation + (curElevation - prevElevation) * timeDiff); 1565 ret++; 1203 1566 } 1204 curImg.tmp.setGpsTime(new Date(curImg.getExifTime().getTime() - offset)); 1205 curImg.tmp.flagNewGpsData(); 1206 1207 ret++; 1567 i--; 1208 1568 } 1209 i--;1210 1569 } 1211 1570 return ret; 1212 1571 } … … 1238 1597 return startIndex; 1239 1598 1240 1599 // This final loop is to check if photos with the exact same EXIF time follows 1241 while ((endIndex < (lstSize -1)) && (images.get(endIndex).getExifTime().getTime()1600 while ((endIndex < (lstSize - 1)) && (images.get(endIndex).getExifTime().getTime() 1242 1601 == images.get(endIndex + 1).getExifTime().getTime())) { 1243 1602 endIndex++; 1244 1603 } -
src/org/openstreetmap/josm/gui/layer/geoimage/Timezone.java
5 5 6 6 import java.text.ParseException; 7 7 import java.util.Objects; 8 import java.util.regex.Matcher; 9 import java.util.regex.Pattern; 8 10 9 11 /** 10 12 * Timezone in hours.<p> … … 49 51 } 50 52 51 53 static Timezone parseTimezone(String timezone) throws ParseException { 52 53 54 if (timezone.isEmpty()) 54 55 return ZERO; 55 56 56 String error = tr("Error while parsing timezone.\nExpected format: {0}", "+H:MM");57 Matcher m = Pattern.compile("^([\\+\\-]?)(\\d{1,2})(?:\\:([0-5]\\d))?$").matcher(timezone); 57 58 58 char sgnTimezone = '+'; 59 StringBuilder hTimezone = new StringBuilder(); 60 StringBuilder mTimezone = new StringBuilder(); 61 int state = 1; // 1=start/sign, 2=hours, 3=minutes. 62 for (int i = 0; i < timezone.length(); i++) { 63 char c = timezone.charAt(i); 64 switch (c) { 65 case ' ': 66 if (state != 2 || hTimezone.length() != 0) 67 throw new ParseException(error, i); 68 break; 69 case '+': 70 case '-': 71 if (state == 1) { 72 sgnTimezone = c; 73 state = 2; 74 } else 75 throw new ParseException(error, i); 76 break; 77 case ':': 78 case '.': 79 if (state == 2) { 80 state = 3; 81 } else 82 throw new ParseException(error, i); 83 break; 84 case '0': 85 case '1': 86 case '2': 87 case '3': 88 case '4': 89 case '5': 90 case '6': 91 case '7': 92 case '8': 93 case '9': 94 switch (state) { 95 case 1: 96 case 2: 97 state = 2; 98 hTimezone.append(c); 99 break; 100 case 3: 101 mTimezone.append(c); 102 break; 103 default: 104 throw new ParseException(error, i); 105 } 106 break; 107 default: 108 throw new ParseException(error, i); 109 } 110 } 111 112 int h = 0; 113 int m = 0; 59 ParseException pe = new ParseException(tr("Error while parsing timezone.\nExpected format: {0}", "±HH:MM"), 0); 114 60 try { 115 h = Integer.parseInt(hTimezone.toString()); 116 if (mTimezone.length() > 0) { 117 m = Integer.parseInt(mTimezone.toString()); 61 if (m.find()) { 62 int sign = m.group(1) == "-" ? -1 : 1; 63 int hour = Integer.parseInt(m.group(2)); 64 int min = m.groupCount() > 3 ? Integer.parseInt(m.group(3)) : 0; 65 return new Timezone(sign * hour + min / 60.0); 118 66 } 119 } catch (NumberFormatException nfe) { 120 // Invalid timezone 121 throw (ParseException) new ParseException(error, 0).initCause(nfe); 67 } catch (IndexOutOfBoundsException | NumberFormatException ex) { 68 pe.initCause(ex); 122 69 } 123 124 if (h > 12 || m > 59) 125 throw new ParseException(error, 0); 126 else 127 return new Timezone((h + m / 60.0) * (sgnTimezone == '-' ? -1 : 1)); 70 throw pe; 128 71 } 129 72 130 73 @Override