Index: src/org/openstreetmap/josm/gui/NavigatableComponent.java
===================================================================
--- src/org/openstreetmap/josm/gui/NavigatableComponent.java	(revision 12147)
+++ src/org/openstreetmap/josm/gui/NavigatableComponent.java	(working copy)
@@ -169,6 +169,13 @@
      * The current state (scale, center, ...) of this map view.
      */
     private transient MapViewState state;
+    /**
+     * A second center value that is carried along in addition to <code>state.getCenter()</code>.
+     * It is the center before alignment to pixel grid. The alignment is normally
+     * very small (0.5 pixel), but in certain workflows it can become noticeable (see #14787).
+     * To amend this, zoom changes, like zooming in or out are based on the unaligned <code>centerCarry</code>.
+     */
+    private EastNorth centerCarry;
 
     /**
      * Main uses weak link to store this, so we need to keep a reference.
@@ -181,6 +188,7 @@
     public NavigatableComponent() {
         setLayout(null);
         state = MapViewState.createDefaultState(getWidth(), getHeight());
+        centerCarry = state.getCenter().getEastNorth();
         Main.addProjectionChangeListener(projectionChangeListener);
     }
 
@@ -301,7 +309,7 @@
      * Zoom in current view. Use configured zoom step and scaling settings.
      */
     public void zoomIn() {
-        zoomTo(state.getCenter().getEastNorth(), scaleZoomIn());
+        zoomTo(centerCarry, scaleZoomIn());
     }
 
     /**
@@ -308,7 +316,7 @@
      * Zoom out current view. Use configured zoom step and scaling settings.
      */
     public void zoomOut() {
-        zoomTo(state.getCenter().getEastNorth(), scaleZoomOut());
+        zoomTo(centerCarry, scaleZoomOut());
     }
 
     protected void updateLocationState() {
@@ -623,19 +631,30 @@
         // snap scale to imagery if needed
         newScale = scaleRound(newScale);
 
-        // Align to the pixel grid:
-        // This is a sub-pixel correction to ensure consistent drawing at a certain scale.
-        // For example take 2 nodes, that have a distance of exactly 2.6 pixels.
-        // Depending on the offset, the distance in rounded or truncated integer
-        // pixels will be 2 or 3. It is preferable to have a consistent distance
-        // and not switch back and forth as the viewport moves. This can be achieved by
-        // locking an arbitrary point to integer pixel coordinates. (Here the EastNorth
-        // origin is used as reference point.)
-        // Note that the normal right mouse button drag moves the map by integer pixel
-        // values, so it is not an issue in this case. It only shows when zooming
-        // in & back out, etc.
-        MapViewState mvs = getState().usingScale(newScale);
-        mvs = mvs.movedTo(mvs.getCenter(), newCenter);
+        EastNorth centerAligned = alignToPixelGrid(newCenter, newScale);
+
+        if (!centerAligned.equals(getCenter()) || !Utils.equalsEpsilon(getScale(), newScale)) {
+            if (!initial) {
+                pushZoomUndo(centerCarry, getScale());
+            }
+            zoomNoUndoTo(newCenter, centerAligned, newScale, initial);
+        }
+    }
+
+    // Align to the pixel grid:
+    // This is a sub-pixel correction to ensure consistent drawing at a certain scale.
+    // For example take 2 nodes, that have a distance of exactly 2.6 pixels.
+    // Depending on the offset, the distance in rounded or truncated integer
+    // pixels will be 2 or 3. It is preferable to have a consistent distance
+    // and not switch back and forth as the viewport moves. This can be achieved by
+    // locking an arbitrary point to integer pixel coordinates. (Here the EastNorth
+    // origin is used as reference point.)
+    // Note that the normal right mouse button drag moves the map by integer pixel
+    // values, so it is not an issue in this case. It only shows when zooming
+    // in & back out, etc.
+    private EastNorth alignToPixelGrid(EastNorth center, double scale) {
+        MapViewState mvs = getState().usingScale(scale);
+        mvs = mvs.movedTo(mvs.getCenter(), center);
         Point2D enOrigin = mvs.getPointFor(new EastNorth(0, 0)).getInView();
         // as a result of the alignment, it is common to round "half integer" values
         // like 1.49999, which is numerically unstable; add small epsilon to resolve this
@@ -644,14 +663,7 @@
                 Math.round(enOrigin.getX()) + epsilon,
                 Math.round(enOrigin.getY()) + epsilon);
         EastNorth enShift = mvs.getForView(enOriginAligned.getX(), enOriginAligned.getY()).getEastNorth();
-        newCenter = newCenter.subtract(enShift);
-
-        if (!newCenter.equals(getCenter()) || !Utils.equalsEpsilon(getScale(), newScale)) {
-            if (!initial) {
-                pushZoomUndo(getCenter(), getScale());
-            }
-            zoomNoUndoTo(newCenter, newScale, initial);
-        }
+        return center.subtract(enShift);
     }
 
     /**
@@ -661,13 +673,14 @@
      * @param newScale The scale to use.
      * @param initial true if this call initializes the viewport.
      */
-    private void zoomNoUndoTo(EastNorth newCenter, double newScale, boolean initial) {
+    private void zoomNoUndoTo(EastNorth newCenterCarry, EastNorth newCenterAligned, double newScale, boolean initial) {
         if (!Utils.equalsEpsilon(getScale(), newScale)) {
             state = state.usingScale(newScale);
         }
-        if (!newCenter.equals(getCenter())) {
-            state = state.movedTo(state.getCenter(), newCenter);
+        if (!newCenterAligned.equals(getCenter())) {
+            state = state.movedTo(state.getCenter(), newCenterAligned);
         }
+        this.centerCarry = newCenterCarry;
         if (!initial) {
             repaint();
             fireZoomChanged();
@@ -850,8 +863,9 @@
     public void zoomPrevious() {
         if (!zoomUndoBuffer.isEmpty()) {
             ZoomData zoom = zoomUndoBuffer.pop();
-            zoomRedoBuffer.push(new ZoomData(getCenter(), getScale()));
-            zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale(), false);
+            zoomRedoBuffer.push(new ZoomData(centerCarry, getScale()));
+            EastNorth centerAligned = alignToPixelGrid(zoom.getCenterEastNorth(), zoom.getScale());
+            zoomNoUndoTo(zoom.getCenterEastNorth(), centerAligned, zoom.getScale(), false);
         }
     }
 
@@ -861,8 +875,9 @@
     public void zoomNext() {
         if (!zoomRedoBuffer.isEmpty()) {
             ZoomData zoom = zoomRedoBuffer.pop();
-            zoomUndoBuffer.push(new ZoomData(getCenter(), getScale()));
-            zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale(), false);
+            zoomUndoBuffer.push(new ZoomData(centerCarry, getScale()));
+            EastNorth centerAligned = alignToPixelGrid(zoom.getCenterEastNorth(), zoom.getScale());
+            zoomNoUndoTo(zoom.getCenterEastNorth(), centerAligned, zoom.getScale(), false);
         }
     }
 
