diff --git a/src/smallmap_gui.cpp b/src/smallmap_gui.cpp index 6bad664..482c950 100644 --- a/src/smallmap_gui.cpp +++ b/src/smallmap_gui.cpp @@ -33,6 +33,8 @@ enum SmallMapWindowWidgets { SM_WIDGET_MAP, SM_WIDGET_LEGEND, SM_WIDGET_BUTTONSPANEL, + SM_WIDGET_ZOOM_IN, + SM_WIDGET_ZOOM_OUT, SM_WIDGET_CONTOUR, SM_WIDGET_VEHICLES, SM_WIDGET_INDUSTRIES, @@ -53,8 +55,10 @@ static const Widget _smallmap_widgets[] = { { WWT_STICKYBOX, RESIZE_LR, COLOUR_BROWN, 338, 349, 0, 13, 0x0, STR_TOOLTIP_STICKY}, // SM_WIDGET_STICKYBOX { WWT_PANEL, RESIZE_RB, COLOUR_BROWN, 0, 349, 14, 157, 0x0, STR_NULL}, // SM_WIDGET_MAP_BORDER { WWT_INSET, RESIZE_RB, COLOUR_BROWN, 2, 347, 16, 155, 0x0, STR_NULL}, // SM_WIDGET_MAP -{ WWT_PANEL, RESIZE_RTB, COLOUR_BROWN, 0, 261, 158, 201, 0x0, STR_NULL}, // SM_WIDGET_LEGEND -{ WWT_PANEL, RESIZE_LRTB, COLOUR_BROWN, 262, 349, 158, 158, 0x0, STR_NULL}, // SM_WIDGET_BUTTONSPANEL +{ WWT_PANEL, RESIZE_RTB, COLOUR_BROWN, 0, 239, 158, 201, 0x0, STR_NULL}, // SM_WIDGET_LEGEND +{ WWT_PANEL, RESIZE_LRTB, COLOUR_BROWN, 240, 349, 158, 158, 0x0, STR_NULL}, // SM_WIDGET_BUTTONSPANEL +{ WWT_IMGBTN, RESIZE_LRTB, COLOUR_BROWN, 240, 261, 158, 179, SPR_IMG_ZOOMIN, STR_TOOLBAR_TOOLTIP_ZOOM_THE_VIEW_IN}, // SM_WIDGET_ZOOM_IN +{ WWT_IMGBTN, RESIZE_LRTB, COLOUR_BROWN, 240, 261, 180, 201, SPR_IMG_ZOOMOUT, STR_TOOLBAR_TOOLTIP_ZOOM_THE_VIEW_OUT}, // SM_WIDGET_ZOOM_OUT { WWT_IMGBTN, RESIZE_LRTB, COLOUR_BROWN, 284, 305, 158, 179, SPR_IMG_SHOW_COUNTOURS, STR_SMALLMAP_TOOLTIP_SHOW_LAND_CONTOURS_ON_MAP}, // SM_WIDGET_CONTOUR { WWT_IMGBTN, RESIZE_LRTB, COLOUR_BROWN, 306, 327, 158, 179, SPR_IMG_SHOW_VEHICLES, STR_SMALLMAP_TOOLTIP_SHOW_VEHICLES_ON_MAP}, // SM_WIDGET_VEHICLES { WWT_IMGBTN, RESIZE_LRTB, COLOUR_BROWN, 328, 349, 158, 179, SPR_IMG_INDUSTRY, STR_SMALLMAP_TOOLTIP_SHOW_INDUSTRIES_ON_MAP}, // SM_WIDGET_INDUSTRIES @@ -86,11 +90,13 @@ static const NWidgetPart _nested_smallmap_widgets[] = { EndContainer(), /* Panel. */ NWidget(NWID_HORIZONTAL), - NWidget(WWT_PANEL, COLOUR_BROWN, SM_WIDGET_LEGEND), SetMinimalSize(262, 44), SetResize(1, 0), EndContainer(), + NWidget(WWT_PANEL, COLOUR_BROWN, SM_WIDGET_LEGEND), SetMinimalSize(240, 44), SetResize(1, 0), EndContainer(), NWidget(NWID_LAYERED), NWidget(NWID_VERTICAL), /* Top button row. */ NWidget(NWID_HORIZONTAL), + NWidget(WWT_IMGBTN, COLOUR_BROWN, SM_WIDGET_ZOOM_IN), SetMinimalSize(22, 22), + SetDataTip(SPR_IMG_ZOOMIN, STR_TOOLBAR_TOOLTIP_ZOOM_THE_VIEW_IN), NWidget(WWT_IMGBTN, COLOUR_BROWN, SM_WIDGET_CENTERMAP), SetMinimalSize(22, 22), SetDataTip(SPR_IMG_SMALLMAP, STR_SMALLMAP_CENTER), NWidget(WWT_IMGBTN, COLOUR_BROWN, SM_WIDGET_CONTOUR), SetMinimalSize(22, 22), @@ -102,6 +108,8 @@ static const NWidgetPart _nested_smallmap_widgets[] = { EndContainer(), /* Bottom button row. */ NWidget(NWID_HORIZONTAL), + NWidget(WWT_IMGBTN, COLOUR_BROWN, SM_WIDGET_ZOOM_OUT), SetMinimalSize(22, 22), + SetDataTip(SPR_IMG_ZOOMOUT, STR_TOOLBAR_TOOLTIP_ZOOM_THE_VIEW_OUT), NWidget(WWT_IMGBTN, COLOUR_BROWN, SM_WIDGET_TOGGLETOWNNAME), SetMinimalSize(22, 22), SetDataTip(SPR_IMG_TOWN, STR_SMALLMAP_TOOLTIP_TOGGLE_TOWN_NAMES_ON_OFF), NWidget(WWT_IMGBTN, COLOUR_BROWN, SM_WIDGET_ROUTES), SetMinimalSize(22, 22), @@ -113,7 +121,7 @@ static const NWidgetPart _nested_smallmap_widgets[] = { EndContainer(), EndContainer(), NWidget(NWID_VERTICAL), - NWidget(WWT_PANEL, COLOUR_BROWN, SM_WIDGET_BUTTONSPANEL), SetMinimalSize(88, 1), SetFill(0, 0), EndContainer(), + NWidget(WWT_PANEL, COLOUR_BROWN, SM_WIDGET_BUTTONSPANEL), SetMinimalSize(110, 1), SetFill(0, 0), EndContainer(), NWidget(NWID_SPACER), SetFill(0, 1), EndContainer(), EndContainer(), @@ -497,19 +505,6 @@ static inline uint32 GetSmallMapOwnerPixels(TileIndex tile) return _owner_colours[o]; } - -static const uint32 _smallmap_mask_left[3] = { - MKCOLOUR(0xFF000000), - MKCOLOUR(0xFFFF0000), - MKCOLOUR(0xFFFFFF00), -}; - -static const uint32 _smallmap_mask_right[] = { - MKCOLOUR(0x000000FF), - MKCOLOUR(0x0000FFFF), - MKCOLOUR(0x00FFFFFF), -}; - /* each tile has 4 x pixels and 1 y pixel */ static GetSmallMapPixels *_smallmap_draw_procs[] = { @@ -549,84 +544,209 @@ class SmallMapWindow : public Window SMT_OWNER, }; + /** minimum number of rows in the legend */ + static const int LEGEND_MIN_ROWS = 7; + + enum SmallmapWindowDistances { + SD_MAP_EXTRA_PADDING = 2, ///< size of borders of the smallmap + SD_MAP_COLUMN_WIDTH = 4, + SD_MAP_ROW_OFFSET = 2, + SD_MAP_MIN_INDUSTRY_WIDTH = 3, + SD_LEGEND_COLUMN_WIDTH = 119, + SD_LEGEND_ROW_HEIGHT = 6, + SD_LEGEND_MIN_HEIGHT = SD_LEGEND_ROW_HEIGHT * LEGEND_MIN_ROWS, + }; + static SmallMapType map_type; static bool show_towns; int32 scroll_x; int32 scroll_y; - int32 subscroll; + uint8 refresh; + static const int FORCE_REFRESH = 0x1F; - static const int COLUMN_WIDTH = 119; - static const int MIN_LEGEND_HEIGHT = 6 * 7; + /** + * zoom level of the smallmap. + * May be something between ZOOM_LVL_NORMAL and ZOOM_LVL_MAX. + */ + ZoomLevel zoom; + + /* The order of calculations when remapping is _very_ important as it introduces rounding errors. + * Everything has to be done just like when drawing the background otherwise the rounding errors are + * different on the background and on the overlay which creates "jumping" behaviour. This means: + * 1. UnScaleByZoom + * 2. divide by TILE_SIZE + * 3. subtract or add things or RemapCoords + * Note: + * We can't divide scroll_{x|y} by TILE_SIZE before scaling as that would mean we can only scroll full tiles. + */ /** - * Remap a map's tile X coordinate (TileX(TileIndex)) to - * a location on this smallmap. - * @param tile_x the tile's X coordinate. + * remap coordinates on the main map into coordinates on the smallmap + * @param pos_x X position on the main map + * @param pos_y Y position on the main map + * @return Point in the smallmap + */ + inline Point RemapPlainCoords(int pos_x, int pos_y) + { + return RemapCoords( + RemapX(pos_x), + RemapY(pos_y), + 0 + ); + } + + /** + * remap a tile coordinate into coordinates on the smallmap + * @param tile the tile to be remapped + * @return Point with coordinates of the tile's upper left corner in the smallmap + */ + inline Point RemapTileCoords(TileIndex tile) + { + return RemapPlainCoords(TileX(tile) * TILE_SIZE, TileY(tile) * TILE_SIZE); + } + + /** + * scale a coordinate from the main map into the smallmap dimension + * @param pos coordinate to be scaled + * @return scaled coordinate + */ + inline int UnScalePlainCoord(int pos) + { + return UnScaleByZoomLower(pos, this->zoom) / TILE_SIZE; + } + + /** + * Remap a map X coordinate to a location on this smallmap. + * @param pos_x the tile's X coordinate. * @return the X coordinate to draw on. */ - inline int RemapX(int tile_x) + inline int RemapX(int pos_x) { - return tile_x - this->scroll_x / TILE_SIZE; + return UnScalePlainCoord(pos_x) - UnScalePlainCoord(this->scroll_x); } /** - * Remap a map's tile Y coordinate (TileY(TileIndex)) to - * a location on this smallmap. - * @param tile_y the tile's Y coordinate. + * Remap a map Y coordinate to a location on this smallmap. + * @param pos_y the tile's Y coordinate. * @return the Y coordinate to draw on. */ - inline int RemapY(int tile_y) + inline int RemapY(int pos_y) { - return tile_y - this->scroll_y / TILE_SIZE; + return UnScalePlainCoord(pos_y) - UnScalePlainCoord(this->scroll_y); } /** - * Draws one column of the small map in a certain mode onto the screen buffer. This - * function looks exactly the same for all types + * Draws at most MAP_COLUMN_WIDTH columns (of one pixel each) of the small map in a certain + * mode onto the screen buffer. This function looks exactly the same for all types. Due to + * the constraints that no less than MAP_COLUMN_WIDTH pixels can be resolved at once via a + * GetSmallMapPixels function and that a single tile may be mapped onto more than one pixel + * in the smallmap dst, xc and yc may point to a place outside the area to be drawn. + * + * col_start, col_end, row_start and row_end give a more precise description of that area which + * is respected when drawing. * * @param dst Pointer to a part of the screen buffer to write to. - * @param xc The X coordinate of the first tile in the column. - * @param yc The Y coordinate of the first tile in the column - * @param pitch Number of pixels to advance in the screen buffer each time a pixel is written. - * @param reps Number of lines to draw - * @param mask ? - * @param proc Pointer to the colour function + * @param xc First unscaled X coordinate of the first tile in the column. + * @param yc First unscaled Y coordinate of the first tile in the column + * @param col_start the first column in the buffer to be actually drawn + * @param col_end the last column to be actually drawn + * @param row_start the first row to be actually drawn + * @param row_end the last row to be actually drawn * @see GetSmallMapPixels(TileIndex) */ - inline void DrawSmallMapStuff(void *dst, uint xc, uint yc, int pitch, int reps, uint32 mask, GetSmallMapPixels *proc) + inline void DrawSmallMapStuff(void *dst, uint xc, uint yc, int col_start, int col_end, int row_start, int row_end) { Blitter *blitter = BlitterFactoryBase::GetCurrentBlitter(); - void *dst_ptr_abs_end = blitter->MoveTo(_screen.dst_ptr, 0, _screen.height); - void *dst_ptr_end = blitter->MoveTo(dst_ptr_abs_end, -4, 0); - - do { - /* check if the tile (xc,yc) is within the map range */ - uint min_xy = _settings_game.construction.freeform_edges ? 1 : 0; - if (IsInsideMM(xc, min_xy, MapMaxX()) && IsInsideMM(yc, min_xy, MapMaxY())) { - /* check if the dst pointer points to a pixel inside the screen buffer */ - if (dst < _screen.dst_ptr) continue; - if (dst >= dst_ptr_abs_end) continue; - - uint32 val = proc(TileXY(xc, yc)) & mask; + GetSmallMapPixels *proc = _smallmap_draw_procs[this->map_type]; + for (int row = 0; row < row_end; row += SD_MAP_ROW_OFFSET) { + if (row >= row_start) { + /* check if the tile (xc,yc) is within the map range */ + uint min_xy = _settings_game.construction.freeform_edges ? 1 : 0; + uint x = ScaleByZoomLower(xc, this->zoom); + uint y = ScaleByZoomLower(yc, this->zoom); + uint32 val = 0; + if (IsInsideMM(x, min_xy, MapMaxX()) && IsInsideMM(y, min_xy, MapMaxY())) { + val = proc(TileXY(x, y)); + } uint8 *val8 = (uint8 *)&val; + for (int i = col_start; i < col_end; ++i ) { + blitter->SetPixel(dst, i, 0, val8[i]); + } + } - if (dst <= dst_ptr_end) { - blitter->SetPixelIfEmpty(dst, 0, 0, val8[0]); - blitter->SetPixelIfEmpty(dst, 1, 0, val8[1]); - blitter->SetPixelIfEmpty(dst, 2, 0, val8[2]); - blitter->SetPixelIfEmpty(dst, 3, 0, val8[3]); - } else { - /* It happens that there are only 1, 2 or 3 pixels left to fill, so in that special case, write till the end of the video-buffer */ - int i = 0; - do { - blitter->SetPixelIfEmpty(dst, 0, 0, val8[i]); - } while (i++, dst = blitter->MoveTo(dst, 1, 0), dst < dst_ptr_abs_end); + /* switch to next row in the column */ + xc++; + yc++; + dst = blitter->MoveTo(dst, 0, SD_MAP_ROW_OFFSET); + } + } + + void DrawVehicles(DrawPixelInfo *dpi) { + if (this->map_type == SMT_CONTOUR || this->map_type == SMT_VEHICLES) { + Vehicle *v; + + FOR_ALL_VEHICLES(v) { + if (v->type != VEH_EFFECT && + (v->vehstatus & (VS_HIDDEN | VS_UNCLICKABLE)) == 0) { + DrawVehicle(dpi, v); + } + } + } + } + + /** + * draws a vehicle in the smallmap if it's in the selected drawing area. + * @param dpi the part of the smallmap to be drawn into + * @param v the vehicle to be drawn + */ + void DrawVehicle(DrawPixelInfo *dpi, Vehicle *v) + { + Blitter *blitter = BlitterFactoryBase::GetCurrentBlitter(); + + /* Remap into flat coordinates. */ + Point pt = RemapTileCoords(v->tile); + + int x = pt.x - dpi->left; + int y = pt.y - dpi->top; + + byte colour = (this->map_type == SMT_VEHICLES) ? _vehicle_type_colours[v->type] : 0xF; + + /* Draw vehicle */ + if (IsInsideMM(y, 0, dpi->height)) { + if (IsInsideMM(x, 0, dpi->width)) { + blitter->SetPixel(dpi->dst_ptr, x, y, colour); + } + if (IsInsideMM(x + 1, 0, dpi->width)) { + blitter->SetPixel(dpi->dst_ptr, x + 1, y, colour); + } + } + } + + void DrawIndustries(DrawPixelInfo *dpi) { + /* Emphasize all industries if current view is zoomed out "Industreis" */ + Blitter *blitter = BlitterFactoryBase::GetCurrentBlitter(); + if ((this->map_type == SMT_INDUSTRY) && (this->zoom > ZOOM_LVL_NORMAL)) { + const Industry *i; + FOR_ALL_INDUSTRIES(i) { + if (_legend_from_industries[_industry_to_list_pos[i->type]].show_on_map) { + Point pt = RemapTileCoords(i->xy); + + int y = pt.y - dpi->top; + if (!IsInsideMM(y, 0, dpi->height)) continue; + + int x = pt.x - dpi->left; + byte colour = GetIndustrySpec(i->type)->map_colour; + + for (int offset = 0; offset < SD_MAP_MIN_INDUSTRY_WIDTH; ++offset) { + if (IsInsideMM(x + offset, 0, dpi->width)) { + blitter->SetPixel(dpi->dst_ptr, x + offset, y, colour); + } + } } } - /* switch to next tile in the column */ - } while (xc++, yc++, dst = blitter->MoveTo(dst, pitch, 0), --reps != 0); + } } public: @@ -652,9 +772,6 @@ public: old_dpi = _cur_dpi; _cur_dpi = dpi; - /* clear it */ - GfxFillRect(dpi->left, dpi->top, dpi->left + dpi->width - 1, dpi->top + dpi->height - 1, 0); - /* setup owner table */ if (this->map_type == SMT_OWNER) { const Company *c; @@ -672,126 +789,88 @@ public: } } - int tile_x = this->scroll_x / TILE_SIZE; - int tile_y = this->scroll_y / TILE_SIZE; + int tile_x = UnScalePlainCoord(this->scroll_x); + int tile_y = UnScalePlainCoord(this->scroll_y); - int dx = dpi->left + this->subscroll; + int dx = dpi->left; tile_x -= dx / 4; tile_y += dx / 4; - dx &= 3; int dy = dpi->top; tile_x += dy / 2; tile_y += dy / 2; + /* prevent some artifacts when partially redrawing. + * I have no idea how this works. + */ + dx &= 3; + dx += 1; if (dy & 1) { tile_x++; dx += 2; - if (dx > 3) { - dx -= 4; - tile_x--; - tile_y++; - } } - void *ptr = blitter->MoveTo(dpi->dst_ptr, -dx - 4, 0); - int x = - dx - 4; + /** + * As we can resolve no less than 4 pixels of the smallmap at once we have to start drawing at an X position <= -4 + * otherwise we get artifacts when partially redrawing. + * Make sure dx provides for that and update tile_x and tile_y accordingly. + */ + while(dx < SD_MAP_COLUMN_WIDTH) { + dx += SD_MAP_COLUMN_WIDTH; + tile_x++; + tile_y--; + } + + /* Beginning at ZOOM_LVL_OUT_2X the background is off by 1 pixel + */ + dy = 0; + if (this->zoom > ZOOM_LVL_NORMAL) { + dy = 1; + } + + /* correct the various problems mentioned above by moving the initial drawing pointer a little */ + void *ptr = blitter->MoveTo(dpi->dst_ptr, -dx, -dy); + int x = -dx; int y = 0; for (;;) { - uint32 mask = 0xFFFFFFFF; - /* distance from left edge */ - if (x >= -3) { - if (x < 0) { - /* mask to use at the left edge */ - mask = _smallmap_mask_left[x + 3]; - } + if (x > -SD_MAP_COLUMN_WIDTH) { /* distance from right edge */ - int t = dpi->width - x; - if (t < 4) { - if (t <= 0) break; // exit loop - /* mask to use at the right edge */ - mask &= _smallmap_mask_right[t - 1]; - } + if (dpi->width - x <= 0) break; - /* number of lines */ - int reps = (dpi->height - y + 1) / 2; - if (reps > 0) { - this->DrawSmallMapStuff(ptr, tile_x, tile_y, dpi->pitch * 2, reps, mask, _smallmap_draw_procs[this->map_type]); - } + int col_start = x < 0 ? -x : 0; + int col_end = x + SD_MAP_COLUMN_WIDTH > dpi->width ? dpi->width - x : SD_MAP_COLUMN_WIDTH; + int row_start = dy - y; + int row_end = dy + dpi->height - y; + this->DrawSmallMapStuff(ptr, tile_x, tile_y, col_start, col_end, row_start, row_end); } if (y == 0) { tile_y++; y++; - ptr = blitter->MoveTo(ptr, 0, 1); + ptr = blitter->MoveTo(ptr, 0, SD_MAP_ROW_OFFSET / 2); } else { tile_x--; y--; - ptr = blitter->MoveTo(ptr, 0, -1); + ptr = blitter->MoveTo(ptr, 0, -SD_MAP_ROW_OFFSET / 2); } - ptr = blitter->MoveTo(ptr, 2, 0); - x += 2; + ptr = blitter->MoveTo(ptr, SD_MAP_COLUMN_WIDTH / 2, 0); + x += SD_MAP_COLUMN_WIDTH / 2; } - /* draw vehicles? */ - if (this->map_type == SMT_CONTOUR || this->map_type == SMT_VEHICLES) { - Vehicle *v; - - FOR_ALL_VEHICLES(v) { - if (v->type != VEH_EFFECT && - (v->vehstatus & (VS_HIDDEN | VS_UNCLICKABLE)) == 0) { - /* Remap into flat coordinates. */ - Point pt = RemapCoords( - this->RemapX(v->x_pos / TILE_SIZE), - this->RemapY(v->y_pos / TILE_SIZE), - 0); - x = pt.x; - y = pt.y; - - /* Check if y is out of bounds? */ - y -= dpi->top; - if (!IsInsideMM(y, 0, dpi->height)) continue; - - /* Default is to draw both pixels. */ - bool skip = false; - - /* Offset X coordinate */ - x -= this->subscroll + 3 + dpi->left; - - if (x < 0) { - /* if x+1 is 0, that means we're on the very left edge, - * and should thus only draw a single pixel */ - if (++x != 0) continue; - skip = true; - } else if (x >= dpi->width - 1) { - /* Check if we're at the very right edge, and if so draw only a single pixel */ - if (x != dpi->width - 1) continue; - skip = true; - } + DrawVehicles(dpi); - /* Calculate pointer to pixel and the colour */ - byte colour = (this->map_type == SMT_VEHICLES) ? _vehicle_type_colours[v->type] : 0xF; - - /* And draw either one or two pixels depending on clipping */ - blitter->SetPixel(dpi->dst_ptr, x, y, colour); - if (!skip) blitter->SetPixel(dpi->dst_ptr, x + 1, y, colour); - } - } - } + DrawIndustries(dpi); if (this->show_towns) { const Town *t; FOR_ALL_TOWNS(t) { /* Remap the town coordinate */ - Point pt = RemapCoords( - this->RemapX(TileX(t->xy)), - this->RemapY(TileY(t->xy)), - 0); - x = pt.x - this->subscroll + 3 - (t->sign.width_small >> 1); + Point pt = RemapTileCoords(t->xy); + x = pt.x - (t->sign.width_small >> 1); y = pt.y; /* Check if the town sign is within bounds */ @@ -812,15 +891,11 @@ public: /* Draw map indicators */ Point pt = RemapCoords(this->scroll_x, this->scroll_y, 0); - x = vp->virtual_left - pt.x; - y = vp->virtual_top - pt.y; - int x2 = (x + vp->virtual_width) / TILE_SIZE; - int y2 = (y + vp->virtual_height) / TILE_SIZE; - x /= TILE_SIZE; - y /= TILE_SIZE; - - x -= this->subscroll; - x2 -= this->subscroll; + /* UnScale everything separately to produce the same rounding errors as when drawing the background */ + x = UnScalePlainCoord(vp->virtual_left) - UnScalePlainCoord(pt.x); + y = UnScalePlainCoord(vp->virtual_top) - UnScalePlainCoord(pt.y); + int x2 = x + UnScalePlainCoord(vp->virtual_width); + int y2 = y + UnScalePlainCoord(vp->virtual_height); DrawVertMapIndicator(x, y, x, y2); DrawVertMapIndicator(x2, y, x2, y2); @@ -834,43 +909,85 @@ public: { ViewPort *vp = FindWindowById(WC_MAIN_WINDOW, 0)->viewport; - int x = ((vp->virtual_width - (this->widget[SM_WIDGET_MAP].right - this->widget[SM_WIDGET_MAP].left) * TILE_SIZE) / 2 + vp->virtual_left) / 4; - int y = ((vp->virtual_height - (this->widget[SM_WIDGET_MAP].bottom - this->widget[SM_WIDGET_MAP].top ) * TILE_SIZE) / 2 + vp->virtual_top ) / 2 - TILE_SIZE * 2; - this->scroll_x = (y - x) & ~0xF; - this->scroll_y = (x + y) & ~0xF; + int zoomed_width = ScaleByZoom((this->widget[SM_WIDGET_MAP].right - this->widget[SM_WIDGET_MAP].left) * TILE_SIZE, this->zoom); + int zoomed_height = ScaleByZoom((this->widget[SM_WIDGET_MAP].bottom - this->widget[SM_WIDGET_MAP].top) * TILE_SIZE, this->zoom); + int x = ((vp->virtual_width - zoomed_width) / 2 + vp->virtual_left); + int y = ((vp->virtual_height - zoomed_height) / 2 + vp->virtual_top); + this->scroll_x = (y * 2 - x) / 4; + this->scroll_y = (x + y * 2) / 4; this->SetDirty(); + this->HandleButtonClick(SM_WIDGET_CENTERMAP); + } + + /** + * Zoom in the map by one level. + * @param cx horizontal coordinate of center point, relative to SM_WIDGET_MAP widget + * @param cy vertical coordinate of center point, relative to SM_WIDGET_MAP widget + */ + void ZoomIn(int cx, int cy) + { + if (this->zoom > ZOOM_LVL_MIN) { + this->zoom--; + this->DoScroll(cx, cy); + this->SetWidgetDisabledState(SM_WIDGET_ZOOM_IN, this->zoom == ZOOM_LVL_MIN); + this->EnableWidget(SM_WIDGET_ZOOM_OUT); + this->SetDirty(); + } + } + + /** + * Zoom out the map by one level. + * @param cx horizontal coordinate of center point, relative to SM_WIDGET_MAP widget + * @param cy vertical coordinate of center point, relative to SM_WIDGET_MAP widget + */ + void ZoomOut(int cx, int cy) + { + if (this->zoom < ZOOM_LVL_MAX) { + this->zoom++; + this->DoScroll(cx / -2, cy / -2); + this->EnableWidget(SM_WIDGET_ZOOM_IN); + this->SetWidgetDisabledState(SM_WIDGET_ZOOM_OUT, this->zoom == ZOOM_LVL_MAX); + this->SetDirty(); + } + } + + virtual void OnTimeout() + { + /* raise any pressed buttons */ + this->SetWidgetsLoweredState(false, SM_WIDGET_ZOOM_OUT, SM_WIDGET_ZOOM_IN, SM_WIDGET_CENTERMAP, WIDGET_LIST_END); + this->InvalidateWidget(SM_WIDGET_ZOOM_OUT); + this->InvalidateWidget(SM_WIDGET_ZOOM_IN); + this->InvalidateWidget(SM_WIDGET_CENTERMAP); } void ResizeLegend() { Widget *legend = &this->widget[SM_WIDGET_LEGEND]; - int rows = (legend->bottom - legend->top) - 1; - int columns = (legend->right - legend->left) / COLUMN_WIDTH; - int new_rows = (this->map_type == SMT_INDUSTRY) ? ((_smallmap_industry_count + columns - 1) / columns) * 6 : MIN_LEGEND_HEIGHT; + int legend_height = (legend->bottom - legend->top) - 1; + int columns = (legend->right - legend->left) / SD_LEGEND_COLUMN_WIDTH; + int new_legend_height = (this->map_type == SMT_INDUSTRY) ? ((_smallmap_industry_count + columns - 1) / columns) * SD_LEGEND_ROW_HEIGHT : SD_LEGEND_MIN_HEIGHT; - new_rows = max(new_rows, MIN_LEGEND_HEIGHT); - - if (new_rows != rows) { - this->SetDirty(); + new_legend_height = max(new_legend_height, (int)SD_LEGEND_MIN_HEIGHT); + if (new_legend_height != legend_height) { /* The legend widget needs manual adjustment as by default * it lays outside the filler widget's bounds. */ legend->top--; /* Resize the filler widget, and move widgets below it. */ - ResizeWindowForWidget(this, SM_WIDGET_BUTTONSPANEL, 0, new_rows - rows); + ResizeWindowForWidget(this, SM_WIDGET_BUTTONSPANEL, 0, new_legend_height - legend_height); legend->top++; /* Resize map border widget so the window stays the same size */ - ResizeWindowForWidget(this, SM_WIDGET_MAP_BORDER, 0, rows - new_rows); + ResizeWindowForWidget(this, SM_WIDGET_MAP_BORDER, 0, legend_height - new_legend_height); /* Manually adjust the map widget as it lies completely within * the map border widget */ - this->widget[SM_WIDGET_MAP].bottom += rows - new_rows; + this->widget[SM_WIDGET_MAP].bottom += legend_height - new_legend_height; this->SetDirty(); } } - SmallMapWindow(const WindowDesc *desc, int window_number) : Window(desc, window_number) + SmallMapWindow(const WindowDesc *desc, int window_number) : Window(desc, window_number), zoom(ZOOM_LVL_NORMAL) { this->LowerWidget(this->map_type + SM_WIDGET_CONTOUR); this->SetWidgetLoweredState(SM_WIDGET_TOGGLETOWNNAME, this->show_towns); @@ -901,7 +1018,7 @@ public: if (tbl->col_break || y >= legend->bottom) { /* Column break needed, continue at top, COLUMN_WIDTH pixels * (one "row") to the right. */ - x += COLUMN_WIDTH; + x += SD_LEGEND_COLUMN_WIDTH; y = y_org; } @@ -914,19 +1031,19 @@ public: if (!tbl->show_on_map) { /* Simply draw the string, not the black border of the legend colour. * This will enforce the idea of the disabled item */ - DrawString(x + 11, x + COLUMN_WIDTH - 1, y, STR_SMALLMAP_INDUSTRY, TC_GREY); + DrawString(x + 11, x + SD_LEGEND_COLUMN_WIDTH - 1, y, STR_SMALLMAP_INDUSTRY, TC_GREY); } else { - DrawString(x + 11, x + COLUMN_WIDTH - 1, y, STR_SMALLMAP_INDUSTRY, TC_BLACK); + DrawString(x + 11, x + SD_LEGEND_COLUMN_WIDTH - 1, y, STR_SMALLMAP_INDUSTRY, TC_BLACK); GfxFillRect(x, y + 1, x + 8, y + 5, 0); // outer border of the legend colour } } else { /* Anything that is not an industry is using normal process */ GfxFillRect(x, y + 1, x + 8, y + 5, 0); - DrawString(x + 11, x + COLUMN_WIDTH - 1, y, tbl->legend); + DrawString(x + 11, x + SD_LEGEND_COLUMN_WIDTH - 1, y, tbl->legend); } GfxFillRect(x + 1, y + 2, x + 7, y + 4, tbl->colour); // legend colour - y += 6; + y += SD_LEGEND_ROW_HEIGHT; } const Widget *wi = &this->widget[SM_WIDGET_MAP]; @@ -952,12 +1069,30 @@ public: Point pt = RemapCoords(this->scroll_x, this->scroll_y, 0); Window *w = FindWindowById(WC_MAIN_WINDOW, 0); w->viewport->follow_vehicle = INVALID_VEHICLE; - w->viewport->dest_scrollpos_x = pt.x + ((_cursor.pos.x - this->left + 2) << 4) - (w->viewport->virtual_width >> 1); - w->viewport->dest_scrollpos_y = pt.y + ((_cursor.pos.y - this->top - 16) << 4) - (w->viewport->virtual_height >> 1); + int scaled_x_off = ScaleByZoom((_cursor.pos.x - this->left - SD_MAP_EXTRA_PADDING) * TILE_SIZE, this->zoom); + int scaled_y_off = ScaleByZoom((_cursor.pos.y - this->top - SD_MAP_EXTRA_PADDING - WD_CAPTION_HEIGHT) * TILE_SIZE, this->zoom); + w->viewport->dest_scrollpos_x = pt.x + scaled_x_off - w->viewport->virtual_width / 2; + w->viewport->dest_scrollpos_y = pt.y + scaled_y_off - w->viewport->virtual_height / 2; this->SetDirty(); } break; + case SM_WIDGET_ZOOM_OUT: + this->ZoomOut( + (this->widget[SM_WIDGET_MAP].right - this->widget[SM_WIDGET_MAP].left) / 2, + (this->widget[SM_WIDGET_MAP].bottom - this->widget[SM_WIDGET_MAP].top) / 2 + ); + this->HandleButtonClick(SM_WIDGET_ZOOM_OUT); + SndPlayFx(SND_15_BEEP); + break; + case SM_WIDGET_ZOOM_IN: + this->ZoomIn( + (this->widget[SM_WIDGET_MAP].right - this->widget[SM_WIDGET_MAP].left) / 2, + (this->widget[SM_WIDGET_MAP].bottom - this->widget[SM_WIDGET_MAP].top) / 2 + ); + this->HandleButtonClick(SM_WIDGET_ZOOM_IN); + SndPlayFx(SND_15_BEEP); + break; case SM_WIDGET_CONTOUR: // Show land contours case SM_WIDGET_VEHICLES: // Show vehicles case SM_WIDGET_INDUSTRIES: // Show industries @@ -994,7 +1129,7 @@ public: if (this->map_type == SMT_INDUSTRY) { /* if click on industries label, find right industry type and enable/disable it */ Widget *wi = &this->widget[SM_WIDGET_LEGEND]; // label panel - uint column = (pt.x - 4) / COLUMN_WIDTH; + uint column = (pt.x - 4) / SD_LEGEND_COLUMN_WIDTH; uint line = (pt.y - wi->top - 2) / 6; int rows_per_column = (wi->bottom - wi->top) / 6; @@ -1033,6 +1168,27 @@ public: } } + virtual void OnMouseWheel(int wheel) + { + /* Cursor position relative to window */ + int cx = _cursor.pos.x - this->left; + int cy = _cursor.pos.y - this->top; + + /* Is cursor over the map ? */ + if (IsInsideMM(cx, this->widget[SM_WIDGET_MAP].left, this->widget[SM_WIDGET_MAP].right + 1) && + IsInsideMM(cy, this->widget[SM_WIDGET_MAP].top, this->widget[SM_WIDGET_MAP].bottom + 1)) { + /* Cursor position relative to map */ + cx -= this->widget[SM_WIDGET_MAP].left; + cy -= this->widget[SM_WIDGET_MAP].top; + + if (wheel < 0) { + this->ZoomIn(cx, cy); + } else { + this->ZoomOut(cx, cy); + } + } + }; + virtual void OnRightClick(Point pt, int widget) { if (widget == SM_WIDGET_MAP) { @@ -1046,61 +1202,58 @@ public: virtual void OnTick() { /* update the window every now and then */ - if ((++this->refresh & 0x1F) == 0) this->SetDirty(); + if (this->refresh++ == FORCE_REFRESH) { + this->refresh = 0; + this->SetDirty(); + } } virtual void OnScroll(Point delta) { _cursor.fix_at = true; + DoScroll(delta.x, delta.y); + this->SetDirty(); + } - int x = this->scroll_x; - int y = this->scroll_y; - - int sub = this->subscroll + delta.x; - - x -= (sub >> 2) << 4; - y += (sub >> 2) << 4; - sub &= 3; - - x += (delta.y >> 1) << 4; - y += (delta.y >> 1) << 4; - - if (delta.y & 1) { - x += TILE_SIZE; - sub += 2; - if (sub > 3) { - sub -= 4; - x -= TILE_SIZE; - y += TILE_SIZE; - } - } - - int hx = (this->widget[SM_WIDGET_MAP].right - this->widget[SM_WIDGET_MAP].left) / 2; - int hy = (this->widget[SM_WIDGET_MAP].bottom - this->widget[SM_WIDGET_MAP].top ) / 2; - int hvx = hx * -4 + hy * 8; - int hvy = hx * 4 + hy * 8; - if (x < -hvx) { - x = -hvx; - sub = 0; - } - if (x > (int)MapMaxX() * TILE_SIZE - hvx) { - x = MapMaxX() * TILE_SIZE - hvx; - sub = 0; - } - if (y < -hvy) { - y = -hvy; - sub = 0; + /** + * Do the actual scrolling, but don't fix the cursor or set the window dirty. + * @param dx x offset to scroll in screen dimension + * @param dy y offset to scroll in screen dimension + */ + void DoScroll(int dx, int dy) + { + /* divide as late as possible to avoid premature reduction to 0, which causes "jumpy" behaviour + * at the same time make sure this is the exact reverse function of the drawing methods in order to + * avoid map indicators shifting around: + * 1. add/subtract + * 2. * TILE_SIZE + * 3. scale + */ + int x = dy * 2 - dx; + int y = dx + dy * 2; + + /* round to next divisible by 4 to allow for smoother scrolling */ + int rem_x = abs(x % 4); + int rem_y = abs(y % 4); + if (rem_x != 0) { + x += x > 0 ? 4 - rem_x : rem_x - 4; } - if (y > (int)MapMaxY() * TILE_SIZE - hvy) { - y = MapMaxY() * TILE_SIZE - hvy; - sub = 0; + if (rem_y != 0) { + y += y > 0 ? 4 - rem_y : rem_y - 4; } - this->scroll_x = x; - this->scroll_y = y; - this->subscroll = sub; - - this->SetDirty(); + this->scroll_x += ScaleByZoomLower(x / 4 * TILE_SIZE, this->zoom); + this->scroll_y += ScaleByZoomLower(y / 4 * TILE_SIZE, this->zoom); + + /* enforce the screen limits */ + int hx = this->widget[SM_WIDGET_MAP].right - this->widget[SM_WIDGET_MAP].left; + int hy = this->widget[SM_WIDGET_MAP].bottom - this->widget[SM_WIDGET_MAP].top; + int hvx = ScaleByZoomLower(hy * 4 - hx * 2, this->zoom); + int hvy = ScaleByZoomLower(hx * 2 + hy * 4, this->zoom); + this->scroll_x = max(-hvx, this->scroll_x); + this->scroll_y = max(-hvy, this->scroll_y); + this->scroll_x = min(MapMaxX() * TILE_SIZE, this->scroll_x); + this->scroll_y = min(MapMaxY() * TILE_SIZE - hvy, this->scroll_y); } virtual void OnResize(Point delta)