# HG changeset patch
# User adf88@interia.pl
# Date 1499662933 -7200
#      Mon Jul 10 07:02:13 2017 +0200
# Branch trunk
# Node ID 9ace1b5aa5cd556478937964a0d54bdb8bc07005
# Parent  0d78b96563c56d72f798bf6767cf6ca5a2ac00c1
Fix "jumping" effect when scrolling viewport over bottom edge of the map (viewport should just stop scrolling).

diff -r 0d78b96563c5 -r 9ace1b5aa5cd src/landscape.cpp
--- a/src/landscape.cpp	Thu Jun 22 17:29:53 2017 +0000
+++ b/src/landscape.cpp	Mon Jul 10 07:02:13 2017 +0200
@@ -89,6 +89,68 @@
 static SnowLine *_snow_line = NULL;
 
 /**
+ * Map 2D viewport or smallmap coordinate to 3D world or tile coordinate.
+ * Function takes into account height of tiles and optionally foundations.
+ *
+ * @param x X viewport 2D coordinate.
+ * @param y Y viewport 2D coordinate.
+ * @param with_foundations Whether to include foundations of the tile, otherwise tile will be treated like a clear tile.
+ * @param[in,out] clamp_to_map (In) If not NULL and set to \c true, clamp the coordinate outside of the map to the closest tile within the map.
+ *                             (Out) Whether clamping occured.
+ * @return 3D world coordinate of map point that is visible at the given screen coordinate (3D perspective).
+ *
+ * @note Inverse of #RemapCoords2 function. Smaller values may get rounded.
+ * @see InverseRemapCoords
+ */
+Point InverseRemapCoords2(int x, int y, bool with_foundations, bool *clamp_to_map)
+{
+	const bool want_clamp = (clamp_to_map != NULL) && *clamp_to_map;
+	if (want_clamp) *clamp_to_map = false; // not clamping (yet)
+
+	/* Initial x/y world coordinate is like if the landscape
+	 * was completely flat on height 0. */
+	Point pt = InverseRemapCoords(x, y);
+
+	const uint max_x = MapMaxX() * TILE_SIZE - 1;
+	const uint max_y = MapMaxY() * TILE_SIZE - 1;
+
+	if (want_clamp) {
+		/* Bring the coordinates near to a valid range. This is mostly due to the
+		 * tiles on the north side of the map possibly being drawn too high due to
+		 * the extra height levels. So at the top we allow a number of extra tiles.
+		 * This number is based on the tile height and pixels. */
+		int extra_tiles = CeilDiv(_settings_game.construction.max_heightlevel * TILE_HEIGHT, TILE_PIXELS);
+		Point orig_pt = pt;
+		pt.x = Clamp(pt.x, -extra_tiles * TILE_SIZE, max_x);
+		pt.y = Clamp(pt.y, -extra_tiles * TILE_SIZE, max_y);
+		*clamp_to_map = (pt.x != orig_pt.x) || (pt.y != orig_pt.y);
+	}
+
+	/* Now find the Z-world coordinate by fix point iteration.
+	 * This is a bit tricky because the tile height is non-continuous at foundations.
+	 * The clicked point should be approached from the back, otherwise there are regions that are not clickable.
+	 * (FOUNDATION_HALFTILE_LOWER on SLOPE_STEEP_S hides north halftile completely)
+	 * So give it a z-malus of 4 in the first iterations. */
+	int z = 0;
+	const int min_coord = _settings_game.construction.freeform_edges ? TILE_SIZE : 0;
+
+	for (int i = 0; i < 5; i++) z = GetSlopePixelZ(Clamp(pt.x + max(z, 4) - 4, min_coord, max_x), Clamp(pt.y + max(z, 4) - 4, min_coord, max_y), with_foundations) / 2;
+	for (int m = 3; m > 0; m--) z = GetSlopePixelZ(Clamp(pt.x + max(z, m) - m, min_coord, max_x), Clamp(pt.y + max(z, m) - m, min_coord, max_y), with_foundations) / 2;
+	for (int i = 0; i < 5; i++) z = GetSlopePixelZ(Clamp(pt.x + z,             min_coord, max_x), Clamp(pt.y + z,             min_coord, max_y), with_foundations) / 2;
+
+	pt.x += z;
+	pt.y += z;
+	if (want_clamp) {
+		Point orig_pt = pt;
+		pt.x = Clamp(pt.x, min_coord, max_x);
+		pt.y = Clamp(pt.y, min_coord, max_y);
+		*clamp_to_map = *clamp_to_map || (pt.x != orig_pt.x) || (pt.y != orig_pt.y);
+	}
+
+	return pt;
+}
+
+/**
  * Applies a foundation to a slope.
  *
  * @pre      Foundation and slope must be valid combined.
@@ -276,11 +338,11 @@
 	return z;
 }
 
-int GetSlopePixelZ(int x, int y)
+int GetSlopePixelZ(int x, int y, bool with_foundations)
 {
 	TileIndex tile = TileVirtXY(x, y);
 
-	return _tile_type_procs[GetTileType(tile)]->get_slope_z_proc(tile, x, y);
+	return _tile_type_procs[with_foundations ? GetTileType(tile) : MP_CLEAR]->get_slope_z_proc(tile, x, y);
 }
 
 /**
diff -r 0d78b96563c5 -r 9ace1b5aa5cd src/landscape.h
--- a/src/landscape.h	Thu Jun 22 17:29:53 2017 +0000
+++ b/src/landscape.h	Mon Jul 10 07:02:13 2017 +0200
@@ -39,7 +39,7 @@
 Slope GetFoundationSlope(TileIndex tile, int *z = NULL);
 
 uint GetPartialPixelZ(int x, int y, Slope corners);
-int GetSlopePixelZ(int x, int y);
+int GetSlopePixelZ(int x, int y, bool with_foundations = true);
 void GetSlopePixelZOnEdge(Slope tileh, DiagDirection edge, int *z1, int *z2);
 
 /**
@@ -108,6 +108,7 @@
  * @param y Y coordinate of the 2D coordinate.
  * @return X and Y components of equivalent world or tile coordinate.
  * @note Inverse of #RemapCoords function. Smaller values may get rounded.
+ * @see InverseRemapCoords2
  */
 static inline Point InverseRemapCoords(int x, int y)
 {
@@ -115,6 +116,8 @@
 	return pt;
 }
 
