Index: trunk/src/org/openstreetmap/josm/data/gpx/GpxImageCorrelation.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/gpx/GpxImageCorrelation.java	(revision 14205)
+++ trunk/src/org/openstreetmap/josm/data/gpx/GpxImageCorrelation.java	(revision 14205)
@@ -0,0 +1,318 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.gpx;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.Pair;
+
+/**
+ * Correlation logic for {@code CorrelateGpxWithImages}.
+ * @since 14205
+ */
+public final class GpxImageCorrelation {
+
+    private GpxImageCorrelation() {
+        // Hide public constructor
+    }
+
+    /**
+     * Match a list of photos to a gpx track with a given offset.
+     * All images need a exifTime attribute and the List must be sorted according to these times.
+     * @param images images to match
+     * @param selectedGpx selected GPX data
+     * @param offset offset
+     * @param forceTags force tagging of all photos, otherwise prefs are used
+     * @return number of matched points
+     */
+    public static int matchGpxTrack(List<? extends GpxImageEntry> images, GpxData selectedGpx, long offset, boolean forceTags) {
+        int ret = 0;
+
+        long prevWpTime = 0;
+        WayPoint prevWp = null;
+
+        List<List<List<WayPoint>>> trks = new ArrayList<>();
+
+        for (GpxTrack trk : selectedGpx.tracks) {
+            List<List<WayPoint>> segs = new ArrayList<>();
+            for (GpxTrackSegment seg : trk.getSegments()) {
+                List<WayPoint> wps = new ArrayList<>(seg.getWayPoints());
+                if (!wps.isEmpty()) {
+                    //remove waypoints at the beginning of the track/segment without timestamps
+                    int wp;
+                    for (wp = 0; wp < wps.size(); wp++) {
+                        if (wps.get(wp).setTimeFromAttribute() != null) {
+                            break;
+                        }
+                    }
+                    if (wp == 0) {
+                        segs.add(wps);
+                    } else if (wp < wps.size()) {
+                        segs.add(wps.subList(wp, wps.size()));
+                    }
+                }
+            }
+            //sort segments by first waypoint
+            if (!segs.isEmpty()) {
+                segs.sort(new Comparator<List<WayPoint>>() {
+                    @Override
+                    public int compare(List<WayPoint> arg0, List<WayPoint> arg1) {
+                        if (arg0.isEmpty() || arg1.isEmpty() || arg0.get(0).time == arg1.get(0).time)
+                            return 0;
+                        return arg0.get(0).time < arg1.get(0).time ? -1 : 1;
+                    }
+                });
+                trks.add(segs);
+            }
+        }
+        //sort tracks by first waypoint of first segment
+        trks.sort(new Comparator<List<List<WayPoint>>>() {
+            @Override
+            public int compare(List<List<WayPoint>> arg0, List<List<WayPoint>> arg1) {
+                if (arg0.isEmpty() || arg0.get(0).isEmpty()
+                        || arg1.isEmpty() || arg1.get(0).isEmpty()
+                        || arg0.get(0).get(0).time == arg1.get(0).get(0).time)
+                    return 0;
+                return arg0.get(0).get(0).time < arg1.get(0).get(0).time ? -1 : 1;
+            }
+        });
+
+        boolean trkInt, trkTag, segInt, segTag;
+        int trkTime, trkDist, trkTagTime, segTime, segDist, segTagTime;
+
+        if (forceTags) { //temporary option to override advanced settings and activate all possible interpolations / tagging methods
+            trkInt = trkTag = segInt = segTag = true;
+            trkTime = trkDist = trkTagTime = segTime = segDist = segTagTime = Integer.MAX_VALUE;
+        } else {
+            // Load the settings
+            trkInt = Config.getPref().getBoolean("geoimage.trk.int", false);
+            trkTime = Config.getPref().getBoolean("geoimage.trk.int.time", false) ?
+                    Config.getPref().getInt("geoimage.trk.int.time.val", 60) : Integer.MAX_VALUE;
+            trkDist = Config.getPref().getBoolean("geoimage.trk.int.dist", false) ?
+                    Config.getPref().getInt("geoimage.trk.int.dist.val", 50) : Integer.MAX_VALUE;
+
+            trkTag = Config.getPref().getBoolean("geoimage.trk.tag", true);
+            trkTagTime = Config.getPref().getBoolean("geoimage.trk.tag.time", true) ?
+                    Config.getPref().getInt("geoimage.trk.tag.time.val", 2) : Integer.MAX_VALUE;
+
+            segInt = Config.getPref().getBoolean("geoimage.seg.int", true);
+            segTime = Config.getPref().getBoolean("geoimage.seg.int.time", true) ?
+                    Config.getPref().getInt("geoimage.seg.int.time.val", 60) : Integer.MAX_VALUE;
+            segDist = Config.getPref().getBoolean("geoimage.seg.int.dist", true) ?
+                    Config.getPref().getInt("geoimage.seg.int.dist.val", 50) : Integer.MAX_VALUE;
+
+            segTag = Config.getPref().getBoolean("geoimage.seg.tag", true);
+            segTagTime = Config.getPref().getBoolean("geoimage.seg.tag.time", true) ?
+                    Config.getPref().getInt("geoimage.seg.tag.time.val", 2) : Integer.MAX_VALUE;
+        }
+        boolean isFirst = true;
+
+        for (int t = 0; t < trks.size(); t++) {
+            List<List<WayPoint>> segs = trks.get(t);
+            for (int s = 0; s < segs.size(); s++) {
+                List<WayPoint> wps = segs.get(s);
+                for (int i = 0; i < wps.size(); i++) {
+                    WayPoint curWp = wps.get(i);
+                    Date parsedTime = curWp.setTimeFromAttribute();
+                    // Interpolate timestamps in the segment, if one or more waypoints miss them
+                    if (parsedTime == null) {
+                        //check if any of the following waypoints has a timestamp...
+                        if (i > 0 && wps.get(i - 1).time != 0) {
+                            long prevWpTimeNoOffset = wps.get(i - 1).getTime().getTime();
+                            double totalDist = 0;
+                            List<Pair<Double, WayPoint>> nextWps = new ArrayList<>();
+                            for (int j = i; j < wps.size(); j++) {
+                                totalDist += wps.get(j - 1).getCoor().greatCircleDistance(wps.get(j).getCoor());
+                                nextWps.add(new Pair<>(totalDist, wps.get(j)));
+                                final Date nextTime = wps.get(j).setTimeFromAttribute();
+                                if (nextTime != null) {
+                                    // ...if yes, interpolate everything in between
+                                    long timeDiff = nextTime.getTime() - prevWpTimeNoOffset;
+                                    for (Pair<Double, WayPoint> pair : nextWps) {
+                                        pair.b.setTime(new Date((long) (prevWpTimeNoOffset + (timeDiff * (pair.a / totalDist)))));
+                                    }
+                                    break;
+                                }
+                            }
+                            parsedTime = curWp.setTimeFromAttribute();
+                            if (parsedTime == null) {
+                                break; //It's pointless to continue with this segment, because none of the following waypoints had a timestamp
+                            }
+                        } else {
+                            // Timestamps on waypoints without preceding timestamps in the same segment can not be interpolated, so try next one
+                            continue;
+                        }
+                    }
+
+                    final long curWpTime = parsedTime.getTime() + offset;
+                    boolean interpolate = true;
+                    int tagTime = 0;
+                    if (i == 0) {
+                        if (s == 0) { //First segment of the track, so apply settings for tracks
+                            if (!trkInt || isFirst || prevWp == null ||
+                                    Math.abs(curWpTime - prevWpTime) > TimeUnit.MINUTES.toMillis(trkTime) ||
+                                    prevWp.getCoor().greatCircleDistance(curWp.getCoor()) > trkDist) {
+                                isFirst = false;
+                                interpolate = false;
+                                if (trkTag) {
+                                    tagTime = trkTagTime;
+                                }
+                            }
+                        } else { //Apply settings for segments
+                            if (!segInt || prevWp == null ||
+                                    Math.abs(curWpTime - prevWpTime) > TimeUnit.MINUTES.toMillis(segTime) ||
+                                    prevWp.getCoor().greatCircleDistance(curWp.getCoor()) > segDist) {
+                                interpolate = false;
+                                if (segTag) {
+                                    tagTime = segTagTime;
+                                }
+                            }
+                        }
+                    }
+                    ret += matchPoints(images, prevWp, prevWpTime, curWp, curWpTime, offset, interpolate, tagTime, false);
+                    prevWp = curWp;
+                    prevWpTime = curWpTime;
+                }
+            }
+        }
+        if (trkTag) {
+            ret += matchPoints(images, prevWp, prevWpTime, prevWp, prevWpTime, offset, false, trkTagTime, true);
+        }
+        return ret;
+    }
+
+    private static Double getElevation(WayPoint wp) {
+        String value = wp.getString(GpxConstants.PT_ELE);
+        if (value != null && !value.isEmpty()) {
+            try {
+                return Double.valueOf(value);
+            } catch (NumberFormatException e) {
+                Logging.warn(e);
+            }
+        }
+        return null;
+    }
+
+    private static int matchPoints(List<? extends GpxImageEntry> images, WayPoint prevWp, long prevWpTime, WayPoint curWp, long curWpTime,
+            long offset, boolean interpolate, int tagTime, boolean isLast) {
+
+        int ret = 0;
+
+        // i is the index of the timewise last photo that has the same or earlier EXIF time
+        int i;
+        if (isLast) {
+            i = images.size() - 1;
+        } else {
+            i = getLastIndexOfListBefore(images, curWpTime);
+        }
+
+        // no photos match
+        if (i < 0)
+            return 0;
+
+        Double speed = null;
+        Double prevElevation = null;
+
+        if (prevWp != null && interpolate) {
+            double distance = prevWp.getCoor().greatCircleDistance(curWp.getCoor());
+            // This is in km/h, 3.6 * m/s
+            if (curWpTime > prevWpTime) {
+                speed = 3600 * distance / (curWpTime - prevWpTime);
+            }
+            prevElevation = getElevation(prevWp);
+        }
+
+        Double curElevation = getElevation(curWp);
+
+        if (!interpolate || isLast) {
+            final long half = Math.abs(curWpTime - prevWpTime) / 2;
+            while (i >= 0) {
+                final GpxImageEntry curImg = images.get(i);
+                final GpxImageEntry curTmp = curImg.getTmp();
+                final long time = curImg.getExifTime().getTime();
+                if ((!isLast && time > curWpTime) || time < prevWpTime) {
+                    break;
+                }
+                long tagms = TimeUnit.MINUTES.toMillis(tagTime);
+                if (curTmp.getPos() == null &&
+                        (Math.abs(time - curWpTime) <= tagms
+                        || Math.abs(prevWpTime - time) <= tagms)) {
+                    if (prevWp != null && time < curWpTime - half) {
+                        curTmp.setPos(prevWp.getCoor());
+                    } else {
+                        curTmp.setPos(curWp.getCoor());
+                    }
+                    curTmp.setGpsTime(new Date(curImg.getExifTime().getTime() - offset));
+                    curTmp.flagNewGpsData();
+                    ret++;
+                }
+                i--;
+            }
+        } else if (prevWp != null) {
+            // This code gives a simple linear interpolation of the coordinates between current and
+            // previous track point assuming a constant speed in between
+            while (i >= 0) {
+                GpxImageEntry curImg = images.get(i);
+                GpxImageEntry curTmp = curImg.getTmp();
+                final long imgTime = curImg.getExifTime().getTime();
+                if (imgTime < prevWpTime) {
+                    break;
+                }
+                if (curTmp.getPos() == null) {
+                    // The values of timeDiff are between 0 and 1, it is not seconds but a dimensionless variable
+                    double timeDiff = (double) (imgTime - prevWpTime) / Math.abs(curWpTime - prevWpTime);
+                    curTmp.setPos(prevWp.getCoor().interpolate(curWp.getCoor(), timeDiff));
+                    curTmp.setSpeed(speed);
+                    if (curElevation != null && prevElevation != null) {
+                        curTmp.setElevation(prevElevation + (curElevation - prevElevation) * timeDiff);
+                    }
+                    curTmp.setGpsTime(new Date(curImg.getExifTime().getTime() - offset));
+                    curTmp.flagNewGpsData();
+
+                    ret++;
+                }
+                i--;
+            }
+        }
+        return ret;
+    }
+
+    private static int getLastIndexOfListBefore(List<? extends GpxImageEntry> images, long searchedTime) {
+        int lstSize = images.size();
+
+        // No photos or the first photo taken is later than the search period
+        if (lstSize == 0 || searchedTime < images.get(0).getExifTime().getTime())
+            return -1;
+
+        // The search period is later than the last photo
+        if (searchedTime > images.get(lstSize - 1).getExifTime().getTime())
+            return lstSize-1;
+
+        // The searched index is somewhere in the middle, do a binary search from the beginning
+        int curIndex;
+        int startIndex = 0;
+        int endIndex = lstSize-1;
+        while (endIndex - startIndex > 1) {
+            curIndex = (endIndex + startIndex) / 2;
+            if (searchedTime > images.get(curIndex).getExifTime().getTime()) {
+                startIndex = curIndex;
+            } else {
+                endIndex = curIndex;
+            }
+        }
+        if (searchedTime < images.get(endIndex).getExifTime().getTime())
+            return startIndex;
+
+        // This final loop is to check if photos with the exact same EXIF time follows
+        while ((endIndex < (lstSize - 1)) && (images.get(endIndex).getExifTime().getTime()
+                == images.get(endIndex + 1).getExifTime().getTime())) {
+            endIndex++;
+        }
+        return endIndex;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/data/gpx/GpxImageEntry.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/gpx/GpxImageEntry.java	(revision 14205)
+++ trunk/src/org/openstreetmap/josm/data/gpx/GpxImageEntry.java	(revision 14205)
@@ -0,0 +1,531 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.gpx;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Date;
+
+import org.openstreetmap.josm.data.coor.CachedLatLon;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.tools.ExifReader;
+import org.openstreetmap.josm.tools.JosmRuntimeException;
+import org.openstreetmap.josm.tools.Logging;
+
+import com.drew.imaging.jpeg.JpegMetadataReader;
+import com.drew.lang.CompoundException;
+import com.drew.metadata.Directory;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.MetadataException;
+import com.drew.metadata.exif.ExifIFD0Directory;
+import com.drew.metadata.exif.GpsDirectory;
+import com.drew.metadata.jpeg.JpegDirectory;
+
+/**
+ * Stores info about each image
+ * @since 14205 (extracted from gui.layer.geoimage.ImageEntry)
+ */
+public class GpxImageEntry implements Comparable<GpxImageEntry>, Cloneable {
+    private File file;
+    private Integer exifOrientation;
+    private LatLon exifCoor;
+    private Double exifImgDir;
+    private Date exifTime;
+    /**
+     * Flag isNewGpsData indicates that the GPS data of the image is new or has changed.
+     * GPS data includes the position, speed, elevation, time (e.g. as extracted from the GPS track).
+     * The flag can used to decide for which image file the EXIF GPS data is (re-)written.
+     */
+    private boolean isNewGpsData;
+    /** Temporary source of GPS time if not correlated with GPX track. */
+    private Date exifGpsTime;
+
+    /**
+     * The following values are computed from the correlation with the gpx track
+     * or extracted from the image EXIF data.
+     */
+    private CachedLatLon pos;
+    /** Speed in kilometer per hour */
+    private Double speed;
+    /** Elevation (altitude) in meters */
+    private Double elevation;
+    /** The time after correlation with a gpx track */
+    private Date gpsTime;
+
+    private int width;
+    private int height;
+
+    /**
+     * When the correlation dialog is open, we like to show the image position
+     * for the current time offset on the map in real time.
+     * On the other hand, when the user aborts this operation, the old values
+     * should be restored. We have a temporary copy, that overrides
+     * the normal values if it is not null. (This may be not the most elegant
+     * solution for this, but it works.)
+     */
+    private GpxImageEntry tmp;
+
+    /**
+     * Constructs a new {@code GpxImageEntry}.
+     */
+    public GpxImageEntry() {}
+
+    /**
+     * Constructs a new {@code GpxImageEntry}.
+     * @param file Path to image file on disk
+     */
+    public GpxImageEntry(File file) {
+        setFile(file);
+    }
+
+    /**
+     * Returns width of the image this GpxImageEntry represents.
+     * @return width of the image this GpxImageEntry represents
+     * @since 13220
+     */
+    public int getWidth() {
+        return width;
+    }
+
+    /**
+     * Returns height of the image this GpxImageEntry represents.
+     * @return height of the image this GpxImageEntry represents
+     * @since 13220
+     */
+    public int getHeight() {
+        return height;
+    }
+
+    /**
+     * Returns the position value. The position value from the temporary copy
+     * is returned if that copy exists.
+     * @return the position value
+     */
+    public CachedLatLon getPos() {
+        if (tmp != null)
+            return tmp.pos;
+        return pos;
+    }
+
+    /**
+     * Returns the speed value. The speed value from the temporary copy is
+     * returned if that copy exists.
+     * @return the speed value
+     */
+    public Double getSpeed() {
+        if (tmp != null)
+            return tmp.speed;
+        return speed;
+    }
+
+    /**
+     * Returns the elevation value. The elevation value from the temporary
+     * copy is returned if that copy exists.
+     * @return the elevation value
+     */
+    public Double getElevation() {
+        if (tmp != null)
+            return tmp.elevation;
+        return elevation;
+    }
+
+    /**
+     * Returns the GPS time value. The GPS time value from the temporary copy
+     * is returned if that copy exists.
+     * @return the GPS time value
+     */
+    public Date getGpsTime() {
+        if (tmp != null)
+            return getDefensiveDate(tmp.gpsTime);
+        return getDefensiveDate(gpsTime);
+    }
+
+    /**
+     * Convenient way to determine if this entry has a GPS time, without the cost of building a defensive copy.
+     * @return {@code true} if this entry has a GPS time
+     * @since 6450
+     */
+    public boolean hasGpsTime() {
+        return (tmp != null && tmp.gpsTime != null) || gpsTime != null;
+    }
+
+    /**
+     * Returns associated file.
+     * @return associated file
+     */
+    public File getFile() {
+        return file;
+    }
+
+    /**
+     * Returns EXIF orientation
+     * @return EXIF orientation
+     */
+    public Integer getExifOrientation() {
+        return exifOrientation != null ? exifOrientation : 1;
+    }
+
+    /**
+     * Returns EXIF time
+     * @return EXIF time
+     */
+    public Date getExifTime() {
+        return getDefensiveDate(exifTime);
+    }
+
+    /**
+     * Convenient way to determine if this entry has a EXIF time, without the cost of building a defensive copy.
+     * @return {@code true} if this entry has a EXIF time
+     * @since 6450
+     */
+    public boolean hasExifTime() {
+        return exifTime != null;
+    }
+
+    /**
+     * Returns the EXIF GPS time.
+     * @return the EXIF GPS time
+     * @since 6392
+     */
+    public Date getExifGpsTime() {
+        return getDefensiveDate(exifGpsTime);
+    }
+
+    /**
+     * Convenient way to determine if this entry has a EXIF GPS time, without the cost of building a defensive copy.
+     * @return {@code true} if this entry has a EXIF GPS time
+     * @since 6450
+     */
+    public boolean hasExifGpsTime() {
+        return exifGpsTime != null;
+    }
+
+    private static Date getDefensiveDate(Date date) {
+        if (date == null)
+            return null;
+        return new Date(date.getTime());
+    }
+
+    public LatLon getExifCoor() {
+        return exifCoor;
+    }
+
+    public Double getExifImgDir() {
+        if (tmp != null)
+            return tmp.exifImgDir;
+        return exifImgDir;
+    }
+
+    /**
+     * Sets the width of this GpxImageEntry.
+     * @param width set the width of this GpxImageEntry
+     * @since 13220
+     */
+    public void setWidth(int width) {
+        this.width = width;
+    }
+
+    /**
+     * Sets the height of this GpxImageEntry.
+     * @param height set the height of this GpxImageEntry
+     * @since 13220
+     */
+    public void setHeight(int height) {
+        this.height = height;
+    }
+
+    /**
+     * Sets the position.
+     * @param pos cached position
+     */
+    public void setPos(CachedLatLon pos) {
+        this.pos = pos;
+    }
+
+    /**
+     * Sets the position.
+     * @param pos position (will be cached)
+     */
+    public void setPos(LatLon pos) {
+        setPos(pos != null ? new CachedLatLon(pos) : null);
+    }
+
+    /**
+     * Sets the speed.
+     * @param speed speed
+     */
+    public void setSpeed(Double speed) {
+        this.speed = speed;
+    }
+
+    /**
+     * Sets the elevation.
+     * @param elevation elevation
+     */
+    public void setElevation(Double elevation) {
+        this.elevation = elevation;
+    }
+
+    /**
+     * Sets associated file.
+     * @param file associated file
+     */
+    public void setFile(File file) {
+        this.file = file;
+    }
+
+    /**
+     * Sets EXIF orientation.
+     * @param exifOrientation EXIF orientation
+     */
+    public void setExifOrientation(Integer exifOrientation) {
+        this.exifOrientation = exifOrientation;
+    }
+
+    /**
+     * Sets EXIF time.
+     * @param exifTime EXIF time
+     */
+    public void setExifTime(Date exifTime) {
+        this.exifTime = getDefensiveDate(exifTime);
+    }
+
+    /**
+     * Sets the EXIF GPS time.
+     * @param exifGpsTime the EXIF GPS time
+     * @since 6392
+     */
+    public void setExifGpsTime(Date exifGpsTime) {
+        this.exifGpsTime = getDefensiveDate(exifGpsTime);
+    }
+
+    public void setGpsTime(Date gpsTime) {
+        this.gpsTime = getDefensiveDate(gpsTime);
+    }
+
+    public void setExifCoor(LatLon exifCoor) {
+        this.exifCoor = exifCoor;
+    }
+
+    public void setExifImgDir(Double exifDir) {
+        this.exifImgDir = exifDir;
+    }
+
+    @Override
+    public GpxImageEntry clone() {
+        try {
+            return (GpxImageEntry) super.clone();
+        } catch (CloneNotSupportedException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    @Override
+    public int compareTo(GpxImageEntry image) {
+        if (exifTime != null && image.exifTime != null)
+            return exifTime.compareTo(image.exifTime);
+        else if (exifTime == null && image.exifTime == null)
+            return 0;
+        else if (exifTime == null)
+            return -1;
+        else
+            return 1;
+    }
+
+    /**
+     * Make a fresh copy and save it in the temporary variable. Use
+     * {@link #applyTmp()} or {@link #discardTmp()} if the temporary variable
+     * is not needed anymore.
+     */
+    public void createTmp() {
+        tmp = clone();
+        tmp.tmp = null;
+    }
+
+    /**
+     * Get temporary variable that is used for real time parameter
+     * adjustments. The temporary variable is created if it does not exist
+     * yet. Use {@link #applyTmp()} or {@link #discardTmp()} if the temporary
+     * variable is not needed anymore.
+     * @return temporary variable
+     */
+    public GpxImageEntry getTmp() {
+        if (tmp == null) {
+            createTmp();
+        }
+        return tmp;
+    }
+
+    /**
+     * Copy the values from the temporary variable to the main instance. The
+     * temporary variable is deleted.
+     * @see #discardTmp()
+     */
+    public void applyTmp() {
+        if (tmp != null) {
+            pos = tmp.pos;
+            speed = tmp.speed;
+            elevation = tmp.elevation;
+            gpsTime = tmp.gpsTime;
+            exifImgDir = tmp.exifImgDir;
+            isNewGpsData = tmp.isNewGpsData;
+            tmp = null;
+        }
+    }
+
+    /**
+     * Delete the temporary variable. Temporary modifications are lost.
+     * @see #applyTmp()
+     */
+    public void discardTmp() {
+        tmp = null;
+    }
+
+    /**
+     * If it has been tagged i.e. matched to a gpx track or retrieved lat/lon from exif
+     * @return {@code true} if it has been tagged
+     */
+    public boolean isTagged() {
+        return pos != null;
+    }
+
+    /**
+     * String representation. (only partial info)
+     */
+    @Override
+    public String toString() {
+        return file.getName()+": "+
+        "pos = "+pos+" | "+
+        "exifCoor = "+exifCoor+" | "+
+        (tmp == null ? " tmp==null" :
+            " [tmp] pos = "+tmp.pos);
+    }
+
+    /**
+     * Indicates that the image has new GPS data.
+     * That flag is set by new GPS data providers.  It is used e.g. by the photo_geotagging plugin
+     * to decide for which image file the EXIF GPS data needs to be (re-)written.
+     * @since 6392
+     */
+    public void flagNewGpsData() {
+        isNewGpsData = true;
+   }
+
+    /**
+     * Remove the flag that indicates new GPS data.
+     * The flag is cleared by a new GPS data consumer.
+     */
+    public void unflagNewGpsData() {
+        isNewGpsData = false;
+    }
+
+    /**
+     * Queries whether the GPS data changed. The flag value from the temporary
+     * copy is returned if that copy exists.
+     * @return {@code true} if GPS data changed, {@code false} otherwise
+     * @since 6392
+     */
+    public boolean hasNewGpsData() {
+        if (tmp != null)
+            return tmp.isNewGpsData;
+        return isNewGpsData;
+    }
+
+    /**
+     * Extract GPS metadata from image EXIF. Has no effect if the image file is not set
+     *
+     * If successful, fills in the LatLon, speed, elevation, image direction, and other attributes
+     * @since 9270
+     */
+    public void extractExif() {
+
+        Metadata metadata;
+
+        if (file == null) {
+            return;
+        }
+
+        try {
+            metadata = JpegMetadataReader.readMetadata(file);
+        } catch (CompoundException | IOException ex) {
+            Logging.error(ex);
+            setExifTime(null);
+            setExifCoor(null);
+            setPos(null);
+            return;
+        }
+
+        // Changed to silently cope with no time info in exif. One case
+        // of person having time that couldn't be parsed, but valid GPS info
+        try {
+            setExifTime(ExifReader.readTime(metadata));
+        } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException ex) {
+            Logging.warn(ex);
+            setExifTime(null);
+        }
+
+        final Directory dir = metadata.getFirstDirectoryOfType(JpegDirectory.class);
+        final Directory dirExif = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
+        final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
+
+        try {
+            if (dirExif != null) {
+                int orientation = dirExif.getInt(ExifIFD0Directory.TAG_ORIENTATION);
+                setExifOrientation(orientation);
+            }
+        } catch (MetadataException ex) {
+            Logging.debug(ex);
+        }
+
+        try {
+            if (dir != null) {
+                // there are cases where these do not match width and height stored in dirExif
+                int width = dir.getInt(JpegDirectory.TAG_IMAGE_WIDTH);
+                int height = dir.getInt(JpegDirectory.TAG_IMAGE_HEIGHT);
+                setWidth(width);
+                setHeight(height);
+            }
+        } catch (MetadataException ex) {
+            Logging.debug(ex);
+        }
+
+        if (dirGps == null) {
+            setExifCoor(null);
+            setPos(null);
+            return;
+        }
+
+        final Double speed = ExifReader.readSpeed(dirGps);
+        if (speed != null) {
+            setSpeed(speed);
+        }
+
+        final Double ele = ExifReader.readElevation(dirGps);
+        if (ele != null) {
+            setElevation(ele);
+        }
+
+        try {
+            final LatLon latlon = ExifReader.readLatLon(dirGps);
+            setExifCoor(latlon);
+            setPos(getExifCoor());
+        } catch (MetadataException | IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
+            Logging.error("Error reading EXIF from file: " + ex);
+            setExifCoor(null);
+            setPos(null);
+        }
+
+        try {
+            final Double direction = ExifReader.readDirection(dirGps);
+            if (direction != null) {
+                setExifImgDir(direction);
+            }
+        } catch (IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
+            Logging.debug(ex);
+        }
+
+        final Date gpsDate = dirGps.getGpsDate();
+        if (gpsDate != null) {
+            setExifGpsTime(gpsDate);
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/data/gpx/GpxTimeOffset.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/gpx/GpxTimeOffset.java	(revision 14205)
+++ trunk/src/org/openstreetmap/josm/data/gpx/GpxTimeOffset.java	(revision 14205)
@@ -0,0 +1,145 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.gpx;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.text.ParseException;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+import org.openstreetmap.josm.tools.JosmDecimalFormatSymbolsProvider;
+import org.openstreetmap.josm.tools.Pair;
+
+/**
+ * Time offset of GPX correlation.
+ * @since 14205 (extracted from {@code CorrelateGpxWithImages})
+ */
+public final class GpxTimeOffset {
+
+    /**
+     * The time offset 0.
+     */
+    public static final GpxTimeOffset ZERO = new GpxTimeOffset(0);
+    private final long milliseconds;
+
+    private GpxTimeOffset(long milliseconds) {
+        this.milliseconds = milliseconds;
+    }
+
+    /**
+     * Constructs a new {@code GpxTimeOffset} from milliseconds.
+     * @param milliseconds time offset in milliseconds.
+     * @return new {@code GpxTimeOffset}
+     */
+    public static GpxTimeOffset milliseconds(long milliseconds) {
+        return new GpxTimeOffset(milliseconds);
+    }
+
+    /**
+     * Constructs a new {@code GpxTimeOffset} from seconds.
+     * @param seconds time offset in seconds.
+     * @return new {@code GpxTimeOffset}
+     */
+    public static GpxTimeOffset seconds(long seconds) {
+        return new GpxTimeOffset(1000 * seconds);
+    }
+
+    /**
+     * Get time offset in milliseconds.
+     * @return time offset in milliseconds
+     */
+    public long getMilliseconds() {
+        return milliseconds;
+    }
+
+    /**
+     * Get time offset in seconds.
+     * @return time offset in seconds
+     */
+    public long getSeconds() {
+        return milliseconds / 1000;
+    }
+
+    /**
+     * Formats time offset.
+     * @return formatted time offset. Format: decimal number
+     */
+    public String formatOffset() {
+        if (milliseconds % 1000 == 0) {
+            return Long.toString(milliseconds / 1000);
+        } else if (milliseconds % 100 == 0) {
+            return String.format(Locale.ENGLISH, "%.1f", milliseconds / 1000.);
+        } else {
+            return String.format(Locale.ENGLISH, "%.3f", milliseconds / 1000.);
+        }
+    }
+
+    /**
+     * Parses time offset.
+     * @param offset time offset. Format: decimal number
+     * @return time offset
+     * @throws ParseException if time offset can't be parsed
+     */
+    public static GpxTimeOffset parseOffset(String offset) throws ParseException {
+        String error = tr("Error while parsing offset.\nExpected format: {0}", "number");
+
+        if (!offset.isEmpty()) {
+            try {
+                if (offset.startsWith("+")) {
+                    offset = offset.substring(1);
+                }
+                return GpxTimeOffset.milliseconds(Math.round(JosmDecimalFormatSymbolsProvider.parseDouble(offset) * 1000));
+            } catch (NumberFormatException nfe) {
+                throw (ParseException) new ParseException(error, 0).initCause(nfe);
+            }
+        } else {
+            return GpxTimeOffset.ZERO;
+        }
+    }
+
+    /**
+     * Returns the day difference.
+     * @return the day difference
+     */
+    public int getDayOffset() {
+        // Find day difference
+        return (int) Math.round(((double) getMilliseconds()) / TimeUnit.DAYS.toMillis(1));
+    }
+
+    /**
+     * Returns offset without day difference.
+     * @return offset without day difference
+     */
+    public GpxTimeOffset withoutDayOffset() {
+        return milliseconds(getMilliseconds() - TimeUnit.DAYS.toMillis(getDayOffset()));
+    }
+
+    /**
+     * Split out timezone and offset.
+     * @return pair of timezone and offset
+     */
+    public Pair<GpxTimezone, GpxTimeOffset> splitOutTimezone() {
+        // In hours
+        final double tz = ((double) withoutDayOffset().getSeconds()) / TimeUnit.HOURS.toSeconds(1);
+
+        // Due to imprecise clocks we might get a "+3:28" timezone, which should obviously be 3:30 with
+        // -2 minutes offset. This determines the real timezone and finds offset.
+        final double timezone = (double) Math.round(tz * 2) / 2; // hours, rounded to one decimal place
+        final long delta = Math.round(getMilliseconds() - timezone * TimeUnit.HOURS.toMillis(1));
+        return Pair.create(new GpxTimezone(timezone), GpxTimeOffset.milliseconds(delta));
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof GpxTimeOffset)) return false;
+        GpxTimeOffset offset = (GpxTimeOffset) o;
+        return milliseconds == offset.milliseconds;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(milliseconds);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/data/gpx/GpxTimezone.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/gpx/GpxTimezone.java	(revision 14205)
+++ trunk/src/org/openstreetmap/josm/data/gpx/GpxTimezone.java	(revision 14205)
@@ -0,0 +1,102 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.gpx;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.text.ParseException;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Timezone in hours.<p>
+ * TODO: should probably be replaced by {@link java.util.TimeZone}.
+ * @since 14205 (extracted from {@code CorrelateGpxWithImages})
+ */
+public final class GpxTimezone {
+
+    /**
+     * The timezone 0.
+     */
+    public static final GpxTimezone ZERO = new GpxTimezone(0.0);
+    private final double timezone;
+
+    /**
+     * Construcs a new {@code GpxTimezone}.
+     * @param hours timezone in hours
+     */
+    public GpxTimezone(double hours) {
+        this.timezone = hours;
+    }
+
+    /**
+     * Returns the timezone in hours.
+     * @return the timezone in hours
+     */
+    public double getHours() {
+        return timezone;
+    }
+
+    /**
+     * Formats time zone.
+     * @return formatted time zone. Format: ±HH:MM
+     */
+    public String formatTimezone() {
+        StringBuilder ret = new StringBuilder();
+
+        double timezone = this.timezone;
+        if (timezone < 0) {
+            ret.append('-');
+            timezone = -timezone;
+        } else {
+            ret.append('+');
+        }
+        ret.append((long) timezone).append(':');
+        int minutes = (int) ((timezone % 1) * 60);
+        if (minutes < 10) {
+            ret.append('0');
+        }
+        ret.append(minutes);
+
+        return ret.toString();
+    }
+
+    /**
+     * Parses timezone.
+     * @param timezone timezone. Expected format: ±HH:MM
+     * @return timezone
+     * @throws ParseException if timezone can't be parsed
+     */
+    public static GpxTimezone parseTimezone(String timezone) throws ParseException {
+        if (timezone.isEmpty())
+            return ZERO;
+
+        Matcher m = Pattern.compile("^([\\+\\-]?)(\\d{1,2})(?:\\:([0-5]\\d))?$").matcher(timezone);
+
+        ParseException pe = new ParseException(tr("Error while parsing timezone.\nExpected format: {0}", "±HH:MM"), 0);
+        try {
+            if (m.find()) {
+                int sign = "-".equals(m.group(1)) ? -1 : 1;
+                int hour = Integer.parseInt(m.group(2));
+                int min = m.group(3) == null ? 0 : Integer.parseInt(m.group(3));
+                return new GpxTimezone(sign * (hour + min / 60.0));
+            }
+        } catch (IndexOutOfBoundsException | NumberFormatException ex) {
+            pe.initCause(ex);
+        }
+        throw pe;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof GpxTimezone)) return false;
+        GpxTimezone timezone1 = (GpxTimezone) o;
+        return Double.compare(timezone1.timezone, timezone) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(timezone);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java	(revision 14204)
+++ trunk/src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java	(revision 14205)
@@ -6,4 +6,5 @@
 
 import java.awt.BorderLayout;
+import java.awt.Component;
 import java.awt.Cursor;
 import java.awt.Dimension;
@@ -35,4 +36,5 @@
 import java.util.Hashtable;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.TimeZone;
@@ -44,4 +46,5 @@
 import javax.swing.JButton;
 import javax.swing.JCheckBox;
+import javax.swing.JComponent;
 import javax.swing.JFileChooser;
 import javax.swing.JLabel;
@@ -52,7 +55,10 @@
 import javax.swing.JSeparator;
 import javax.swing.JSlider;
+import javax.swing.JSpinner;
 import javax.swing.ListSelectionModel;
 import javax.swing.MutableComboBoxModel;
+import javax.swing.SpinnerNumberModel;
 import javax.swing.SwingConstants;
+import javax.swing.border.Border;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
@@ -62,6 +68,8 @@
 import org.openstreetmap.josm.actions.DiskAccessAction;
 import org.openstreetmap.josm.actions.ExtensionFileFilter;
-import org.openstreetmap.josm.data.gpx.GpxConstants;
 import org.openstreetmap.josm.data.gpx.GpxData;
+import org.openstreetmap.josm.data.gpx.GpxImageCorrelation;
+import org.openstreetmap.josm.data.gpx.GpxTimeOffset;
+import org.openstreetmap.josm.data.gpx.GpxTimezone;
 import org.openstreetmap.josm.data.gpx.GpxTrack;
 import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
@@ -75,4 +83,8 @@
 import org.openstreetmap.josm.gui.layer.GpxLayer;
 import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
+import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
+import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
+import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
 import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
 import org.openstreetmap.josm.gui.widgets.FileChooserManager;
@@ -84,4 +96,5 @@
 import org.openstreetmap.josm.io.nmea.NmeaReader;
 import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.spi.preferences.IPreferences;
 import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.ImageProvider;
@@ -101,6 +114,7 @@
 
     private final transient GeoImageLayer yLayer;
-    private transient Timezone timezone;
-    private transient Offset delta;
+    private transient GpxTimezone timezone;
+    private transient GpxTimeOffset delta;
+    private static boolean forceTags = false;
 
     /**
@@ -112,4 +126,5 @@
         new ImageProvider("dialogs/geoimage/gpx2img").getResource().attachImageIcon(this, true);
         this.yLayer = layer;
+        MainApplication.getLayerManager().addLayerChangeListener(new GpxLayerAddedListener());
     }
 
@@ -130,5 +145,5 @@
             // Parse values again, to display an error if the format is not recognized
             try {
-                timezone = Timezone.parseTimezone(tfTimezone.getText().trim());
+                timezone = GpxTimezone.parseTimezone(tfTimezone.getText().trim());
             } catch (ParseException e) {
                 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), e.getMessage(),
@@ -138,5 +153,5 @@
 
             try {
-                delta = Offset.parseOffset(tfOffset.getText().trim());
+                delta = GpxTimeOffset.parseOffset(tfOffset.getText().trim());
             } catch (ParseException e) {
                 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), e.getMessage(),
@@ -323,4 +338,202 @@
     }
 
+    private class AdvancedSettingsActionListener implements ActionListener {
+
+        private class CheckBoxActionListener implements ActionListener {
+            private final JComponent[] comps;
+
+            CheckBoxActionListener(JComponent... c) {
+                comps = Objects.requireNonNull(c);
+            }
+
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                setEnabled((JCheckBox) e.getSource());
+            }
+
+            public void setEnabled(JCheckBox cb) {
+                for (JComponent comp : comps) {
+                    if (comp instanceof JSpinner) {
+                        comp.setEnabled(cb.isSelected());
+                    } else if (comp instanceof JPanel) {
+                        boolean en = cb.isSelected();
+                        for (Component c : comp.getComponents()) {
+                            if (c instanceof JSpinner) {
+                                c.setEnabled(en);
+                            } else {
+                                c.setEnabled(cb.isSelected());
+                                if (en && c instanceof JCheckBox) {
+                                    en = ((JCheckBox) c).isSelected();
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        private void addCheckBoxActionListener(JCheckBox cb, JComponent... c) {
+            CheckBoxActionListener listener = new CheckBoxActionListener(c);
+            cb.addActionListener(listener);
+            listener.setEnabled(cb);
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+
+            IPreferences s = Config.getPref();
+            JPanel p = new JPanel(new GridBagLayout());
+
+            Border border1 = BorderFactory.createEmptyBorder(0, 20, 0, 0);
+            Border border2 = BorderFactory.createEmptyBorder(10, 0, 5, 0);
+            Border border = BorderFactory.createEmptyBorder(0, 40, 0, 0);
+            FlowLayout layout = new FlowLayout();
+
+            JLabel l = new JLabel(tr("Segment settings"));
+            l.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0));
+            p.add(l, GBC.eol());
+            JCheckBox cInterpolSeg = new JCheckBox(tr("Interpolate between segments"), s.getBoolean("geoimage.seg.int", true));
+            cInterpolSeg.setBorder(border1);
+            p.add(cInterpolSeg, GBC.eol());
+
+            JCheckBox cInterpolSegTime = new JCheckBox(tr("only when the segments are less than # minutes apart:"),
+                    s.getBoolean("geoimage.seg.int.time", true));
+            JSpinner sInterpolSegTime = new JSpinner(
+                    new SpinnerNumberModel(s.getInt("geoimage.seg.int.time.val", 60), 0, Integer.MAX_VALUE, 1));
+            ((JSpinner.DefaultEditor) sInterpolSegTime.getEditor()).getTextField().setColumns(3);
+            JPanel pInterpolSegTime = new JPanel(layout);
+            pInterpolSegTime.add(cInterpolSegTime);
+            pInterpolSegTime.add(sInterpolSegTime);
+            pInterpolSegTime.setBorder(border);
+            p.add(pInterpolSegTime, GBC.eol());
+
+            JCheckBox cInterpolSegDist = new JCheckBox(tr("only when the segments are less than # meters apart:"),
+                    s.getBoolean("geoimage.seg.int.dist", true));
+            JSpinner sInterpolSegDist = new JSpinner(
+                    new SpinnerNumberModel(s.getInt("geoimage.seg.int.dist.val", 50), 0, Integer.MAX_VALUE, 1));
+            ((JSpinner.DefaultEditor) sInterpolSegDist.getEditor()).getTextField().setColumns(3);
+            JPanel pInterpolSegDist = new JPanel(layout);
+            pInterpolSegDist.add(cInterpolSegDist);
+            pInterpolSegDist.add(sInterpolSegDist);
+            pInterpolSegDist.setBorder(border);
+            p.add(pInterpolSegDist, GBC.eol());
+
+            JCheckBox cTagSeg = new JCheckBox(tr("Tag images at the closest end of a segment, when not interpolated"),
+                    s.getBoolean("geoimage.seg.tag", true));
+            cTagSeg.setBorder(border1);
+            p.add(cTagSeg, GBC.eol());
+
+            JCheckBox cTagSegTime = new JCheckBox(tr("only within # minutes of the closest trackpoint:"),
+                    s.getBoolean("geoimage.seg.tag.time", true));
+            JSpinner sTagSegTime = new JSpinner(
+                    new SpinnerNumberModel(s.getInt("geoimage.seg.tag.time.val", 2), 0, Integer.MAX_VALUE, 1));
+            ((JSpinner.DefaultEditor) sTagSegTime.getEditor()).getTextField().setColumns(3);
+            JPanel pTagSegTime = new JPanel(layout);
+            pTagSegTime.add(cTagSegTime);
+            pTagSegTime.add(sTagSegTime);
+            pTagSegTime.setBorder(border);
+            p.add(pTagSegTime, GBC.eol());
+
+            l = new JLabel(tr("Track settings (note that multiple tracks can be in one GPX file)"));
+            l.setBorder(border2);
+            p.add(l, GBC.eol());
+            JCheckBox cInterpolTrack = new JCheckBox(tr("Interpolate between tracks"), s.getBoolean("geoimage.trk.int", false));
+            cInterpolTrack.setBorder(border1);
+            p.add(cInterpolTrack, GBC.eol());
+
+            JCheckBox cInterpolTrackTime = new JCheckBox(tr("only when the tracks are less than # minutes apart:"),
+                    s.getBoolean("geoimage.trk.int.time", false));
+            JSpinner sInterpolTrackTime = new JSpinner(
+                    new SpinnerNumberModel(s.getInt("geoimage.trk.int.time.val", 60), 0, Integer.MAX_VALUE, 1));
+            ((JSpinner.DefaultEditor) sInterpolTrackTime.getEditor()).getTextField().setColumns(3);
+            JPanel pInterpolTrackTime = new JPanel(layout);
+            pInterpolTrackTime.add(cInterpolTrackTime);
+            pInterpolTrackTime.add(sInterpolTrackTime);
+            pInterpolTrackTime.setBorder(border);
+            p.add(pInterpolTrackTime, GBC.eol());
+
+            JCheckBox cInterpolTrackDist = new JCheckBox(tr("only when the tracks are less than # meters apart:"),
+                    s.getBoolean("geoimage.trk.int.dist", false));
+            JSpinner sInterpolTrackDist = new JSpinner(
+                    new SpinnerNumberModel(s.getInt("geoimage.trk.int.dist.val", 50), 0, Integer.MAX_VALUE, 1));
+            ((JSpinner.DefaultEditor) sInterpolTrackDist.getEditor()).getTextField().setColumns(3);
+            JPanel pInterpolTrackDist = new JPanel(layout);
+            pInterpolTrackDist.add(cInterpolTrackDist);
+            pInterpolTrackDist.add(sInterpolTrackDist);
+            pInterpolTrackDist.setBorder(border);
+            p.add(pInterpolTrackDist, GBC.eol());
+
+            JCheckBox cTagTrack = new JCheckBox("<html>" +
+                    tr("Tag images at the closest end of a track, when not interpolated<br>" +
+                    "(also applies before the first and after the last track)") + "</html>",
+                    s.getBoolean("geoimage.trk.tag", true));
+            cTagTrack.setBorder(border1);
+            p.add(cTagTrack, GBC.eol());
+
+            JCheckBox cTagTrackTime = new JCheckBox(tr("only within # minutes of the closest trackpoint:"),
+                    s.getBoolean("geoimage.trk.tag.time", true));
+            JSpinner sTagTrackTime = new JSpinner(
+                    new SpinnerNumberModel(s.getInt("geoimage.trk.tag.time.val", 2), 0, Integer.MAX_VALUE, 1));
+            ((JSpinner.DefaultEditor) sTagTrackTime.getEditor()).getTextField().setColumns(3);
+            JPanel pTagTrackTime = new JPanel(layout);
+            pTagTrackTime.add(cTagTrackTime);
+            pTagTrackTime.add(sTagTrackTime);
+            pTagTrackTime.setBorder(border);
+            p.add(pTagTrackTime, GBC.eol());
+
+            l = new JLabel(tr("Advanced"));
+            l.setBorder(border2);
+            p.add(l, GBC.eol());
+            JCheckBox cForce = new JCheckBox("<html>" +
+                    tr("Force tagging of all pictures (temporarily overrides the settings above).") + "<br>" +
+                    tr("This option will not be saved permanently.") + "</html>", forceTags);
+            cForce.setBorder(BorderFactory.createEmptyBorder(0, 20, 10, 0));
+            p.add(cForce, GBC.eol());
+
+            addCheckBoxActionListener(cInterpolSegTime, sInterpolSegTime);
+            addCheckBoxActionListener(cInterpolSegDist, sInterpolSegDist);
+            addCheckBoxActionListener(cInterpolSeg, pInterpolSegTime, pInterpolSegDist);
+
+            addCheckBoxActionListener(cTagSegTime, sTagSegTime);
+            addCheckBoxActionListener(cTagSeg, pTagSegTime);
+
+            addCheckBoxActionListener(cInterpolTrackTime, sInterpolTrackTime);
+            addCheckBoxActionListener(cInterpolTrackDist, sInterpolTrackDist);
+            addCheckBoxActionListener(cInterpolTrack, pInterpolTrackTime, pInterpolTrackDist);
+
+            addCheckBoxActionListener(cTagTrackTime, sTagTrackTime);
+            addCheckBoxActionListener(cTagTrack, pTagTrackTime);
+
+
+            ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(), tr("Advanced settings"), tr("OK"), tr("Cancel"))
+                            .setButtonIcons("ok", "cancel").setContent(p);
+            if (ed.showDialog().getValue() == 1) {
+
+                s.putBoolean("geoimage.seg.int", cInterpolSeg.isSelected());
+                s.putBoolean("geoimage.seg.int.dist", cInterpolSegDist.isSelected());
+                s.putInt("geoimage.seg.int.dist.val", (int) sInterpolSegDist.getValue());
+                s.putBoolean("geoimage.seg.int.time", cInterpolSegTime.isSelected());
+                s.putInt("geoimage.seg.int.time.val", (int) sInterpolSegTime.getValue());
+                s.putBoolean("geoimage.seg.tag", cTagSeg.isSelected());
+                s.putBoolean("geoimage.seg.tag.time", cTagSegTime.isSelected());
+                s.putInt("geoimage.seg.tag.time.val", (int) sTagSegTime.getValue());
+
+                s.putBoolean("geoimage.trk.int", cInterpolTrack.isSelected());
+                s.putBoolean("geoimage.trk.int.dist", cInterpolTrackDist.isSelected());
+                s.putInt("geoimage.trk.int.dist.val", (int) sInterpolTrackDist.getValue());
+                s.putBoolean("geoimage.trk.int.time", cInterpolTrackTime.isSelected());
+                s.putInt("geoimage.trk.int.time.val", (int) sInterpolTrackTime.getValue());
+                s.putBoolean("geoimage.trk.tag", cTagTrack.isSelected());
+                s.putBoolean("geoimage.trk.tag.time", cTagTrackTime.isSelected());
+                s.putInt("geoimage.trk.tag.time.val", (int) sTagTrackTime.getValue());
+
+                forceTags = cForce.isSelected(); // This setting is not supposed to be saved permanently
+
+                statusBarUpdater.updateStatusBar();
+                yLayer.updateBufferAndRepaint();
+            }
+        }
+    }
+
     /**
      * This action listener is called when the user has a photo of the time of his GPS receiver. It
@@ -399,5 +612,5 @@
 
                 String tzDesc = tzStr + " (" +
-                        new Timezone(((double) tz.getRawOffset()) / TimeUnit.HOURS.toMillis(1)).formatTimezone() +
+                        new GpxTimezone(((double) tz.getRawOffset()) / TimeUnit.HOURS.toMillis(1)).formatTimezone() +
                         ')';
                 vtTimezones.add(tzDesc);
@@ -417,5 +630,5 @@
 
             cbTimezones.setSelectedItem(defaultTz.getID() + " (" +
-                    new Timezone(((double) defaultTz.getRawOffset()) / TimeUnit.HOURS.toMillis(1)).formatTimezone() +
+                    new GpxTimezone(((double) defaultTz.getRawOffset()) / TimeUnit.HOURS.toMillis(1)).formatTimezone() +
                     ')');
 
@@ -515,5 +728,5 @@
 
                 Config.getPref().put("geoimage.timezoneid", tzId);
-                tfOffset.setText(Offset.milliseconds(delta).formatOffset());
+                tfOffset.setText(GpxTimeOffset.milliseconds(delta).formatOffset());
                 tfTimezone.setText(tzValue);
 
@@ -524,4 +737,30 @@
             yLayer.updateBufferAndRepaint();
         }
+    }
+
+    private class GpxLayerAddedListener implements LayerChangeListener {
+        @Override
+        public void layerAdded(LayerAddEvent e) {
+            if (syncDialog != null && syncDialog.isVisible()) {
+                Layer layer = e.getAddedLayer();
+                if (layer instanceof GpxLayer) {
+                    GpxLayer gpx = (GpxLayer) layer;
+                    GpxDataWrapper gdw = new GpxDataWrapper(gpx.getName(), gpx.data, gpx.data.storageFile);
+                    gpxLst.add(gdw);
+                    MutableComboBoxModel<GpxDataWrapper> model = (MutableComboBoxModel<GpxDataWrapper>) cbGpx.getModel();
+                    if (gpxLst.get(0).file == null) {
+                        gpxLst.remove(0);
+                        model.removeElementAt(0);
+                    }
+                    model.addElement(gdw);
+                }
+            }
+        }
+
+        @Override
+        public void layerRemoving(LayerRemoveEvent e) {}
+
+        @Override
+        public void layerOrderChanged(LayerOrderChangeEvent e) {}
     }
 
@@ -578,7 +817,8 @@
 
         try {
-            timezone = Timezone.parseTimezone(Optional.ofNullable(Config.getPref().get("geoimage.timezone", "0:00")).orElse("0:00"));
+            timezone = GpxTimezone.parseTimezone(Optional.ofNullable(Config.getPref().get("geoimage.timezone", "0:00")).orElse("0:00"));
         } catch (ParseException e) {
-            timezone = Timezone.ZERO;
+            timezone = GpxTimezone.ZERO;
+            Logging.trace(e);
         }
 
@@ -587,7 +827,8 @@
 
         try {
-            delta = Offset.parseOffset(Config.getPref().get("geoimage.delta", "0"));
+            delta = GpxTimeOffset.parseOffset(Config.getPref().get("geoimage.delta", "0"));
         } catch (ParseException e) {
-            delta = Offset.ZERO;
+            delta = GpxTimeOffset.ZERO;
+            Logging.trace(e);
         }
 
@@ -606,4 +847,7 @@
         JButton buttonAdjust = new JButton(tr("Manual adjust"));
         buttonAdjust.addActionListener(new AdjustActionListener());
+
+        JButton buttonAdvanced = new JButton(tr("Advanced settings..."));
+        buttonAdvanced.addActionListener(new AdvancedSettingsActionListener());
 
         JLabel labelPosition = new JLabel(tr("Override position for: "));
@@ -668,7 +912,10 @@
 
         gbc = GBC.std().fill(GBC.BOTH).insets(5, 5, 5, 5);
-        gbc.gridx = 2;
+        gbc.gridx = 1;
         gbc.gridy = y++;
         gbc.weightx = 0.5;
+        panelTf.add(buttonAdvanced, gbc);
+
+        gbc.gridx = 2;
         panelTf.add(buttonAutoGuess, gbc);
 
@@ -716,4 +963,5 @@
 
         statusBarUpdater.updateStatusBar();
+        yLayer.updateBufferAndRepaint();
 
         outerPanel = new JPanel(new BorderLayout());
@@ -782,6 +1030,6 @@
         private String statusText() {
             try {
-                timezone = Timezone.parseTimezone(tfTimezone.getText().trim());
-                delta = Offset.parseOffset(tfOffset.getText().trim());
+                timezone = GpxTimezone.parseTimezone(tfTimezone.getText().trim());
+                delta = GpxTimeOffset.parseOffset(tfOffset.getText().trim());
             } catch (ParseException e) {
                 return e.getMessage();
@@ -801,5 +1049,5 @@
             for (ImageEntry ie : dateImgLst) {
                 ie.createTmp();
-                ie.tmp.setPos(null);
+                ie.getTmp().setPos(null);
             }
 
@@ -808,6 +1056,6 @@
                 return tr("No gpx selected");
 
-            final long offsetMs = ((long) (timezone.getHours() * TimeUnit.HOURS.toMillis(1))) + delta.getMilliseconds(); // in milliseconds
-            lastNumMatched = matchGpxTrack(dateImgLst, selGpx.data, offsetMs);
+            final long offsetMs = ((long) (timezone.getHours() * TimeUnit.HOURS.toMillis(-1))) + delta.getMilliseconds(); // in milliseconds
+            lastNumMatched = GpxImageCorrelation.matchGpxTrack(dateImgLst, selGpx.data, offsetMs, forceTags);
 
             return trn("<html>Matched <b>{0}</b> of <b>{1}</b> photo to GPX track.</html>",
@@ -838,8 +1086,8 @@
         public void actionPerformed(ActionEvent arg0) {
 
-            final Offset offset = Offset.milliseconds(
+            final GpxTimeOffset offset = GpxTimeOffset.milliseconds(
                     delta.getMilliseconds() + Math.round(timezone.getHours() * TimeUnit.HOURS.toMillis(1)));
             final int dayOffset = offset.getDayOffset();
-            final Pair<Timezone, Offset> timezoneOffsetPair = offset.withoutDayOffset().splitOutTimezone();
+            final Pair<GpxTimezone, GpxTimeOffset> timezoneOffsetPair = offset.withoutDayOffset().splitOutTimezone();
 
             // Info Labels
@@ -854,5 +1102,5 @@
             // CHECKSTYLE.OFF: ParenPad
             for (int i = -12; i <= 12; i += 6) {
-                labelTable.put(i * 2, new JLabel(new Timezone(i).formatTimezone()));
+                labelTable.put(i * 2, new JLabel(new GpxTimezone(i).formatTimezone()));
             }
             // CHECKSTYLE.ON: ParenPad
@@ -872,5 +1120,5 @@
             // CHECKSTYLE.OFF: ParenPad
             for (int i = -60; i <= 60; i += 30) {
-                labelTable.put(i * 10, new JLabel(Offset.seconds(i).formatOffset()));
+                labelTable.put(i * 10, new JLabel(GpxTimeOffset.seconds(i).formatOffset()));
             }
             // CHECKSTYLE.ON: ParenPad
@@ -883,11 +1131,11 @@
                 @Override
                 public void stateChanged(ChangeEvent e) {
-                    timezone = new Timezone(sldTimezone.getValue() / 2.);
+                    timezone = new GpxTimezone(sldTimezone.getValue() / 2.);
 
                     lblTimezone.setText(tr("Timezone: {0}", timezone.formatTimezone()));
                     lblMinutes.setText(tr("Minutes: {0}", sldMinutes.getValue()));
-                    lblSeconds.setText(tr("Seconds: {0}", Offset.milliseconds(100L * sldSeconds.getValue()).formatOffset()));
-
-                    delta = Offset.milliseconds(100L * sldSeconds.getValue()
+                    lblSeconds.setText(tr("Seconds: {0}", GpxTimeOffset.milliseconds(100L * sldSeconds.getValue()).formatOffset()));
+
+                    delta = GpxTimeOffset.milliseconds(100L * sldSeconds.getValue()
                             + TimeUnit.MINUTES.toMillis(sldMinutes.getValue())
                             + TimeUnit.DAYS.toMillis(dayOffset));
@@ -968,5 +1216,5 @@
      * @throws NoGpxTimestamps when the gpx track does not contain a timestamp
      */
-    static Pair<Timezone, Offset> autoGuess(List<ImageEntry> imgs, GpxData gpx) throws NoGpxTimestamps {
+    static Pair<GpxTimezone, GpxTimeOffset> autoGuess(List<ImageEntry> imgs, GpxData gpx) throws NoGpxTimestamps {
 
         // Init variables
@@ -991,5 +1239,5 @@
         }
 
-        return Offset.milliseconds(firstExifDate - firstGPXDate).splitOutTimezone();
+        return GpxTimeOffset.milliseconds(firstExifDate - firstGPXDate).splitOutTimezone();
     }
 
@@ -1006,5 +1254,5 @@
 
             try {
-                final Pair<Timezone, Offset> r = autoGuess(imgs, gpx);
+                final Pair<GpxTimezone, GpxTimeOffset> r = autoGuess(imgs, gpx);
                 timezone = r.a;
                 delta = r.b;
@@ -1088,160 +1336,3 @@
     }
 
-    /**
-     * Match a list of photos to a gpx track with a given offset.
-     * All images need a exifTime attribute and the List must be sorted according to these times.
-     * @param images images to match
-     * @param selectedGpx selected GPX data
-     * @param offset offset
-     * @return number of matched points
-     */
-    static int matchGpxTrack(List<ImageEntry> images, GpxData selectedGpx, long offset) {
-        int ret = 0;
-
-        for (GpxTrack trk : selectedGpx.tracks) {
-            for (GpxTrackSegment segment : trk.getSegments()) {
-
-                long prevWpTime = 0;
-                WayPoint prevWp = null;
-
-                for (WayPoint curWp : segment.getWayPoints()) {
-                    final Date parsedTime = curWp.setTimeFromAttribute();
-                    if (parsedTime != null) {
-                        final long curWpTime = parsedTime.getTime() + offset;
-                        ret += matchPoints(images, prevWp, prevWpTime, curWp, curWpTime, offset);
-
-                        prevWp = curWp;
-                        prevWpTime = curWpTime;
-                        continue;
-                    }
-                    prevWp = null;
-                    prevWpTime = 0;
-                }
-            }
-        }
-        return ret;
-    }
-
-    private static Double getElevation(WayPoint wp) {
-        String value = wp.getString(GpxConstants.PT_ELE);
-        if (value != null && !value.isEmpty()) {
-            try {
-                return Double.valueOf(value);
-            } catch (NumberFormatException e) {
-                Logging.warn(e);
-            }
-        }
-        return null;
-    }
-
-    static int matchPoints(List<ImageEntry> images, WayPoint prevWp, long prevWpTime,
-            WayPoint curWp, long curWpTime, long offset) {
-        // Time between the track point and the previous one, 5 sec if first point, i.e. photos take
-        // 5 sec before the first track point can be assumed to be take at the starting position
-        long interval = prevWpTime > 0 ? Math.abs(curWpTime - prevWpTime) : TimeUnit.SECONDS.toMillis(5);
-        int ret = 0;
-
-        // i is the index of the timewise last photo that has the same or earlier EXIF time
-        int i = getLastIndexOfListBefore(images, curWpTime);
-
-        // no photos match
-        if (i < 0)
-            return 0;
-
-        Double speed = null;
-        Double prevElevation = null;
-
-        if (prevWp != null) {
-            double distance = prevWp.getCoor().greatCircleDistance(curWp.getCoor());
-            // This is in km/h, 3.6 * m/s
-            if (curWpTime > prevWpTime) {
-                speed = 3600 * distance / (curWpTime - prevWpTime);
-            }
-            prevElevation = getElevation(prevWp);
-        }
-
-        Double curElevation = getElevation(curWp);
-
-        // First trackpoint, then interval is set to five seconds, i.e. photos up to five seconds
-        // before the first point will be geotagged with the starting point
-        if (prevWpTime == 0 || curWpTime <= prevWpTime) {
-            while (i >= 0) {
-                final ImageEntry curImg = images.get(i);
-                long time = curImg.getExifTime().getTime();
-                if (time > curWpTime || time < curWpTime - interval) {
-                    break;
-                }
-                if (curImg.tmp.getPos() == null) {
-                    curImg.tmp.setPos(curWp.getCoor());
-                    curImg.tmp.setSpeed(speed);
-                    curImg.tmp.setElevation(curElevation);
-                    curImg.tmp.setGpsTime(new Date(curImg.getExifTime().getTime() - offset));
-                    curImg.tmp.flagNewGpsData();
-                    ret++;
-                }
-                i--;
-            }
-            return ret;
-        }
-
-        // This code gives a simple linear interpolation of the coordinates between current and
-        // previous track point assuming a constant speed in between
-        while (i >= 0) {
-            ImageEntry curImg = images.get(i);
-            long imgTime = curImg.getExifTime().getTime();
-            if (imgTime < prevWpTime) {
-                break;
-            }
-
-            if (prevWp != null && curImg.tmp.getPos() == null) {
-                // The values of timeDiff are between 0 and 1, it is not seconds but a dimensionless variable
-                double timeDiff = (double) (imgTime - prevWpTime) / interval;
-                curImg.tmp.setPos(prevWp.getCoor().interpolate(curWp.getCoor(), timeDiff));
-                curImg.tmp.setSpeed(speed);
-                if (curElevation != null && prevElevation != null) {
-                    curImg.tmp.setElevation(prevElevation + (curElevation - prevElevation) * timeDiff);
-                }
-                curImg.tmp.setGpsTime(new Date(curImg.getExifTime().getTime() - offset));
-                curImg.tmp.flagNewGpsData();
-
-                ret++;
-            }
-            i--;
-        }
-        return ret;
-    }
-
-    private static int getLastIndexOfListBefore(List<ImageEntry> images, long searchedTime) {
-        int lstSize = images.size();
-
-        // No photos or the first photo taken is later than the search period
-        if (lstSize == 0 || searchedTime < images.get(0).getExifTime().getTime())
-            return -1;
-
-        // The search period is later than the last photo
-        if (searchedTime > images.get(lstSize - 1).getExifTime().getTime())
-            return lstSize-1;
-
-        // The searched index is somewhere in the middle, do a binary search from the beginning
-        int curIndex;
-        int startIndex = 0;
-        int endIndex = lstSize-1;
-        while (endIndex - startIndex > 1) {
-            curIndex = (endIndex + startIndex) / 2;
-            if (searchedTime > images.get(curIndex).getExifTime().getTime()) {
-                startIndex = curIndex;
-            } else {
-                endIndex = curIndex;
-            }
-        }
-        if (searchedTime < images.get(endIndex).getExifTime().getTime())
-            return startIndex;
-
-        // This final loop is to check if photos with the exact same EXIF time follows
-        while ((endIndex < (lstSize-1)) && (images.get(endIndex).getExifTime().getTime()
-                == images.get(endIndex + 1).getExifTime().getTime())) {
-            endIndex++;
-        }
-        return endIndex;
-    }
 }
Index: trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java	(revision 14204)
+++ trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java	(revision 14205)
@@ -4,71 +4,21 @@
 import java.awt.Image;
 import java.io.File;
-import java.io.IOException;
 import java.util.Collections;
-import java.util.Date;
 
-import org.openstreetmap.josm.data.coor.CachedLatLon;
-import org.openstreetmap.josm.data.coor.LatLon;
-import org.openstreetmap.josm.tools.ExifReader;
-import org.openstreetmap.josm.tools.JosmRuntimeException;
-import org.openstreetmap.josm.tools.Logging;
-
-import com.drew.imaging.jpeg.JpegMetadataReader;
-import com.drew.lang.CompoundException;
-import com.drew.metadata.Directory;
-import com.drew.metadata.Metadata;
-import com.drew.metadata.MetadataException;
-import com.drew.metadata.exif.ExifIFD0Directory;
-import com.drew.metadata.exif.GpsDirectory;
-import com.drew.metadata.jpeg.JpegDirectory;
+import org.openstreetmap.josm.data.gpx.GpxImageEntry;
 
 /**
- * Stores info about each image
+ * Stores info about each image, with an optional thumbnail
+ * @since 2662
  */
-public final class ImageEntry implements Comparable<ImageEntry>, Cloneable {
-    private File file;
-    private Integer exifOrientation;
-    private LatLon exifCoor;
-    private Double exifImgDir;
-    private Date exifTime;
-    /**
-     * Flag isNewGpsData indicates that the GPS data of the image is new or has changed.
-     * GPS data includes the position, speed, elevation, time (e.g. as extracted from the GPS track).
-     * The flag can used to decide for which image file the EXIF GPS data is (re-)written.
-     */
-    private boolean isNewGpsData;
-    /** Temporary source of GPS time if not correlated with GPX track. */
-    private Date exifGpsTime;
+public final class ImageEntry extends GpxImageEntry {
+
     private Image thumbnail;
-
-    /**
-     * The following values are computed from the correlation with the gpx track
-     * or extracted from the image EXIF data.
-     */
-    private CachedLatLon pos;
-    /** Speed in kilometer per hour */
-    private Double speed;
-    /** Elevation (altitude) in meters */
-    private Double elevation;
-    /** The time after correlation with a gpx track */
-    private Date gpsTime;
-
-    private int width;
-    private int height;
-
-    /**
-     * When the correlation dialog is open, we like to show the image position
-     * for the current time offset on the map in real time.
-     * On the other hand, when the user aborts this operation, the old values
-     * should be restored. We have a temporary copy, that overrides
-     * the normal values if it is not null. (This may be not the most elegant
-     * solution for this, but it works.)
-     */
-    ImageEntry tmp;
 
     /**
      * Constructs a new {@code ImageEntry}.
      */
-    public ImageEntry() {}
+    public ImageEntry() {
+    }
 
     /**
@@ -77,143 +27,5 @@
      */
     public ImageEntry(File file) {
-        setFile(file);
-    }
-
-    /**
-     * Returns width of the image this ImageEntry represents.
-     * @return width of the image this ImageEntry represents
-     * @since 13220
-     */
-    public int getWidth() {
-        return width;
-    }
-
-    /**
-     * Returns height of the image this ImageEntry represents.
-     * @return height of the image this ImageEntry represents
-     * @since 13220
-     */
-    public int getHeight() {
-        return height;
-    }
-
-    /**
-     * Returns the position value. The position value from the temporary copy
-     * is returned if that copy exists.
-     * @return the position value
-     */
-    public CachedLatLon getPos() {
-        if (tmp != null)
-            return tmp.pos;
-        return pos;
-    }
-
-    /**
-     * Returns the speed value. The speed value from the temporary copy is
-     * returned if that copy exists.
-     * @return the speed value
-     */
-    public Double getSpeed() {
-        if (tmp != null)
-            return tmp.speed;
-        return speed;
-    }
-
-    /**
-     * Returns the elevation value. The elevation value from the temporary
-     * copy is returned if that copy exists.
-     * @return the elevation value
-     */
-    public Double getElevation() {
-        if (tmp != null)
-            return tmp.elevation;
-        return elevation;
-    }
-
-    /**
-     * Returns the GPS time value. The GPS time value from the temporary copy
-     * is returned if that copy exists.
-     * @return the GPS time value
-     */
-    public Date getGpsTime() {
-        if (tmp != null)
-            return getDefensiveDate(tmp.gpsTime);
-        return getDefensiveDate(gpsTime);
-    }
-
-    /**
-     * Convenient way to determine if this entry has a GPS time, without the cost of building a defensive copy.
-     * @return {@code true} if this entry has a GPS time
-     * @since 6450
-     */
-    public boolean hasGpsTime() {
-        return (tmp != null && tmp.gpsTime != null) || gpsTime != null;
-    }
-
-    /**
-     * Returns associated file.
-     * @return associated file
-     */
-    public File getFile() {
-        return file;
-    }
-
-    /**
-     * Returns EXIF orientation
-     * @return EXIF orientation
-     */
-    public Integer getExifOrientation() {
-        return exifOrientation != null ? exifOrientation : 1;
-    }
-
-    /**
-     * Returns EXIF time
-     * @return EXIF time
-     */
-    public Date getExifTime() {
-        return getDefensiveDate(exifTime);
-    }
-
-    /**
-     * Convenient way to determine if this entry has a EXIF time, without the cost of building a defensive copy.
-     * @return {@code true} if this entry has a EXIF time
-     * @since 6450
-     */
-    public boolean hasExifTime() {
-        return exifTime != null;
-    }
-
-    /**
-     * Returns the EXIF GPS time.
-     * @return the EXIF GPS time
-     * @since 6392
-     */
-    public Date getExifGpsTime() {
-        return getDefensiveDate(exifGpsTime);
-    }
-
-    /**
-     * Convenient way to determine if this entry has a EXIF GPS time, without the cost of building a defensive copy.
-     * @return {@code true} if this entry has a EXIF GPS time
-     * @since 6450
-     */
-    public boolean hasExifGpsTime() {
-        return exifGpsTime != null;
-    }
-
-    private static Date getDefensiveDate(Date date) {
-        if (date == null)
-            return null;
-        return new Date(date.getTime());
-    }
-
-    public LatLon getExifCoor() {
-        return exifCoor;
-    }
-
-    public Double getExifImgDir() {
-        if (tmp != null)
-            return tmp.exifImgDir;
-        return exifImgDir;
+        super(file);
     }
 
@@ -251,317 +63,3 @@
         }
     }
-
-    /**
-     * Sets the width of this ImageEntry.
-     * @param width set the width of this ImageEntry
-     * @since 13220
-     */
-    public void setWidth(int width) {
-        this.width = width;
-    }
-
-    /**
-     * Sets the height of this ImageEntry.
-     * @param height set the height of this ImageEntry
-     * @since 13220
-     */
-    public void setHeight(int height) {
-        this.height = height;
-    }
-
-    /**
-     * Sets the position.
-     * @param pos cached position
-     */
-    public void setPos(CachedLatLon pos) {
-        this.pos = pos;
-    }
-
-    /**
-     * Sets the position.
-     * @param pos position (will be cached)
-     */
-    public void setPos(LatLon pos) {
-        setPos(pos != null ? new CachedLatLon(pos) : null);
-    }
-
-    /**
-     * Sets the speed.
-     * @param speed speed
-     */
-    public void setSpeed(Double speed) {
-        this.speed = speed;
-    }
-
-    /**
-     * Sets the elevation.
-     * @param elevation elevation
-     */
-    public void setElevation(Double elevation) {
-        this.elevation = elevation;
-    }
-
-    /**
-     * Sets associated file.
-     * @param file associated file
-     */
-    public void setFile(File file) {
-        this.file = file;
-    }
-
-    /**
-     * Sets EXIF orientation.
-     * @param exifOrientation EXIF orientation
-     */
-    public void setExifOrientation(Integer exifOrientation) {
-        this.exifOrientation = exifOrientation;
-    }
-
-    /**
-     * Sets EXIF time.
-     * @param exifTime EXIF time
-     */
-    public void setExifTime(Date exifTime) {
-        this.exifTime = getDefensiveDate(exifTime);
-    }
-
-    /**
-     * Sets the EXIF GPS time.
-     * @param exifGpsTime the EXIF GPS time
-     * @since 6392
-     */
-    public void setExifGpsTime(Date exifGpsTime) {
-        this.exifGpsTime = getDefensiveDate(exifGpsTime);
-    }
-
-    public void setGpsTime(Date gpsTime) {
-        this.gpsTime = getDefensiveDate(gpsTime);
-    }
-
-    public void setExifCoor(LatLon exifCoor) {
-        this.exifCoor = exifCoor;
-    }
-
-    public void setExifImgDir(Double exifDir) {
-        this.exifImgDir = exifDir;
-    }
-
-    @Override
-    public ImageEntry clone() {
-        try {
-            return (ImageEntry) super.clone();
-        } catch (CloneNotSupportedException e) {
-            throw new IllegalStateException(e);
-        }
-    }
-
-    @Override
-    public int compareTo(ImageEntry image) {
-        if (exifTime != null && image.exifTime != null)
-            return exifTime.compareTo(image.exifTime);
-        else if (exifTime == null && image.exifTime == null)
-            return 0;
-        else if (exifTime == null)
-            return -1;
-        else
-            return 1;
-    }
-
-    /**
-     * Make a fresh copy and save it in the temporary variable. Use
-     * {@link #applyTmp()} or {@link #discardTmp()} if the temporary variable
-     * is not needed anymore.
-     */
-    public void createTmp() {
-        tmp = clone();
-        tmp.tmp = null;
-    }
-
-    /**
-     * Get temporary variable that is used for real time parameter
-     * adjustments. The temporary variable is created if it does not exist
-     * yet. Use {@link #applyTmp()} or {@link #discardTmp()} if the temporary
-     * variable is not needed anymore.
-     * @return temporary variable
-     */
-    public ImageEntry getTmp() {
-        if (tmp == null) {
-            createTmp();
-        }
-        return tmp;
-    }
-
-    /**
-     * Copy the values from the temporary variable to the main instance. The
-     * temporary variable is deleted.
-     * @see #discardTmp()
-     */
-    public void applyTmp() {
-        if (tmp != null) {
-            pos = tmp.pos;
-            speed = tmp.speed;
-            elevation = tmp.elevation;
-            gpsTime = tmp.gpsTime;
-            exifImgDir = tmp.exifImgDir;
-            isNewGpsData = tmp.isNewGpsData;
-            tmp = null;
-        }
-    }
-
-    /**
-     * Delete the temporary variable. Temporary modifications are lost.
-     * @see #applyTmp()
-     */
-    public void discardTmp() {
-        tmp = null;
-    }
-
-    /**
-     * If it has been tagged i.e. matched to a gpx track or retrieved lat/lon from exif
-     * @return {@code true} if it has been tagged
-     */
-    public boolean isTagged() {
-        return pos != null;
-    }
-
-    /**
-     * String representation. (only partial info)
-     */
-    @Override
-    public String toString() {
-        return file.getName()+": "+
-        "pos = "+pos+" | "+
-        "exifCoor = "+exifCoor+" | "+
-        (tmp == null ? " tmp==null" :
-            " [tmp] pos = "+tmp.pos);
-    }
-
-    /**
-     * Indicates that the image has new GPS data.
-     * That flag is set by new GPS data providers.  It is used e.g. by the photo_geotagging plugin
-     * to decide for which image file the EXIF GPS data needs to be (re-)written.
-     * @since 6392
-     */
-    public void flagNewGpsData() {
-        isNewGpsData = true;
-   }
-
-    /**
-     * Remove the flag that indicates new GPS data.
-     * The flag is cleared by a new GPS data consumer.
-     */
-    public void unflagNewGpsData() {
-        isNewGpsData = false;
-    }
-
-    /**
-     * Queries whether the GPS data changed. The flag value from the temporary
-     * copy is returned if that copy exists.
-     * @return {@code true} if GPS data changed, {@code false} otherwise
-     * @since 6392
-     */
-    public boolean hasNewGpsData() {
-        if (tmp != null)
-            return tmp.isNewGpsData;
-        return isNewGpsData;
-    }
-
-    /**
-     * Extract GPS metadata from image EXIF. Has no effect if the image file is not set
-     *
-     * If successful, fills in the LatLon, speed, elevation, image direction, and other attributes
-     * @since 9270
-     */
-    public void extractExif() {
-
-        Metadata metadata;
-
-        if (file == null) {
-            return;
-        }
-
-        try {
-            metadata = JpegMetadataReader.readMetadata(file);
-        } catch (CompoundException | IOException ex) {
-            Logging.error(ex);
-            setExifTime(null);
-            setExifCoor(null);
-            setPos(null);
-            return;
-        }
-
-        // Changed to silently cope with no time info in exif. One case
-        // of person having time that couldn't be parsed, but valid GPS info
-        try {
-            setExifTime(ExifReader.readTime(metadata));
-        } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException ex) {
-            Logging.warn(ex);
-            setExifTime(null);
-        }
-
-        final Directory dir = metadata.getFirstDirectoryOfType(JpegDirectory.class);
-        final Directory dirExif = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
-        final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
-
-        try {
-            if (dirExif != null) {
-                int orientation = dirExif.getInt(ExifIFD0Directory.TAG_ORIENTATION);
-                setExifOrientation(orientation);
-            }
-        } catch (MetadataException ex) {
-            Logging.debug(ex);
-        }
-
-        try {
-            if (dir != null) {
-                // there are cases where these do not match width and height stored in dirExif
-                int width = dir.getInt(JpegDirectory.TAG_IMAGE_WIDTH);
-                int height = dir.getInt(JpegDirectory.TAG_IMAGE_HEIGHT);
-                setWidth(width);
-                setHeight(height);
-            }
-        } catch (MetadataException ex) {
-            Logging.debug(ex);
-        }
-
-        if (dirGps == null) {
-            setExifCoor(null);
-            setPos(null);
-            return;
-        }
-
-        final Double speed = ExifReader.readSpeed(dirGps);
-        if (speed != null) {
-            setSpeed(speed);
-        }
-
-        final Double ele = ExifReader.readElevation(dirGps);
-        if (ele != null) {
-            setElevation(ele);
-        }
-
-        try {
-            final LatLon latlon = ExifReader.readLatLon(dirGps);
-            setExifCoor(latlon);
-            setPos(getExifCoor());
-        } catch (MetadataException | IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
-            Logging.error("Error reading EXIF from file: " + ex);
-            setExifCoor(null);
-            setPos(null);
-        }
-
-        try {
-            final Double direction = ExifReader.readDirection(dirGps);
-            if (direction != null) {
-                setExifImgDir(direction);
-            }
-        } catch (IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
-            Logging.debug(ex);
-        }
-
-        final Date gpsDate = dirGps.getGpsDate();
-        if (gpsDate != null) {
-            setExifGpsTime(gpsDate);
-        }
-    }
 }
Index: trunk/src/org/openstreetmap/josm/gui/layer/geoimage/Offset.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/geoimage/Offset.java	(revision 14204)
+++ 	(revision )
@@ -1,102 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.layer.geoimage;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.text.ParseException;
-import java.util.Locale;
-import java.util.Objects;
-import java.util.concurrent.TimeUnit;
-
-import org.openstreetmap.josm.tools.JosmDecimalFormatSymbolsProvider;
-import org.openstreetmap.josm.tools.Pair;
-
-/**
- * Time offset of GPX correlation.
- * @since 11914 (extracted from {@link CorrelateGpxWithImages})
- */
-public final class Offset {
-
-    static final Offset ZERO = new Offset(0);
-    private final long milliseconds;
-
-    private Offset(long milliseconds) {
-        this.milliseconds = milliseconds;
-    }
-
-    static Offset milliseconds(long milliseconds) {
-        return new Offset(milliseconds);
-    }
-
-    static Offset seconds(long seconds) {
-        return new Offset(1000 * seconds);
-    }
-
-    long getMilliseconds() {
-        return milliseconds;
-    }
-
-    long getSeconds() {
-        return milliseconds / 1000;
-    }
-
-    String formatOffset() {
-        if (milliseconds % 1000 == 0) {
-            return Long.toString(milliseconds / 1000);
-        } else if (milliseconds % 100 == 0) {
-            return String.format(Locale.ENGLISH, "%.1f", milliseconds / 1000.);
-        } else {
-            return String.format(Locale.ENGLISH, "%.3f", milliseconds / 1000.);
-        }
-    }
-
-    static Offset parseOffset(String offset) throws ParseException {
-        String error = tr("Error while parsing offset.\nExpected format: {0}", "number");
-
-        if (!offset.isEmpty()) {
-            try {
-                if (offset.startsWith("+")) {
-                    offset = offset.substring(1);
-                }
-                return Offset.milliseconds(Math.round(JosmDecimalFormatSymbolsProvider.parseDouble(offset) * 1000));
-            } catch (NumberFormatException nfe) {
-                throw (ParseException) new ParseException(error, 0).initCause(nfe);
-            }
-        } else {
-            return Offset.ZERO;
-        }
-    }
-
-    int getDayOffset() {
-        // Find day difference
-        return (int) Math.round(((double) getMilliseconds()) / TimeUnit.DAYS.toMillis(1));
-    }
-
-    Offset withoutDayOffset() {
-        return milliseconds(getMilliseconds() - TimeUnit.DAYS.toMillis(getDayOffset()));
-    }
-
-    Pair<Timezone, Offset> splitOutTimezone() {
-        // In hours
-        final double tz = ((double) withoutDayOffset().getSeconds()) / TimeUnit.HOURS.toSeconds(1);
-
-        // Due to imprecise clocks we might get a "+3:28" timezone, which should obviously be 3:30 with
-        // -2 minutes offset. This determines the real timezone and finds offset.
-        final double timezone = (double) Math.round(tz * 2) / 2; // hours, rounded to one decimal place
-        final long delta = Math.round(getMilliseconds() - timezone * TimeUnit.HOURS.toMillis(1));
-        return Pair.create(new Timezone(timezone), Offset.milliseconds(delta));
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (!(o instanceof Offset)) return false;
-        Offset offset = (Offset) o;
-        return milliseconds == offset.milliseconds;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(milliseconds);
-    }
-}
Index: trunk/src/org/openstreetmap/josm/gui/layer/geoimage/Timezone.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/geoimage/Timezone.java	(revision 14204)
+++ 	(revision )
@@ -1,142 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.layer.geoimage;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.text.ParseException;
-import java.util.Objects;
-
-/**
- * Timezone in hours.<p>
- * TODO: should probably be replaced by {@link java.util.TimeZone}.
- * @since 11914 (extracted from {@link CorrelateGpxWithImages})
- */
-public final class Timezone {
-
-    static final Timezone ZERO = new Timezone(0.0);
-    private final double timezone;
-
-    Timezone(double hours) {
-        this.timezone = hours;
-    }
-
-    /**
-     * Returns the timezone in hours.
-     * @return the timezone in hours
-     */
-    public double getHours() {
-        return timezone;
-    }
-
-    String formatTimezone() {
-        StringBuilder ret = new StringBuilder();
-
-        double timezone = this.timezone;
-        if (timezone < 0) {
-            ret.append('-');
-            timezone = -timezone;
-        } else {
-            ret.append('+');
-        }
-        ret.append((long) timezone).append(':');
-        int minutes = (int) ((timezone % 1) * 60);
-        if (minutes < 10) {
-            ret.append('0');
-        }
-        ret.append(minutes);
-
-        return ret.toString();
-    }
-
-    static Timezone parseTimezone(String timezone) throws ParseException {
-
-        if (timezone.isEmpty())
-            return ZERO;
-
-        String error = tr("Error while parsing timezone.\nExpected format: {0}", "+H:MM");
-
-        char sgnTimezone = '+';
-        StringBuilder hTimezone = new StringBuilder();
-        StringBuilder mTimezone = new StringBuilder();
-        int state = 1; // 1=start/sign, 2=hours, 3=minutes.
-        for (int i = 0; i < timezone.length(); i++) {
-            char c = timezone.charAt(i);
-            switch (c) {
-                case ' ':
-                    if (state != 2 || hTimezone.length() != 0)
-                        throw new ParseException(error, i);
-                    break;
-                case '+':
-                case '-':
-                    if (state == 1) {
-                        sgnTimezone = c;
-                        state = 2;
-                    } else
-                        throw new ParseException(error, i);
-                    break;
-                case ':':
-                case '.':
-                    if (state == 2) {
-                        state = 3;
-                    } else
-                        throw new ParseException(error, i);
-                    break;
-                case '0':
-                case '1':
-                case '2':
-                case '3':
-                case '4':
-                case '5':
-                case '6':
-                case '7':
-                case '8':
-                case '9':
-                    switch (state) {
-                        case 1:
-                        case 2:
-                            state = 2;
-                            hTimezone.append(c);
-                            break;
-                        case 3:
-                            mTimezone.append(c);
-                            break;
-                        default:
-                            throw new ParseException(error, i);
-                    }
-                    break;
-                default:
-                    throw new ParseException(error, i);
-            }
-        }
-
-        int h = 0;
-        int m = 0;
-        try {
-            h = Integer.parseInt(hTimezone.toString());
-            if (mTimezone.length() > 0) {
-                m = Integer.parseInt(mTimezone.toString());
-            }
-        } catch (NumberFormatException nfe) {
-            // Invalid timezone
-            throw (ParseException) new ParseException(error, 0).initCause(nfe);
-        }
-
-        if (h > 12 || m > 59)
-            throw new ParseException(error, 0);
-        else
-            return new Timezone((h + m / 60.0) * (sgnTimezone == '-' ? -1 : 1));
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (!(o instanceof Timezone)) return false;
-        Timezone timezone1 = (Timezone) o;
-        return Double.compare(timezone1.timezone, timezone) == 0;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(timezone);
-    }
-}
Index: trunk/src/org/openstreetmap/josm/io/session/GeoImageSessionImporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/session/GeoImageSessionImporter.java	(revision 14204)
+++ trunk/src/org/openstreetmap/josm/io/session/GeoImageSessionImporter.java	(revision 14205)
@@ -11,4 +11,5 @@
 
 import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.gpx.GpxImageEntry;
 import org.openstreetmap.josm.gui.layer.GpxLayer;
 import org.openstreetmap.josm.gui.layer.Layer;
@@ -71,5 +72,5 @@
     }
 
-    private static void handleElement(ImageEntry entry, Element attrElem) {
+    private static void handleElement(GpxImageEntry entry, Element attrElem) {
         try {
             switch(attrElem.getTagName()) {
