Index: dist/photoadjust.jar
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/java-archive
Index: plugins/photoadjust/src/org/openstreetmap/josm/plugins/photoadjust/InterpolateImages.java
===================================================================
--- plugins/photoadjust/src/org/openstreetmap/josm/plugins/photoadjust/InterpolateImages.java	(nonexistent)
+++ plugins/photoadjust/src/org/openstreetmap/josm/plugins/photoadjust/InterpolateImages.java	(working copy)
@@ -0,0 +1,121 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.photoadjust;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.awt.geom.Point2D;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.data.ImageData;
+import org.openstreetmap.josm.data.ImageData.ImageDataUpdateListener;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.MapView;
+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.layer.geoimage.GeoImageLayer;
+import org.openstreetmap.josm.gui.layer.geoimage.ImageEntry;
+
+/**
+ * Interpolate a part of a geotagged sequence
+ */
+public class InterpolateImages extends JosmAction implements LayerChangeListener, ImageDataUpdateListener {
+
+    public InterpolateImages() {
+        super(tr("Interpolate position between the 2 images"),
+                null,
+                tr("Interpolate images position between the 2 selected images"),
+                null, false, false);
+
+        installAdapters();
+        updateEnabledState();
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent evt) {
+        if (!getLayerWithTwoSelectedImages().isPresent()) {
+            return;
+        }
+        GeoImageLayer layer = getLayerWithTwoSelectedImages().get();
+        ImageData data = layer.getImageData();
+
+        interpolate(data, MainApplication.getMap().mapView);
+    }
+
+    public void interpolate(ImageData data, MapView mapView) {
+        List<ImageEntry> images = data.getImages();
+        List<ImageEntry> selectedImages = data.getSelectedImages();
+        Collections.sort(selectedImages);
+        ImageEntry firstPhoto = selectedImages.get(0);
+        ImageEntry lastPhoto = selectedImages.get(1);
+        final Point2D firstPos = mapView.getPoint2D(firstPhoto.getPos());
+        final Point2D lastPos = mapView.getPoint2D(lastPhoto.getPos());
+        int nbSegments = images.indexOf(lastPhoto) - images.indexOf(firstPhoto);
+        double firstPosX = firstPos.getX();
+        double firstPosY = firstPos.getY();
+        double offsetX = (lastPos.getX() - firstPosX) / nbSegments;
+        double offsetY = (lastPos.getY() - firstPosY) / nbSegments;
+        int firstIndex = images.indexOf(firstPhoto);
+        for (int idx = 1; idx < nbSegments; idx++) {
+            ImageEntry image = images.get(firstIndex + idx);
+            LatLon newPos = mapView.getLatLon(
+            		firstPosX + offsetX * idx,
+                    firstPosY + offsetY * idx
+                    );
+            image.setPos(newPos);
+            image.flagNewGpsData();
+        }
+        data.notifyImageUpdate();
+    }
+
+    @Override
+    protected void installAdapters() {
+        MainApplication.getLayerManager().addLayerChangeListener(this);
+    }
+
+    @Override
+    protected void updateEnabledState() {
+        setEnabled(getLayerWithTwoSelectedImages().isPresent());
+    }
+
+    private static Optional<GeoImageLayer> getLayerWithTwoSelectedImages() {
+        List<GeoImageLayer> list = MainApplication.getLayerManager().getLayersOfType(GeoImageLayer.class);
+        return list.stream().filter(l -> l.getImageData().getSelectedImages().size() == 2).findFirst();
+    }
+
+    @Override
+    public void layerAdded(LayerAddEvent evt) {
+        Layer layer = evt.getAddedLayer();
+        if (layer instanceof GeoImageLayer) {
+            ((GeoImageLayer) layer).getImageData().addImageDataUpdateListener(this);
+        }
+    }
+
+    @Override
+    public void layerRemoving(LayerRemoveEvent evt) {
+        Layer layer = evt.getRemovedLayer();
+
+        if (layer instanceof GeoImageLayer) {
+            ((GeoImageLayer) layer).getImageData().removeImageDataUpdateListener(this);
+        }
+        updateEnabledState();
+    }
+
+    @Override
+    public void layerOrderChanged(LayerOrderChangeEvent evt) {}
+
+    @Override
+    public void imageDataUpdated(ImageData data) {}
+
+    @Override
+    public void selectedImageChanged(ImageData data) {
+        updateEnabledState();
+    }
+}
Index: plugins/photoadjust/src/org/openstreetmap/josm/plugins/photoadjust/PhotoAdjustPlugin.java
===================================================================
--- plugins/photoadjust/src/org/openstreetmap/josm/plugins/photoadjust/PhotoAdjustPlugin.java	(revision 35109)
+++ plugins/photoadjust/src/org/openstreetmap/josm/plugins/photoadjust/PhotoAdjustPlugin.java	(working copy)
@@ -8,6 +8,7 @@
 import java.util.List;
 
 import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.MainMenu;
 import org.openstreetmap.josm.gui.MapFrame;
 import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