+Point InverseRemapCoords2(int x, int y, bool with_foundations = false, bool *clamp_to_map = NULL);
+
 uint ApplyFoundationToSlope(Foundation f, Slope *s);
 /**
  * Applies a foundation to a slope.
diff -r 0d78b96563c5 -r 9ace1b5aa5cd src/smallmap_gui.cpp
--- a/src/smallmap_gui.cpp	Thu Jun 22 17:29:53 2017 +0000
+++ b/src/smallmap_gui.cpp	Mon Jul 10 07:02:13 2017 +0200
@@ -916,8 +916,8 @@
 	/* Find main viewport. */
 	const ViewPort *vp = FindWindowById(WC_MAIN_WINDOW, 0)->viewport;
 
-	Point upper_left_smallmap_coord  = TranslateXYToTileCoord(vp, vp->left, vp->top, false);
-	Point lower_right_smallmap_coord = TranslateXYToTileCoord(vp, vp->left + vp->width - 1, vp->top + vp->height - 1, false);
+	Point upper_left_smallmap_coord  = InverseRemapCoords2(vp->virtual_left, vp->virtual_top);
+	Point lower_right_smallmap_coord = InverseRemapCoords2(vp->virtual_left + vp->virtual_width - 1, vp->virtual_top + vp->virtual_height - 1);
 
 	Point upper_left = this->RemapTile(upper_left_smallmap_coord.x / (int)TILE_SIZE, upper_left_smallmap_coord.y / (int)TILE_SIZE);
 	upper_left.x -= this->subscroll;
