Index: /trunk/src/org/openstreetmap/josm/data/gpx/GpxImageCorrelation.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/gpx/GpxImageCorrelation.java	(revision 18060)
+++ /trunk/src/org/openstreetmap/josm/data/gpx/GpxImageCorrelation.java	(revision 18061)
@@ -3,10 +3,15 @@
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.projection.Projection;
+import org.openstreetmap.josm.data.projection.ProjectionRegistry;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Pair;
+import org.openstreetmap.josm.tools.Utils;
 
 /**
@@ -25,57 +30,15 @@
      * @param images images to match
      * @param selectedGpx selected GPX data
-     * @param offset offset
-     * @param forceTags force tagging of all photos, otherwise prefs are used
+     * @param settings correlation settings
      * @return number of matched points
      */
-    public static int matchGpxTrack(List<? extends GpxImageEntry> images, GpxData selectedGpx, long offset, boolean forceTags) {
+    public static int matchGpxTrack(List<? extends GpxImageEntry> images, GpxData selectedGpx, GpxImageCorrelationSettings settings) {
         int ret = 0;
-
-        long prevWpTime = 0;
-        WayPoint prevWp = null;
-
-        List<List<List<WayPoint>>> trks = new ArrayList<>();
-
-        for (IGpxTrack trk : selectedGpx.tracks) {
-            List<List<WayPoint>> segs = new ArrayList<>();
-            for (IGpxTrackSegment 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).hasDate()) {
-                            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((o1, o2) -> {
-                    if (o1.isEmpty() || o2.isEmpty())
-                        return 0;
-                    return o1.get(0).compareTo(o2.get(0));
-                });
-                trks.add(segs);
-            }
-        }
-        //sort tracks by first waypoint of first segment
-        trks.sort((o1, o2) -> {
-            if (o1.isEmpty() || o1.get(0).isEmpty()
-             || o2.isEmpty() || o2.get(0).isEmpty())
-                return 0;
-            return o1.get(0).get(0).compareTo(o2.get(0).get(0));
-        });
 
         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
+        if (settings.isForceTags()) {
+            // 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;
@@ -102,12 +65,17 @@
                     Config.getPref().getInt("geoimage.seg.tag.time.val", 2) : Integer.MAX_VALUE;
         }
+
+        final GpxImageDirectionPositionSettings dirpos = settings.getDirectionPositionSettings();
+        final long offset = settings.getOffset();
+
         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);