@@ -37,6 +38,8 @@
         super(info);
         GeoImageLayer.registerMenuAddition(new UntaggedGeoImageLayerAction());
         PhotoPropertyEditor.init();
+        MainMenu menu = MainApplication.getMenu();
+        MainMenu.add(menu.toolsMenu, new InterpolateImages());
         initAdapters();
     }
 
Index: plugins/photoadjust/test/unit/org/openstreetmap/josm/plugins/photoadjust/InterpolateImagesTest.java
===================================================================
--- plugins/photoadjust/test/unit/org/openstreetmap/josm/plugins/photoadjust/InterpolateImagesTest.java	(nonexistent)
+++ plugins/photoadjust/test/unit/org/openstreetmap/josm/plugins/photoadjust/InterpolateImagesTest.java	(working copy)
@@ -0,0 +1,59 @@
+package org.openstreetmap.josm.plugins.photoadjust;
+
+import java.awt.geom.Point2D;
+import java.io.File;
+import java.util.ArrayList;
+
+import org.junit.Test;
+import org.openstreetmap.josm.data.ImageData;
+import org.openstreetmap.josm.data.coor.CachedLatLon;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.layer.geoimage.ImageEntry;
+
+import mockit.Delegate;
+import mockit.Expectations;
+import mockit.Mocked;
+
+public class InterpolateImagesTest {
+
+    @Test
+    public void testInterpolate(@Mocked(stubOutClassInitialization=true) MapView view) {
+        InterpolateImages interpolate = new InterpolateImages();
+        ArrayList<ImageEntry> list = new ArrayList<>();
+        ImageEntry first = new ImageEntry(new File("test1"));
+        ImageEntry last = new ImageEntry(new File("test3"));
+        ImageEntry middle = new ImageEntry(new File("test2"));
+        list.add(first);
+        list.add(middle);
+        list.add(last);
+
+        ImageData data = new ImageData(list);
+        new Expectations(first) {{
+            first.getPos(); result = new CachedLatLon(0, 0);
+        }};
+        new Expectations(last) {{
+            last.getPos(); result = new CachedLatLon(1, 1);
+        }};
+        new Expectations(middle) {{
+            middle.setPos(new LatLon(0, 0));
+            middle.flagNewGpsData();
+        }};
+        new Expectations(view) {{
+            view.getPoint2D((LatLon) any); result = new Delegate() {
+                Point2D aDelegateMethod(LatLon pos) {
+                    return pos.getX() == 0 ? new Point2D.Double(0, 0) : new Point2D.Double(10, 5);
+                }
+            };
+            view.getLatLon(5.0, 2.5); result = new LatLon(0, 0);
+        }};
+        new Expectations(data) {{
+            data.notifyImageUpdate();
+        }};
+
+        data.addImageToSelection(first);
+        data.addImageToSelection(last);
+        interpolate.interpolate(data, view);
+    }
+
+}