@@ -1629,7 +1629,7 @@
 void SmallMapWindow::SmallMapCenterOnCurrentPos()
 {
 	const ViewPort *vp = FindWindowById(WC_MAIN_WINDOW, 0)->viewport;
-	Point viewport_center = TranslateXYToTileCoord(vp, vp->left + vp->width / 2, vp->top + vp->height / 2);
+	Point viewport_center = InverseRemapCoords2(vp->virtual_left + vp->virtual_width / 2, vp->virtual_top + vp->virtual_height / 2);
 
 	int sub;
 	const NWidgetBase *wid = this->GetWidget<NWidgetBase>(WID_SM_MAP);
diff -r 0d78b96563c5 -r 9ace1b5aa5cd src/viewport.cpp
--- a/src/viewport.cpp	Thu Jun 22 17:29:53 2017 +0000
+++ b/src/viewport.cpp	Mon Jul 10 07:02:13 2017 +0200
@@ -395,65 +395,27 @@
 }
 
 /**
- * Translate screen coordinate in a viewport to a tile coordinate
+ * Translate global screen coordinate to underlying tile coordinate in a viewport.
+ *
+ * Returns exact point of the map that is visible in the given place
+ * of the viewport (3D perspective), height of tiles and foundations matter.
+ *
  * @param vp  Viewport that contains the (\a x, \a y) screen coordinate
- * @param x   Screen x coordinate
- * @param y   Screen y coordinate
- * @param clamp_to_map Clamp the coordinate outside of the map to the closest tile within the map.
- * @return Tile coordinate
+ * @param x   Screen x coordinate, distance in pixels from the left edge of the screen
+ * @param y   Screen y coordinate, distance in pixels from the top edge of the screen
+ * @param clamp_to_map Clamp the coordinate outside of the map to the closest tile within the map
+ * @return Tile coordinate or (-1, -1) if given x or y is not within viewport frame
  */
 Point TranslateXYToTileCoord(const ViewPort *vp, int x, int y, bool clamp_to_map)
 {
-	Point pt;
-	int a, b;
-	int z;
-
-	if ( (uint)(x -= vp->left) >= (uint)vp->width ||
-				(uint)(y -= vp->top) >= (uint)vp->height) {
-				Point pt = {-1, -1};
-				return pt;
-	}
-
-	x = (ScaleByZoom(x, vp->zoom) + vp->virtual_left) >> (2 + ZOOM_LVL_SHIFT);
-	y = (ScaleByZoom(y, vp->zoom) + vp->virtual_top) >> (1 + ZOOM_LVL_SHIFT);
-
-	a = y - x;
-	b = y + x;
-
-	if (clamp_to_map) {
-		/* Bring the coordinates near to a valid range. This is mostly due to the
-		 * tiles on the north side of the map possibly being drawn too high due to
-		 * the extra height levels. So at the top we allow a number of extra tiles.
-		 * This number is based on the tile height and pixels. */
-		int extra_tiles = CeilDiv(_settings_game.construction.max_heightlevel * TILE_HEIGHT, TILE_PIXELS);
-		a = Clamp(a, -extra_tiles * TILE_SIZE, MapMaxX() * TILE_SIZE - 1);
-		b = Clamp(b, -extra_tiles * TILE_SIZE, MapMaxY() * TILE_SIZE - 1);
+	if (!IsInsideBS(x, vp->left, vp->width) || !IsInsideBS(y, vp->top, vp->height)) {
+		Point pt = { -1, -1 };
+		return pt;
 	}
 
-	/* (a, b) is the X/Y-world coordinate that belongs to (x,y) if the landscape would be completely flat on height 0.
-	 * Now find the Z-world coordinate by fix point iteration.
-	 * This is a bit tricky because the tile height is non-continuous at foundations.
-	 * The clicked point should be approached from the back, otherwise there are regions that are not clickable.
-	 * (FOUNDATION_HALFTILE_LOWER on SLOPE_STEEP_S hides north halftile completely)
-	 * So give it a z-malus of 4 in the first iterations.
-	 */
-	z = 0;
-
-	int min_coord = _settings_game.construction.freeform_edges ? TILE_SIZE : 0;
-
-	for (int i = 0; i < 5; i++) z = GetSlopePixelZ(Clamp(a + max(z, 4) - 4, min_coord, MapMaxX() * TILE_SIZE - 1), Clamp(b + max(z, 4) - 4, min_coord, MapMaxY() * TILE_SIZE - 1)) / 2;
-	for (int malus = 3; malus > 0; malus--) z = GetSlopePixelZ(Clamp(a + max(z, malus) - malus, min_coord, MapMaxX() * TILE_SIZE - 1), Clamp(b + max(z, malus) - malus, min_coord, MapMaxY() * TILE_SIZE - 1)) / 2;
-	for (int i = 0; i < 5; i++) z = GetSlopePixelZ(Clamp(a + z, min_coord, MapMaxX() * TILE_SIZE - 1), Clamp(b + z, min_coord, MapMaxY() * TILE_SIZE - 1)) / 2;
-
-	if (clamp_to_map) {
-		pt.x = Clamp(a + z, min_coord, MapMaxX() * TILE_SIZE - 1);
-		pt.y = Clamp(b + z, min_coord, MapMaxY() * TILE_SIZE - 1);
-	} else {
-		pt.x = a + z;
-		pt.y = b + z;
-	}
-
-	return pt;
+	return InverseRemapCoords2(
+			ScaleByZoom(x - vp->left, vp->zoom) + vp->virtual_left,
+			ScaleByZoom(y - vp->top, vp->zoom) + vp->virtual_top, true, &clamp_to_map);
 }
 
 /* When used for zooming, check area below current coordinates (x,y)
@@ -1086,6 +1048,12 @@
 static int GetViewportY(Point tile)
 {
 	/* Each increment in X or Y direction moves down by half a tile, i.e. TILE_PIXELS / 2. */
+	/* XXX: Seems invalid. Why divide by 2?
+	 *      Each such half of a tile is TILE_PIXELS << ZOOM_LVL_SHIFT units high,
+	 *      entire tile is as twice as high. Each single increment in tile X or Y
+	 *      moves by TILE_PIXELS << ZOOM_LVL_SHIFT, not by half of this distance.
+	 *      This differs from what RemapCoords2 would return for the given tile,
+	 *      proportions seems disturbed here. */
 	return (tile.y * (int)(TILE_PIXELS / 2) + tile.x * (int)(TILE_PIXELS / 2) - TilePixelHeightOutsideMap(tile.x, tile.y)) << ZOOM_LVL_SHIFT;
 }
 
