Ticket #16681: GPXCorrelate.patch

File GPXCorrelate.patch, 35.3 KB (added by Bjoeni, 6 years ago)
  • src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java

     
    55import static org.openstreetmap.josm.tools.I18n.trn;
    66
    77import java.awt.BorderLayout;
     8import java.awt.Component;
    89import java.awt.Cursor;
    910import java.awt.Dimension;
    1011import java.awt.FlowLayout;
     
    4344import javax.swing.BorderFactory;
    4445import javax.swing.JButton;
    4546import javax.swing.JCheckBox;
     47import javax.swing.JComponent;
    4648import javax.swing.JFileChooser;
    4749import javax.swing.JLabel;
    4850import javax.swing.JList;
     
    5153import javax.swing.JScrollPane;
    5254import javax.swing.JSeparator;
    5355import javax.swing.JSlider;
     56import javax.swing.JSpinner;
    5457import javax.swing.ListSelectionModel;
    5558import javax.swing.MutableComboBoxModel;
     59import javax.swing.SpinnerNumberModel;
    5660import javax.swing.SwingConstants;
     61import javax.swing.border.Border;
    5762import javax.swing.event.ChangeEvent;
    5863import javax.swing.event.ChangeListener;
    5964import javax.swing.event.DocumentEvent;
     
    7479import org.openstreetmap.josm.gui.io.importexport.NMEAImporter;
    7580import org.openstreetmap.josm.gui.layer.GpxLayer;
    7681import org.openstreetmap.josm.gui.layer.Layer;
     82import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
     83import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
     84import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
     85import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
    7786import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
    7887import org.openstreetmap.josm.gui.widgets.FileChooserManager;
    7988import org.openstreetmap.josm.gui.widgets.JosmComboBox;
     
    8392import org.openstreetmap.josm.io.IGpxReader;
    8493import org.openstreetmap.josm.io.nmea.NmeaReader;
    8594import org.openstreetmap.josm.spi.preferences.Config;
     95import org.openstreetmap.josm.spi.preferences.IPreferences;
    8696import org.openstreetmap.josm.tools.GBC;
    8797import org.openstreetmap.josm.tools.ImageProvider;
    8898import org.openstreetmap.josm.tools.JosmRuntimeException;
     
    102112    private final transient GeoImageLayer yLayer;
    103113    private transient Timezone timezone;
    104114    private transient Offset delta;
     115    private static boolean forceTags = false;
    105116
    106117    /**
    107118     * Constructs a new {@code CorrelateGpxWithImages} action.
     
    111122        super(tr("Correlate to GPX"));
    112123        new ImageProvider("dialogs/geoimage/gpx2img").getResource().attachImageIcon(this, true);
    113124        this.yLayer = layer;
     125        MainApplication.getLayerManager().addLayerChangeListener(new GpxLayerAddedListener());
    114126    }
    115127
    116128    private final class SyncDialogWindowListener extends WindowAdapter {
     
    322334        }
    323335    }
    324336
     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
    325532    /**
    326533     * This action listener is called when the user has a photo of the time of his GPS receiver. It
    327534     * displays the list of photos of the layer, and upon selection displays the selected photo.
     
    525732        }
    526733    }
    527734
     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
    528759    @Override
    529760    public void actionPerformed(ActionEvent ae) {
    530761        // Construct the list of loaded GPX tracks
     
    606837        JButton buttonAdjust = new JButton(tr("Manual adjust"));
    607838        buttonAdjust.addActionListener(new AdjustActionListener());
    608839
     840        JButton buttonAdvanced = new JButton(tr("Advanced settings..."));
     841        buttonAdvanced.addActionListener(new AdvancedSettingsActionListener());
     842
    609843        JLabel labelPosition = new JLabel(tr("Override position for: "));
    610844
    611845        int numAll = getSortedImgList(true, true).size();
     
    666900        gbc.weightx = 0.5;
    667901        panelTf.add(buttonViewGpsPhoto, gbc);
    668902
     903
    669904        gbc = GBC.std().fill(GBC.BOTH).insets(5, 5, 5, 5);
    670         gbc.gridx = 2;
     905        gbc.gridx = 1;
    671906        gbc.gridy = y++;
    672907        gbc.weightx = 0.5;
     908        panelTf.add(buttonAdvanced, gbc);
     909
     910        gbc.gridx = 2;
    673911        panelTf.add(buttonAutoGuess, gbc);
    674912
    675913        gbc.gridx = 3;
     
    715953        cbTaggedImg.addItemListener(statusBarUpdaterWithRepaint);
    716954
    717955        statusBarUpdater.updateStatusBar();
     956        yLayer.updateBufferAndRepaint();
    718957
    719958        outerPanel = new JPanel(new BorderLayout());
    720959        outerPanel.add(statusBar, BorderLayout.PAGE_END);
     
    8071046            if (selGpx == null)
    8081047                return tr("No gpx selected");
    8091048
    810             final long offsetMs = ((long) (timezone.getHours() * TimeUnit.HOURS.toMillis(1))) + delta.getMilliseconds(); // in milliseconds
     1049            final long offsetMs = ((long) (timezone.getHours() * TimeUnit.HOURS.toMillis(-1))) + delta.getMilliseconds(); // in milliseconds
    8111050            lastNumMatched = matchGpxTrack(dateImgLst, selGpx.data, offsetMs);
    8121051
    8131052            return trn("<html>Matched <b>{0}</b> of <b>{1}</b> photo to GPX track.</html>",
     
    10981337    static int matchGpxTrack(List<ImageEntry> images, GpxData selectedGpx, long offset) {
    10991338        int ret = 0;
    11001339
     1340        long prevWpTime = 0;
     1341        WayPoint prevWp = null;
     1342
     1343        List<List<List<WayPoint>>> trks = new ArrayList<>();
     1344
    11011345        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        });
    11031388
    1104                 long prevWpTime = 0;
    1105                 WayPoint prevWp = null;
     1389        boolean trkInt, trkTag, segInt, segTag;
     1390        int trkTime, trkDist, trkTagTime, segTime, segDist, segTagTime;
    11061391
    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;
    11121400
    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                        }
    11161447                    }
    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;
    11191473                }
    11201474            }
    11211475        }
     1476        if (trkTag) {
     1477            ret += matchPoints(images, prevWp, prevWpTime, prevWp, prevWpTime, offset, false, trkTagTime, true);
     1478        }
    11221479        return ret;
    11231480    }
    11241481
     
    11341491        return null;
    11351492    }
    11361493
    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
    11421497        int ret = 0;
    11431498
    11441499        // 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        }
    11461506
    11471507        // no photos match
    11481508        if (i < 0)
     
    11511511        Double speed = null;
    11521512        Double prevElevation = null;
    11531513
    1154         if (prevWp != null) {
     1514        if (prevWp != null && interpolate) {
    11551515            double distance = prevWp.getCoor().greatCircleDistance(curWp.getCoor());
    11561516            // This is in km/h, 3.6 * m/s
    11571517            if (curWpTime > prevWpTime) {
     
    11621522
    11631523        Double curElevation = getElevation(curWp);
    11641524
    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;
    11681527            while (i >= 0) {
    11691528                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) {
    11721531                    break;
    11731532                }
    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                    }
    11781539                    curImg.tmp.setGpsTime(new Date(curImg.getExifTime().getTime() - offset));
    11791540                    curImg.tmp.flagNewGpsData();
    11801541                    ret++;
     
    11811542                }
    11821543                i--;
    11831544            }
    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();
    11861564
    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++;
    12031566                }
    1204                 curImg.tmp.setGpsTime(new Date(curImg.getExifTime().getTime() - offset));
    1205                 curImg.tmp.flagNewGpsData();
    1206 
    1207                 ret++;
     1567                i--;
    12081568            }
    1209             i--;
    12101569        }
    12111570        return ret;
    12121571    }
     
    12381597            return startIndex;
    12391598
    12401599        // 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()
    12421601                == images.get(endIndex + 1).getExifTime().getTime())) {
    12431602            endIndex++;
    12441603        }
  • src/org/openstreetmap/josm/gui/layer/geoimage/Timezone.java

     
    55
    66import java.text.ParseException;
    77import java.util.Objects;
     8import java.util.regex.Matcher;
     9import java.util.regex.Pattern;
    810
    911/**
    1012 * Timezone in hours.<p>
     
    4951    }
    5052
    5153    static Timezone parseTimezone(String timezone) throws ParseException {
    52 
    5354        if (timezone.isEmpty())
    5455            return ZERO;
    5556
    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);
    5758
    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);
    11460        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);
    11866            }
    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);
    12269        }
    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;
    12871    }
    12972
    13073    @Override