+        long prevWpTime = 0;
+        WayPoint prevWp = null;
+
+        for (List<List<WayPoint>> segs : loadTracks(selectedGpx.tracks)) {
+            boolean firstSegment = true;
+            for (List<WayPoint> wps : segs) {
                 for (int i = 0; i < wps.size(); i++) {
-                    WayPoint curWp = wps.get(i);
+                    final WayPoint curWp = wps.get(i);
                     // Interpolate timestamps in the segment, if one or more waypoints miss them
                     if (!curWp.hasDate()) {
@@ -142,5 +110,7 @@
                     int tagTime = 0;
                     if (i == 0) {
-                        if (s == 0) { //First segment of the track, so apply settings for tracks
+                        if (firstSegment) {
+                            // First segment of the track, so apply settings for tracks
+                            firstSegment = false;
                             if (!trkInt || isFirst || prevWp == null ||
                                     Math.abs(curWpTime - prevWpTime) > TimeUnit.MINUTES.toMillis(trkTime) ||
@@ -152,5 +122,6 @@
                                 }
                             }
-                        } else { //Apply settings for segments
+                        } else {
+                            // Apply settings for segments
                             if (!segInt || prevWp == null ||
                                     Math.abs(curWpTime - prevWpTime) > TimeUnit.MINUTES.toMillis(segTime) ||
@@ -163,5 +134,5 @@
                         }
                     }
-                    ret += matchPoints(images, prevWp, prevWpTime, curWp, curWpTime, offset, interpolate, tagTime, false);
+                    ret += matchPoints(images, prevWp, prevWpTime, curWp, curWpTime, offset, interpolate, tagTime, false, dirpos);
                     prevWp = curWp;
                     prevWpTime = curWpTime;
@@ -170,7 +141,48 @@
         }
         if (trkTag && prevWp != null) {
-            ret += matchPoints(images, prevWp, prevWpTime, prevWp, prevWpTime, offset, false, trkTagTime, true);
+            ret += matchPoints(images, prevWp, prevWpTime, prevWp, prevWpTime, offset, false, trkTagTime, true, dirpos);
         }
         return ret;
+    }
+
+    static List<List<List<WayPoint>>> loadTracks(Collection<IGpxTrack> tracks) {
+        List<List<List<WayPoint>>> trks = new ArrayList<>();
+        for (IGpxTrack trk : tracks) {
+            List<List<WayPoint>> segs = new ArrayList<>();
+            for (IGpxTrackSegment 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).hasDate()) {
+                            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((o1, o2) -> {
+                    if (o1.isEmpty() || o2.isEmpty())
+                        return 0;
+                    return o1.get(0).compareTo(o2.get(0));
+                });
+                trks.add(segs);
+            }
+        }
+        //sort tracks by first waypoint of first segment
+        trks.sort((o1, o2) -> {
+            if (o1.isEmpty() || o1.get(0).isEmpty()
+             || o2.isEmpty() || o2.get(0).isEmpty())
+                return 0;
+            return o1.get(0).get(0).compareTo(o2.get(0).get(0));
+        });
+        return trks;
     }
 
@@ -190,5 +202,5 @@
 
     private static int matchPoints(List<? extends GpxImageEntry> images, WayPoint prevWp, long prevWpTime, WayPoint curWp, long curWpTime,
-            long offset, boolean interpolate, int tagTime, boolean isLast) {
+            long offset, boolean interpolate, int tagTime, boolean isLast, GpxImageDirectionPositionSettings dirpos) {
 
         int ret = 0;
@@ -218,5 +230,5 @@
         }
 
-        Double curElevation = getElevation(curWp);
+        final Double curElevation = getElevation(curWp);
 
         if (!interpolate || isLast) {
@@ -250,6 +262,6 @@
             // previous track point assuming a constant speed in between
             while (i >= 0) {
-                GpxImageEntry curImg = images.get(i);
-                GpxImageEntry curTmp = curImg.getTmp();
+                final GpxImageEntry curImg = images.get(i);
+                final GpxImageEntry curTmp = curImg.getTmp();
                 final long imgTime = curImg.getExifInstant().toEpochMilli();
                 if (imgTime < prevWpTime) {
@@ -258,9 +270,29 @@
                 if (!curTmp.hasNewGpsData()) {
                     // 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));
+                    final double timeDiff = (double) (imgTime - prevWpTime) / Math.abs(curWpTime - prevWpTime);
+                    final boolean shiftXY = dirpos.getShiftImageX() != 0d || dirpos.getShiftImageY() != 0d;
+                    final LatLon prevCoor = prevWp.getCoor();
+                    final LatLon curCoor = curWp.getCoor();
+                    LatLon position = prevCoor.interpolate(curCoor, timeDiff);
+                    if (shiftXY || dirpos.isSetImageDirection()) {
+                        double direction = prevCoor.bearing(curCoor);
+                        if (dirpos.isSetImageDirection()) {
+                            curTmp.setExifImgDir((Utils.toDegrees(direction) + dirpos.getImageDirectionAngleOffset()) % 360d);
+                            direction = Utils.toRadians(curTmp.getExifImgDir());
+                        }
+                        if (shiftXY) {
+                            final Projection proj = ProjectionRegistry.getProjection();
+                            final double offsetX = dirpos.getShiftImageX();
+                            final double offsetY = dirpos.getShiftImageY();
+                            final double r = Math.sqrt(offsetX * offsetX + offsetY * offsetY);
+                            final double orientation = (direction + LatLon.ZERO.bearing(new LatLon(offsetX, offsetY))) % (2 * Math.PI);
+                            position = proj.eastNorth2latlon(proj.latlon2eastNorth(position)
+                                    .add(r * Math.sin(orientation), r * Math.cos(orientation)));
+                        }
+                    }
+                    curTmp.setPos(position);
                     curTmp.setSpeed(speed);
                     if (curElevation != null && prevElevation != null) {
-                        curTmp.setElevation(prevElevation + (curElevation - prevElevation) * timeDiff);
+                        curTmp.setElevation(prevElevation + (curElevation - prevElevation) * timeDiff + dirpos.getElevationShift());
                     }
                     curTmp.setGpsTime(curImg.getExifInstant().minusMillis(offset));
Index: /trunk/src/org/openstreetmap/josm/data/gpx/GpxImageCorrelationSettings.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/gpx/GpxImageCorrelationSettings.java	(revision 18061)
+++ /trunk/src/org/openstreetmap/josm/data/gpx/GpxImageCorrelationSettings.java	(revision 18061)
@@ -0,0 +1,61 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.gpx;
+
+import java.util.Objects;
+
+/**
+ * Correlation settings used by {@link GpxImageCorrelation}.
+ * @since 18061
+ */
+public class GpxImageCorrelationSettings {
+
+    private final long offset;
+    private final boolean forceTags;
+    private final GpxImageDirectionPositionSettings directionPositionSettings;
+
+    /**
+     * Constructs a new {@code GpxImageCorrelationSettings}.
+     * @param offset offset in milliseconds
+     * @param forceTags force tagging of all photos, otherwise prefs are used
+     */
+    public GpxImageCorrelationSettings(long offset, boolean forceTags) {
+        this(offset, forceTags, new GpxImageDirectionPositionSettings(false, 0, 0, 0, 0));
+    }
+
+    /**
+     * Constructs a new {@code GpxImageCorrelationSettings}.
+     * @param offset offset in milliseconds
+     * @param forceTags force tagging of all photos, otherwise prefs are used
+     * @param directionPositionSettings direction/position settings
+     */
+    public GpxImageCorrelationSettings(long offset, boolean forceTags,
+            GpxImageDirectionPositionSettings directionPositionSettings) {
+        this.offset = offset;
+        this.forceTags = forceTags;
+        this.directionPositionSettings = Objects.requireNonNull(directionPositionSettings);
+    }
+
+    /**
+     * Returns the offset in milliseconds.
+     * @return the offset in milliseconds
+     */
+    public long getOffset() {
+        return offset;
+    }
+
+    /**
+     * Determines if tagging of all photos must be forced, otherwise prefs are used
+     * @return {@code true} if tagging of all photos must be forced, otherwise prefs are used
+     */
+    public boolean isForceTags() {
+        return forceTags;
+    }
+
+    /**
+     * Returns the direction/position settings.
+     * @return the direction/position settings
+     */
+    public GpxImageDirectionPositionSettings getDirectionPositionSettings() {
+        return directionPositionSettings;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/gpx/GpxImageDirectionPositionSettings.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/gpx/GpxImageDirectionPositionSettings.java	(revision 18061)
+++ /trunk/src/org/openstreetmap/josm/data/gpx/GpxImageDirectionPositionSettings.java	(revision 18061)
@@ -0,0 +1,72 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.gpx;
+
+/**
+ * Image direction / position modification settings used by {@link GpxImageCorrelationSettings}.
+ * @since 18061
+ */
+public class GpxImageDirectionPositionSettings {
+
+    private final boolean setImageDirection;
+    private final double imageDirectionAngleOffset;
+    private final double shiftImageX;
+    private final double shiftImageY;
+    private final double elevationShift;
+
+    /**
+     * Constructs a new {@code GpxImageDirectionPositionSettings}.
+     * @param setImageDirection determines if image direction must be set towards the next GPX waypoint
+     * @param imageDirectionAngleOffset direction angle offset in degrees
+     * @param shiftImageX image shift on X axis relative to the direction in meters
+     * @param shiftImageY image shift on Y axis relative to the direction in meters
+     * @param elevationShift image elevation shift in meters
+     */
+    public GpxImageDirectionPositionSettings(
+            boolean setImageDirection, double imageDirectionAngleOffset, double shiftImageX, double shiftImageY, double elevationShift) {
+        this.setImageDirection = setImageDirection;
+        this.imageDirectionAngleOffset = imageDirectionAngleOffset;
+        this.shiftImageX = shiftImageX;
+        this.shiftImageY = shiftImageY;
+        this.elevationShift = elevationShift;
+    }
+
+    /**
+     * Determines if image direction must be set towards the next GPX waypoint
+     * @return {@code true} if image direction must be set towards the next GPX waypoint
+     */
+    public boolean isSetImageDirection() {
+        return setImageDirection;
+    }
+
+    /**
+     * Returns direction angle offset in degrees
+     * @return direction angle offset in degrees
+     */
+    public double getImageDirectionAngleOffset() {
+        return imageDirectionAngleOffset;
+    }
+
+    /**
+     * Returns image shift on X axis relative to the direction in meters
+     * @return image shift on X axis relative to the direction in meters
+     */
+    public double getShiftImageX() {
+        return shiftImageX;
+    }
+
+    /**
+     * Returns image shift on Y axis relative to the direction in meters
+     * @return image shift on Y axis relative to the direction in meters
+     */
+    public double getShiftImageY() {
+        return shiftImageY;
+    }
+
+    /**
+     * Returns image elevation shift in meters
+     * @return image elevation shift in meters
+     */
+    public double getElevationShift() {
+        return elevationShift;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java	(revision 18060)
+++ /trunk/src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java	(revision 18061)
@@ -42,9 +42,14 @@
 import javax.swing.MutableComboBoxModel;
 import javax.swing.SwingConstants;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
 import javax.swing.event.DocumentEvent;
 import javax.swing.event.DocumentListener;
 
+import org.openstreetmap.josm.actions.ExpertToggleAction;
+import org.openstreetmap.josm.actions.ExpertToggleAction.ExpertModeChangeListener;
 import org.openstreetmap.josm.data.gpx.GpxData;
 import org.openstreetmap.josm.data.gpx.GpxImageCorrelation;
+import org.openstreetmap.josm.data.gpx.GpxImageCorrelationSettings;
 import org.openstreetmap.josm.data.gpx.GpxImageEntry;
 import org.openstreetmap.josm.data.gpx.GpxTimeOffset;
@@ -77,5 +82,5 @@
  * @since 2566
  */
-public class CorrelateGpxWithImages extends AbstractAction implements Destroyable {
+public class CorrelateGpxWithImages extends AbstractAction implements ExpertModeChangeListener, Destroyable {
 
     private static MutableComboBoxModel<GpxDataWrapper> gpxModel;
@@ -94,4 +99,5 @@
         new ImageProvider("dialogs/geoimage/gpx2img").getResource().attachImageIcon(this, true);
         this.yLayer = layer;
+        ExpertToggleAction.addExpertModeChangeListener(this);
     }
 
@@ -237,4 +243,6 @@
     private JCheckBox cbShowThumbs;
     private JLabel statusBarText;
+    private JSeparator sepDirectionPosition;
+    private ImageDirectionPositionPanel pDirectionPosition;
 
     // remember the last number of matched photos
@@ -556,4 +564,15 @@
         panelTf.add(cbShowThumbs, gbc);
 
+        gbc = GBC.eol().fill(GBC.HORIZONTAL).insets(0, 12, 0, 0);
+        sepDirectionPosition = new JSeparator(SwingConstants.HORIZONTAL);
+        panelTf.add(sepDirectionPosition, gbc);
+
+        gbc = GBC.eol();
+        gbc.gridwidth = 3;
+        pDirectionPosition = ImageDirectionPositionPanel.forGpxTrace();
+        panelTf.add(pDirectionPosition, gbc);
+
+        expertChanged(ExpertToggleAction.isExpert());
+
         final JPanel statusBar = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
         statusBar.setBorder(BorderFactory.createLoweredBevelBorder());
@@ -563,4 +582,5 @@
 
         RepaintTheMapListener repaintTheMap = new RepaintTheMapListener();
+        pDirectionPosition.addFocusListenerOnComponent(repaintTheMap);
         tfTimezone.addFocusListener(repaintTheMap);
         tfOffset.addFocusListener(repaintTheMap);
@@ -570,4 +590,5 @@
         cbExifImg.addItemListener(statusBarUpdaterWithRepaint);
         cbTaggedImg.addItemListener(statusBarUpdaterWithRepaint);
+        pDirectionPosition.addChangeListenerOnComponents(statusBarUpdaterWithRepaint);
 
         statusBarUpdater.matchAndUpdateStatusBar();
@@ -596,4 +617,17 @@
     }
 
+    @Override
+    public void expertChanged(boolean isExpert) {
+        if (sepDirectionPosition != null) {
+            sepDirectionPosition.setVisible(isExpert);
+        }
+        if (pDirectionPosition != null) {
+            pDirectionPosition.setVisible(isExpert);
+        }
+        if (syncDialog != null) {
+            syncDialog.pack();
+        }
+    }
+
     private static void removeDuplicates(File file) {
         for (int i = gpxModel.getSize() - 1; i >= 0; i--) {
@@ -613,5 +647,5 @@
     private final transient StatusBarUpdater statusBarUpdaterWithRepaint = new StatusBarUpdater(true);
 
-    private class StatusBarUpdater implements DocumentListener, ItemListener, ActionListener {
+    private class StatusBarUpdater implements DocumentListener, ItemListener, ChangeListener, ActionListener {
         private final boolean doRepaint;
 
@@ -637,4 +671,9 @@
         @Override
         public void itemStateChanged(ItemEvent e) {
+            matchAndUpdateStatusBar();
+        }
+
+        @Override
+        public void stateChanged(ChangeEvent e) {
             matchAndUpdateStatusBar();
         }
@@ -681,5 +720,8 @@
 
             final long offsetMs = ((long) (timezone.getHours() * TimeUnit.HOURS.toMillis(1))) + delta.getMilliseconds(); // in milliseconds
-            lastNumMatched = GpxImageCorrelation.matchGpxTrack(dateImgLst, selGpx.data, offsetMs, forceTags);
+            lastNumMatched = GpxImageCorrelation.matchGpxTrack(dateImgLst, selGpx.data,
+                    pDirectionPosition.isVisible() ?
+                            new GpxImageCorrelationSettings(offsetMs, forceTags, pDirectionPosition.getSettings()) :
+                            new GpxImageCorrelationSettings(offsetMs, forceTags));
 
             return trn("<html>Matched <b>{0}</b> of <b>{1}</b> photo to GPX track.</html>",
@@ -869,4 +911,5 @@
     @Override
     public void destroy() {
+        ExpertToggleAction.removeExpertModeChangeListener(this);
         if (cbGpx != null) {
             // Force the JCombobox to remove its eventListener from the static GpxDataWrapper
@@ -874,4 +917,15 @@
             cbGpx = null;
         }
+
+        outerPanel = null;
+        tfTimezone = null;
+        tfOffset = null;
+        cbExifImg = null;
+        cbTaggedImg = null;
+        cbShowThumbs = null;
+        statusBarText = null;
+        sepDirectionPosition = null;
+        pDirectionPosition = null;
+
         closeDialog();
     }
Index: /trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageDirectionPositionPanel.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageDirectionPositionPanel.java	(revision 18061)
+++ /trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageDirectionPositionPanel.java	(revision 18061)
@@ -0,0 +1,115 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.layer.geoimage;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.GridBagLayout;
+import java.awt.event.FocusListener;
+
+import javax.swing.JCheckBox;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSeparator;
+import javax.swing.JSpinner;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.SwingConstants;
+import javax.swing.event.ChangeListener;
+
+import org.openstreetmap.josm.data.gpx.GpxImageDirectionPositionSettings;
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ * Panel allowing user to enter {@link GpxImageDirectionPositionSettings}.
+ * @since 18061
+ */
+public class ImageDirectionPositionPanel extends JPanel {
+
+    private final JCheckBox cChangeImageDirection = new JCheckBox();
+    private final JSpinner sOffsetDegrees = new JSpinner(new SpinnerNumberModel(0, -360, 360, 1));
+
+    private final JSpinner sX = new JSpinner(new SpinnerNumberModel(0.0, -50.0, 50.0, 0.1));
+    private final JSpinner sY = new JSpinner(new SpinnerNumberModel(0.0, -50.0, 50.0, 0.1));
+    private final JSpinner sZ = new JSpinner(new SpinnerNumberModel(0.0, -20.0, 20.0, 0.1));
+
+    /**
+     * Constructs a new {@code ImageMetadataModificationPanel}
+     * @param changeDirectionText the text displayed for the change image direction combobox
+     */
+    protected ImageDirectionPositionPanel(String changeDirectionText) {
+        super(new GridBagLayout());
+
+        cChangeImageDirection.setText(changeDirectionText);
+        add(cChangeImageDirection, GBC.eol().insets(0, 0, 0, 5));
+        cChangeImageDirection.addActionListener(e -> sOffsetDegrees.setEnabled(!sOffsetDegrees.isEnabled()));
+        addSetting(tr("Offset angle in degrees:"), sOffsetDegrees);
+        sOffsetDegrees.setEnabled(false);
+
+        add(new JSeparator(SwingConstants.HORIZONTAL),
+                GBC.eol().fill(GBC.HORIZONTAL).insets(0, 12, 0, 12));
+
+        add(new JLabel(tr("Shift image relative to the direction (in meters)")),
+                GBC.eol().insets(0, 0, 0, 5));
+        addSetting(tr("X:"), sX);
+        addSetting(tr("Y:"), sY);
+        addSetting(tr("Elevation:"), sZ);
+    }
+
+    /**
+     * Returns a new {@code ImageMetadataModificationPanel} in a GPX trace context.
+     * @return a new {@code ImageMetadataModificationPanel} in a GPX trace context
+     */
+    public static ImageDirectionPositionPanel forGpxTrace() {
+        return new ImageDirectionPositionPanel(tr("Set image direction towards the next GPX waypoint"));
+    }
+
+    /**
+     * Returns a new {@code ImageMetadataModificationPanel} in an image sequence context.
+     * @return a new {@code ImageMetadataModificationPanel} in an image sequence context
+     */
+    public static ImageDirectionPositionPanel forImageSequence() {
+        return new ImageDirectionPositionPanel(tr("Set image direction towards the next one"));
+    }
+
+    protected void addSetting(String text, JComponent component) {
+        add(new JLabel(text, JLabel.RIGHT), GBC.std().insets(15, 0, 5, 5).fill(GBC.HORIZONTAL).weight(0, 0));
+        add(component, GBC.std().fill(GBC.HORIZONTAL));
+        add(GBC.glue(1, 0), GBC.eol().fill(GBC.HORIZONTAL).weight(1, 0));
+    }
+
+    /**
+     * Returns the settings set by user.
+     * @return the settings set by user
+     */
+    public GpxImageDirectionPositionSettings getSettings() {
+        return new GpxImageDirectionPositionSettings(
+                cChangeImageDirection.isSelected(),
+                (Integer) sOffsetDegrees.getValue(),
+                (Double) sX.getValue(),
+                (Double) sY.getValue(),
+                (Double) sZ.getValue());
+    }
+
+    /**
+     * Adds a focus listener on all spinners of this panel.
+     * @param focusListener focus listener to add
+     */
+    public void addFocusListenerOnComponent(FocusListener focusListener) {
+        sOffsetDegrees.addFocusListener(focusListener);
+        sX.addFocusListener(focusListener);
+        sY.addFocusListener(focusListener);
+        sZ.addFocusListener(focusListener);
+    }
+
+    /**
+     * Adds a change listener on all checkboxes and spinners of this panel.
+     * @param listener change listener to add
+     */
+    public void addChangeListenerOnComponents(ChangeListener listener) {
+        cChangeImageDirection.addChangeListener(listener);
+        sOffsetDegrees.addChangeListener(listener);
+        sX.addChangeListener(listener);
+        sY.addChangeListener(listener);
+        sZ.addChangeListener(listener);
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/gui/widgets/DefaultTextComponentValidator.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/widgets/DefaultTextComponentValidator.java	(revision 18060)
+++ /trunk/src/org/openstreetmap/josm/gui/widgets/DefaultTextComponentValidator.java	(revision 18061)
@@ -15,5 +15,5 @@
     /**
      * Constructs a new {@code DefaultTextComponentValidator}.
-     * @param tc he text component. Must not be null.
+     * @param tc the text component. Must not be null.
      * @param validFeedback text displayed for valid feedback
      * @param invalidFeedback text displayed for invalid feedback
Index: /trunk/test/unit/org/openstreetmap/josm/data/gpx/GpxImageCorrelationTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/data/gpx/GpxImageCorrelationTest.java	(revision 18060)
+++ /trunk/test/unit/org/openstreetmap/josm/data/gpx/GpxImageCorrelationTest.java	(revision 18061)
@@ -115,5 +115,5 @@
     @Test
     void testMatchGpxTrack1() {
-        assertEquals(7, GpxImageCorrelation.matchGpxTrack(images, gpx, 0, false));
+        assertEquals(7, GpxImageCorrelation.matchGpxTrack(images, gpx, new GpxImageCorrelationSettings(0, false)));
         assertEquals(null, ib.getPos());
         assertEquals(new CachedLatLon(47.19286847859621, 8.79732714034617), i0.getPos()); // start of track
@@ -167,5 +167,5 @@
         s.putBoolean("geoimage.seg.int", false);
 
-        assertEquals(4, GpxImageCorrelation.matchGpxTrack(images, gpx, 0, false));
+        assertEquals(4, GpxImageCorrelation.matchGpxTrack(images, gpx, new GpxImageCorrelationSettings(0, false)));
         assertEquals(null, ib.getPos());
         assertEquals(null, i0.getPos());
@@ -195,5 +195,5 @@
         s.putBoolean("geoimage.seg.int", false);
 
-        assertEquals(6, GpxImageCorrelation.matchGpxTrack(images, gpx, 0, false));
+        assertEquals(6, GpxImageCorrelation.matchGpxTrack(images, gpx, new GpxImageCorrelationSettings(0, false)));
         assertEquals(null, ib.getPos());
         assertEquals(new CachedLatLon(47.19286847859621, 8.79732714034617), i0.getPos());
@@ -217,5 +217,5 @@
     @Test
     void testMatchGpxTrack4() {
-        assertEquals(9, GpxImageCorrelation.matchGpxTrack(images, gpx, 0, true));
+        assertEquals(9, GpxImageCorrelation.matchGpxTrack(images, gpx, new GpxImageCorrelationSettings(0, true)));
         assertEquals(new CachedLatLon(47.19286847859621, 8.79732714034617), ib.getPos());
         assertEquals(new CachedLatLon(47.19286847859621, 8.79732714034617), i0.getPos());
@@ -254,5 +254,5 @@
         s.putBoolean("geoimage.seg.int.dist", false);
 
-        assertEquals(9, GpxImageCorrelation.matchGpxTrack(images, gpx, 0, false));
+        assertEquals(9, GpxImageCorrelation.matchGpxTrack(images, gpx, new GpxImageCorrelationSettings(0, false)));
         assertEquals(new CachedLatLon(47.19286847859621, 8.79732714034617), ib.getPos());
         assertEquals(new CachedLatLon(47.19286847859621, 8.79732714034617), i0.getPos());
@@ -288,5 +288,5 @@
         s.putBoolean("geoimage.seg.int", false);
 
-        assertEquals(4, GpxImageCorrelation.matchGpxTrack(images, gpx, 0, false));
+        assertEquals(4, GpxImageCorrelation.matchGpxTrack(images, gpx, new GpxImageCorrelationSettings(0, false)));
     }
 
@@ -307,5 +307,5 @@
         s.putBoolean("geoimage.seg.int", false);
 
-        assertEquals(6, GpxImageCorrelation.matchGpxTrack(images, gpx, 0, false));
+        assertEquals(6, GpxImageCorrelation.matchGpxTrack(images, gpx, new GpxImageCorrelationSettings(0, false)));
     }
 
@@ -326,5 +326,5 @@
         s.putBoolean("geoimage.seg.int", false);
 
-        assertEquals(4, GpxImageCorrelation.matchGpxTrack(images, gpx, 0, false));
+        assertEquals(4, GpxImageCorrelation.matchGpxTrack(images, gpx, new GpxImageCorrelationSettings(0, false)));
     }
 
@@ -345,5 +345,5 @@
         s.putBoolean("geoimage.seg.int", false);
 
-        assertEquals(6, GpxImageCorrelation.matchGpxTrack(images, gpx, 0, false));
+        assertEquals(6, GpxImageCorrelation.matchGpxTrack(images, gpx, new GpxImageCorrelationSettings(0, false)));
     }
 