@@ -1663,121 +1631,34 @@
 }
 
 /**
- * Continue criteria for the SearchMapEdge function.
- * @param iter       Value to check.
- * @param iter_limit Maximum value for the iter
- * @param sy         Screen y coordinate calculated for the tile at hand
- * @param sy_limit   Limit to the screen y coordinate
- * @return True when we should continue searching.
+ * Ensure that a given viewport has valid scroll position within the map.
+ *
+ * There must be a visible piece of map in the center of the viewport.
+ * If there isn't, the viewport will be scrolled to nearest such location.
+ *
+ * @param vp The viewport.
+ * @param[in,out] scroll_x Viewport X scroll.
+ * @param[in,out] scroll_x Viewport Y scroll.
  */
-typedef bool ContinueMapEdgeSearch(int iter, int iter_limit, int sy, int sy_limit);
-
-/** Continue criteria for searching a no-longer-visible tile in negative direction, starting at some tile. */
-static inline bool ContinueLowerMapEdgeSearch(int iter, int iter_limit, int sy, int sy_limit) { return iter > 0          && sy > sy_limit; }
-/** Continue criteria for searching a no-longer-visible tile in positive direction, starting at some tile. */
-static inline bool ContinueUpperMapEdgeSearch(int iter, int iter_limit, int sy, int sy_limit) { return iter < iter_limit && sy < sy_limit; }
-
-/**
- * Searches, starting at the given tile, by applying the given offset to iter, for a no longer visible tile.
- * The whole sense of this function is keeping the to-be-written code small, thus it is a little bit abstracted
- * so the same function can be used for both the X and Y locations. As such a reference to one of the elements
- * in curr_tile was needed.
- * @param curr_tile  A tile
- * @param iter       Reference to either the X or Y of curr_tile.
- * @param iter_limit Upper search limit for the iter value.
- * @param offset     Search in steps of this size
- * @param sy_limit   Search limit to be passed to the criteria
- * @param continue_criteria Search as long as this criteria is true
- * @return The final value of iter.
- */
-static int SearchMapEdge(Point &curr_tile, int &iter, int iter_limit, int offset, int sy_limit, ContinueMapEdgeSearch continue_criteria)
-{
-	int sy;
-	do {
-		iter = Clamp(iter + offset, 0, iter_limit);
-		sy = GetViewportY(curr_tile);
-	} while (continue_criteria(iter, iter_limit, sy, sy_limit));
-
-	return iter;
-}
-
-/**
- * Determine the clamping of either the X or Y coordinate to the map.
- * @param curr_tile   A tile
- * @param iter        Reference to either the X or Y of curr_tile.
- * @param iter_limit  Upper search limit for the iter value.
- * @param start       Start value for the iteration.
- * @param other_ref   Reference to the opposite axis in curr_tile than of iter.
- * @param other_value Start value for of the opposite axis
- * @param vp_value    Value of the viewport location in the opposite axis as for iter.
- * @param other_limit Limit for the other value, so if iter is X, then other_limit is for Y.
- * @param vp_top      Top of the viewport.
- * @param vp_bottom   Bottom of the viewport.
- * @return Clamped version of vp_value.
- */
-static inline int ClampXYToMap(Point &curr_tile, int &iter, int iter_limit, int start, int &other_ref, int other_value, int vp_value, int other_limit, int vp_top, int vp_bottom)
+static inline void ClampViewportToMap(const ViewPort *vp, int *scroll_x, int *scroll_y)
 {
-	bool upper_edge = other_value < _settings_game.construction.max_heightlevel / 4;
-
-	/*
-	 * First get an estimate of the tiles relevant for us at that edge.  Relevant in the sense
-	 * "at least close to the visible area". Thus, we don't look at exactly each tile, inspecting
-	 * e.g. every tenth should be enough. After all, the desired screen limit is set such that
-	 * the bordermost tiles are painted in the middle of the screen when one hits the limit,
-	 * i.e. it is no harm if there is some small error in that calculation
-	 */
-
-	other_ref = upper_edge ? 0 : other_limit;
-	iter = start;
-	int min_iter = SearchMapEdge(curr_tile, iter, iter_limit, upper_edge ? -10 : +10, vp_top,    upper_edge ? ContinueLowerMapEdgeSearch : ContinueUpperMapEdgeSearch);
-	iter = start;
-	int max_iter = SearchMapEdge(curr_tile, iter, iter_limit, upper_edge ? +10 : -10, vp_bottom, upper_edge ? ContinueUpperMapEdgeSearch : ContinueLowerMapEdgeSearch);
-
-	max_iter = min(max_iter + _settings_game.construction.max_heightlevel / 4, iter_limit);
-	min_iter = min(min_iter, max_iter);
-
-	/* Now, calculate the highest heightlevel of these tiles. Again just as an estimate. */
-	int max_heightlevel_at_edge = 0;
-	for (iter = min_iter; iter <= max_iter; iter += 10) {
-		max_heightlevel_at_edge = max(max_heightlevel_at_edge, (int)TileHeight(TileXY(curr_tile.x, curr_tile.y)));
+	/* Centre of the viewport is hot spot. */
+	Point pt = {
+		*scroll_x + vp->virtual_width / 2,
+		*scroll_y + vp->virtual_height / 2
+	};
+
+	/* Find nearrest tile that is within map borders. */
+	bool clamp = true;
+	pt = InverseRemapCoords2(pt.x, pt.y, false, &clamp);
+
+	/* Were the coordinates clamped? */
+	if (clamp) {
+		/* Convert back to viewport coordinates and remove centering. */
+		pt = RemapCoords2(pt.x, pt.y);
+		*scroll_x = pt.x - vp->virtual_width / 2;
+		*scroll_y = pt.y - vp->virtual_height / 2;
 	}
-
-	/* Based on that heightlevel, calculate the limit. For the upper edge a tile with height zero would
-	 * get a limit of zero, on the other side it depends on the number of tiles along the axis. */
-	return upper_edge ?
-			max(vp_value, -max_heightlevel_at_edge * (int)(TILE_HEIGHT * 2 * ZOOM_LVL_BASE)) :
-			min(vp_value, (other_limit * TILE_SIZE * 4 - max_heightlevel_at_edge * TILE_HEIGHT * 2) * ZOOM_LVL_BASE);
-}
-
-static inline void ClampViewportToMap(const ViewPort *vp, int &x, int &y)
-{
-	int original_y = y;
-
-	/* Centre of the viewport is hot spot */
-	x += vp->virtual_width / 2;
-	y += vp->virtual_height / 2;
-
-	/* Convert viewport coordinates to map coordinates
-	 * Calculation is scaled by 4 to avoid rounding errors */
-	int vx = -x + y * 2;
-	int vy =  x + y * 2;
-
-	/* Find out which tile corresponds to (vx,vy) if one assumes height zero.  The cast is necessary to prevent C++ from
-	 * converting the result to an uint, which gives an overflow instead of a negative result... */
-	int tx = vx / (int)(TILE_SIZE * 4 * ZOOM_LVL_BASE);
-	int ty = vy / (int)(TILE_SIZE * 4 * ZOOM_LVL_BASE);
-
-	Point curr_tile;
-	vx = ClampXYToMap(curr_tile, curr_tile.y, MapMaxY(), ty, curr_tile.x, tx, vx, MapMaxX(), original_y, original_y + vp->virtual_height);
-	vy = ClampXYToMap(curr_tile, curr_tile.x, MapMaxX(), tx, curr_tile.y, ty, vy, MapMaxY(), original_y, original_y + vp->virtual_height);
-
-	/* Convert map coordinates to viewport coordinates */
-	x = (-vx + vy) / 2;
-	y = ( vx + vy) / 4;
-
-	/* Remove centering */
-	x -= vp->virtual_width / 2;
-	y -= vp->virtual_height / 2;
 }
 
 /**
@@ -1797,7 +1678,7 @@
 		SetViewportPosition(w, pt.x, pt.y);
 	} else {
 		/* Ensure the destination location is within the map */
-		ClampViewportToMap(vp, w->viewport->dest_scrollpos_x, w->viewport->dest_scrollpos_y);
+		ClampViewportToMap(vp, &w->viewport->dest_scrollpos_x, &w->viewport->dest_scrollpos_y);
 
 		int delta_x = w->viewport->dest_scrollpos_x - w->viewport->scrollpos_x;
 		int delta_y = w->viewport->dest_scrollpos_y - w->viewport->scrollpos_y;
@@ -1817,7 +1698,7 @@
 								w->viewport->scrollpos_y == w->viewport->dest_scrollpos_y);
 		}
 
-		ClampViewportToMap(vp, w->viewport->scrollpos_x, w->viewport->scrollpos_y);
+		ClampViewportToMap(vp, &w->viewport->scrollpos_x, &w->viewport->scrollpos_y);
 
 		SetViewportPosition(w, w->viewport->scrollpos_x, w->viewport->scrollpos_y);
 		if (update_overlay) RebuildViewportOverlay(w);
