diff --git a/src/viewport.cpp b/src/viewport.cpp --- a/src/viewport.cpp +++ b/src/viewport.cpp @@ -82,6 +82,7 @@ #include "tilehighlight_func.h" #include "window_gui.h" #include "linkgraph/linkgraph_gui.h" +#include "viewport_kdtree.h" #include "viewport_sprite_sorter.h" #include "bridge_map.h" #include "company_base.h" @@ -99,6 +100,10 @@ Point _tile_fract_coords; +ViewportSignKdtree _viewport_sign_kdtree(&Kdtree_ViewportSignXYFunc); +static int _viewport_sign_maxwidth = 0; + + static const int MAX_TILE_EXTENT_LEFT = ZOOM_LVL_BASE * TILE_PIXELS; ///< Maximum left extent of tile relative to north corner. static const int MAX_TILE_EXTENT_RIGHT = ZOOM_LVL_BASE * TILE_PIXELS; ///< Maximum right extent of tile relative to north corner. static const int MAX_TILE_EXTENT_TOP = ZOOM_LVL_BASE * MAX_BUILDING_PIXELS; ///< Maximum top extent of tile relative to north corner (not considering bridges). @@ -1214,62 +1219,117 @@ void ViewportAddString(const DrawPixelIn } } -static void ViewportAddTownNames(DrawPixelInfo *dpi) +static Rect ExpandRectWithViewportSignMargins(Rect r, ZoomLevel zoom) { - if (!HasBit(_display_opt, DO_SHOW_TOWN_NAMES) || _game_mode == GM_MENU) return; - - const Town *t; - FOR_ALL_TOWNS(t) { - ViewportAddString(dpi, ZOOM_LVL_OUT_16X, &t->cache.sign, - _settings_client.gui.population_in_label ? STR_VIEWPORT_TOWN_POP : STR_VIEWPORT_TOWN, - STR_VIEWPORT_TOWN_TINY_WHITE, STR_VIEWPORT_TOWN_TINY_BLACK, - t->index, t->cache.population); - } + /* Pessimistically always use normal font, but also assume small font is never larger in either dimension */ + const int fh = FONT_HEIGHT_NORMAL; + const int max_tw = _viewport_sign_maxwidth / 2 + 1; + const int expand_y = ScaleByZoom(VPSM_TOP + fh + VPSM_BOTTOM, zoom); + const int expand_x = ScaleByZoom(VPSM_LEFT + max_tw + VPSM_RIGHT, zoom); + + r.left -= expand_x; + r.right += expand_x; + r.top -= expand_y; + r.bottom += expand_y; + + return r; } - -static void ViewportAddStationNames(DrawPixelInfo *dpi) +static void ViewportAddKdtreeSigns(DrawPixelInfo *dpi) { - if (!(HasBit(_display_opt, DO_SHOW_STATION_NAMES) || HasBit(_display_opt, DO_SHOW_WAYPOINT_NAMES)) || _game_mode == GM_MENU) return; + Rect search_rect{ dpi->left, dpi->top, dpi->left + dpi->width, dpi->top + dpi->height }; + search_rect = ExpandRectWithViewportSignMargins(search_rect, dpi->zoom); + + bool show_stations = HasBit(_display_opt, DO_SHOW_STATION_NAMES) && _game_mode != GM_MENU; + bool show_waypoints = HasBit(_display_opt, DO_SHOW_WAYPOINT_NAMES) && _game_mode != GM_MENU; + bool show_towns = HasBit(_display_opt, DO_SHOW_TOWN_NAMES) && _game_mode != GM_MENU; + bool show_signs = HasBit(_display_opt, DO_SHOW_SIGNS) && !IsInvisibilitySet(TO_SIGNS); + bool show_competitors = HasBit(_display_opt, DO_SHOW_COMPETITOR_SIGNS); const BaseStation *st; - FOR_ALL_BASE_STATIONS(st) { - /* Check whether the base station is a station or a waypoint */ - bool is_station = Station::IsExpected(st); - - /* Don't draw if the display options are disabled */ - if (!HasBit(_display_opt, is_station ? DO_SHOW_STATION_NAMES : DO_SHOW_WAYPOINT_NAMES)) continue; - - /* Don't draw if station is owned by another company and competitor station names are hidden. Stations owned by none are never ignored. */ - if (!HasBit(_display_opt, DO_SHOW_COMPETITOR_SIGNS) && _local_company != st->owner && st->owner != OWNER_NONE) continue; - - ViewportAddString(dpi, ZOOM_LVL_OUT_16X, &st->sign, - is_station ? STR_VIEWPORT_STATION : STR_VIEWPORT_WAYPOINT, - (is_station ? STR_VIEWPORT_STATION : STR_VIEWPORT_WAYPOINT) + 1, STR_NULL, + const Sign *si; + + /* Collect all the items first and draw afterwards, to ensure layering */ + std::vector stations; + std::vector towns; + std::vector signs; + + _viewport_sign_kdtree.FindContained(search_rect.left, search_rect.top, search_rect.right, search_rect.bottom, [&](const ViewportSignKdtreeItem & item) { + switch (item.type) { + case ViewportSignKdtreeItem::VKI_STATION: + if (!show_stations) break; + st = BaseStation::Get(item.id.station); + + /* Don't draw if station is owned by another company and competitor station names are hidden. Stations owned by none are never ignored. */ + if (!show_competitors && _local_company != st->owner && st->owner != OWNER_NONE) break; + + stations.push_back(st); + break; + + case ViewportSignKdtreeItem::VKI_WAYPOINT: + if (!show_waypoints) break; + st = BaseStation::Get(item.id.station); + + /* Don't draw if station is owned by another company and competitor station names are hidden. Stations owned by none are never ignored. */ + if (!show_competitors && _local_company != st->owner && st->owner != OWNER_NONE) break; + + stations.push_back(st); + break; + + case ViewportSignKdtreeItem::VKI_TOWN: + if (!show_towns) break; + towns.push_back(Town::Get(item.id.town)); + break; + + case ViewportSignKdtreeItem::VKI_SIGN: + if (!show_signs) break; + si = Sign::Get(item.id.sign); + + /* Don't draw if sign is owned by another company and competitor signs should be hidden. + * Note: It is intentional that also signs owned by OWNER_NONE are hidden. Bankrupt + * companies can leave OWNER_NONE signs after them. */ + if (!show_competitors && _local_company != si->owner && si->owner != OWNER_DEITY) break; + + signs.push_back(si); + break; + + default: + NOT_REACHED(); + } + }); + + /* Layering order (bottom to top): Town names, signs, stations */ + + for (const auto *t : towns) { + ViewportAddString(dpi, ZOOM_LVL_OUT_16X, &t->cache.sign, + _settings_client.gui.population_in_label ? STR_VIEWPORT_TOWN_POP : STR_VIEWPORT_TOWN, + STR_VIEWPORT_TOWN_TINY_WHITE, STR_VIEWPORT_TOWN_TINY_BLACK, + t->index, t->cache.population); + } + + for (const auto *si : signs) { + ViewportAddString(dpi, ZOOM_LVL_OUT_16X, &si->sign, + STR_WHITE_SIGN, + (IsTransparencySet(TO_SIGNS) || si->owner == OWNER_DEITY) ? STR_VIEWPORT_SIGN_SMALL_WHITE : STR_VIEWPORT_SIGN_SMALL_BLACK, STR_NULL, + si->index, 0, (si->owner == OWNER_NONE) ? COLOUR_GREY : (si->owner == OWNER_DEITY ? INVALID_COLOUR : _company_colours[si->owner])); + } + + for (const auto *st : stations) { + if (Station::IsExpected(st)) { + /* Station */ + ViewportAddString(dpi, ZOOM_LVL_OUT_16X, &st->sign, + STR_VIEWPORT_STATION, STR_VIEWPORT_STATION + 1, STR_NULL, st->index, st->facilities, (st->owner == OWNER_NONE || !st->IsInUse()) ? COLOUR_GREY : _company_colours[st->owner]); + } else { + /* Waypoint */ + ViewportAddString(dpi, ZOOM_LVL_OUT_16X, &st->sign, + STR_VIEWPORT_WAYPOINT, STR_VIEWPORT_WAYPOINT + 1, STR_NULL, + st->index, st->facilities, (st->owner == OWNER_NONE || !st->IsInUse()) ? COLOUR_GREY : _company_colours[st->owner]); + } } } -static void ViewportAddSigns(DrawPixelInfo *dpi) -{ - /* Signs are turned off or are invisible */ - if (!HasBit(_display_opt, DO_SHOW_SIGNS) || IsInvisibilitySet(TO_SIGNS)) return; - - const Sign *si; - FOR_ALL_SIGNS(si) { - /* Don't draw if sign is owned by another company and competitor signs should be hidden. - * Note: It is intentional that also signs owned by OWNER_NONE are hidden. Bankrupt - * companies can leave OWNER_NONE signs after them. */ - if (!HasBit(_display_opt, DO_SHOW_COMPETITOR_SIGNS) && _local_company != si->owner && si->owner != OWNER_DEITY) continue; - - ViewportAddString(dpi, ZOOM_LVL_OUT_16X, &si->sign, - STR_WHITE_SIGN, - (IsTransparencySet(TO_SIGNS) || si->owner == OWNER_DEITY) ? STR_VIEWPORT_SIGN_SMALL_WHITE : STR_VIEWPORT_SIGN_SMALL_BLACK, STR_NULL, - si->index, 0, (si->owner == OWNER_NONE) ? COLOUR_GREY : (si->owner == OWNER_DEITY ? INVALID_COLOUR : _company_colours[si->owner])); - } -} - /** * Update the position of the viewport sign. * @param center the (preferred) center of the viewport sign @@ -1520,9 +1580,7 @@ void ViewportDoDraw(const ViewPort *vp, ViewportAddLandscape(); ViewportAddVehicles(&_vd.dpi); - ViewportAddTownNames(&_vd.dpi); - ViewportAddStationNames(&_vd.dpi); - ViewportAddSigns(&_vd.dpi); + ViewportAddKdtreeSigns(&_vd.dpi); DrawTextEffects(&_vd.dpi); @@ -1930,75 +1988,202 @@ static bool CheckClickOnViewportSign(con int sign_half_width = ScaleByZoom((small ? sign->width_small : sign->width_normal) / 2, vp->zoom); int sign_height = ScaleByZoom(VPSM_TOP + (small ? FONT_HEIGHT_SMALL : FONT_HEIGHT_NORMAL) + VPSM_BOTTOM, vp->zoom); - x = ScaleByZoom(x - vp->left, vp->zoom) + vp->virtual_left; - y = ScaleByZoom(y - vp->top, vp->zoom) + vp->virtual_top; - return y >= sign->top && y < sign->top + sign_height && x >= sign->center - sign_half_width && x < sign->center + sign_half_width; } -static bool CheckClickOnTown(const ViewPort *vp, int x, int y) + +/** + * Check whether any viewport sign was clicked, and dispatch the click. + * @param vp the clicked viewport + * @param x X position of click + * @param y Y position of click + * @return true if the sign was hit + */ +static bool CheckClickOnViewportSign(const ViewPort *vp, int x, int y) { - if (!HasBit(_display_opt, DO_SHOW_TOWN_NAMES)) return false; - - const Town *t; - FOR_ALL_TOWNS(t) { - if (CheckClickOnViewportSign(vp, x, y, &t->cache.sign)) { - ShowTownViewWindow(t->index); - return true; + x = ScaleByZoom(x - vp->left, vp->zoom) + vp->virtual_left; + y = ScaleByZoom(y - vp->top, vp->zoom) + vp->virtual_top; + + Rect search_rect{ x - 1, y - 1, x + 1, y + 1 }; + search_rect = ExpandRectWithViewportSignMargins(search_rect, vp->zoom); + + bool show_stations = HasBit(_display_opt, DO_SHOW_STATION_NAMES) && _game_mode != GM_MENU; + bool show_waypoints = HasBit(_display_opt, DO_SHOW_WAYPOINT_NAMES) && _game_mode != GM_MENU; + bool show_towns = HasBit(_display_opt, DO_SHOW_TOWN_NAMES) && _game_mode != GM_MENU; + bool show_signs = HasBit(_display_opt, DO_SHOW_SIGNS) && !IsInvisibilitySet(TO_SIGNS); + bool show_competitors = HasBit(_display_opt, DO_SHOW_COMPETITOR_SIGNS); + + /* Topmost of each type that was hit */ + BaseStation *st = NULL, *last_st = NULL; + Town *t = NULL, *last_t = NULL; + Sign *si = NULL, *last_si = NULL; + + /* See ViewportAddKdtreeSigns() for details on the search logic */ + _viewport_sign_kdtree.FindContained(search_rect.left, search_rect.top, search_rect.right, search_rect.bottom, [&](const ViewportSignKdtreeItem & item) { + switch (item.type) { + case ViewportSignKdtreeItem::VKI_STATION: + if (!show_stations) break; + st = BaseStation::Get(item.id.station); + if (!show_competitors && _local_company != st->owner && st->owner != OWNER_NONE) break; + if (CheckClickOnViewportSign(vp, x, y, &st->sign)) last_st = st; + break; + + case ViewportSignKdtreeItem::VKI_WAYPOINT: + if (!show_waypoints) break; + st = BaseStation::Get(item.id.station); + if (!show_competitors && _local_company != st->owner && st->owner != OWNER_NONE) break; + if (CheckClickOnViewportSign(vp, x, y, &st->sign)) last_st = st; + break; + + case ViewportSignKdtreeItem::VKI_TOWN: + if (!show_towns) break; + t = Town::Get(item.id.town); + if (CheckClickOnViewportSign(vp, x, y, &t->cache.sign)) last_t = t; + break; + + case ViewportSignKdtreeItem::VKI_SIGN: + if (!show_signs) break; + si = Sign::Get(item.id.sign); + if (!show_competitors && _local_company != si->owner && si->owner != OWNER_DEITY) break; + if (CheckClickOnViewportSign(vp, x, y, &si->sign)) last_si = si; + break; + + default: + NOT_REACHED(); } + }); + + /* Select which hit to handle based on priority */ + if (last_st != NULL) { + if (Station::IsExpected(last_st)) { + ShowStationViewWindow(last_st->index); + } else { + ShowWaypointWindow(Waypoint::From(last_st)); + } + return true; + } else if (last_t != NULL) { + ShowTownViewWindow(last_t->index); + return true; + } else if (last_si != NULL) { + HandleClickOnSign(last_si); + return true; + } else { + return false; } - - return false; } -static bool CheckClickOnStation(const ViewPort *vp, int x, int y) + +ViewportSignKdtreeItem ViewportSignKdtreeItem::MakeStation(StationID id) { - if (!(HasBit(_display_opt, DO_SHOW_STATION_NAMES) || HasBit(_display_opt, DO_SHOW_WAYPOINT_NAMES)) || IsInvisibilitySet(TO_SIGNS)) return false; - - const BaseStation *st; - FOR_ALL_BASE_STATIONS(st) { - /* Check whether the base station is a station or a waypoint */ - bool is_station = Station::IsExpected(st); - - /* Don't check if the display options are disabled */ - if (!HasBit(_display_opt, is_station ? DO_SHOW_STATION_NAMES : DO_SHOW_WAYPOINT_NAMES)) continue; - - /* Don't check if competitor signs are not shown and the sign isn't owned by the local company */ - if (!HasBit(_display_opt, DO_SHOW_COMPETITOR_SIGNS) && _local_company != st->owner && st->owner != OWNER_NONE) continue; - - if (CheckClickOnViewportSign(vp, x, y, &st->sign)) { - if (is_station) { - ShowStationViewWindow(st->index); - } else { - ShowWaypointWindow(Waypoint::From(st)); - } - return true; - } + ViewportSignKdtreeItem item; + item.type = VKI_STATION; + item.id.station = id; + + const Station *st = Station::Get(id); + Point pt = RemapCoords2(TileX(st->xy) * TILE_SIZE, TileY(st->xy) * TILE_SIZE); + + pt.y -= 32 * ZOOM_LVL_BASE; + if ((st->facilities & FACIL_AIRPORT) && st->airport.type == AT_OILRIG) pt.y -= 16 * ZOOM_LVL_BASE; + + item.center = pt.x; + item.top = pt.y; + + /* Assume the sign can be a candidate for drawing, so measure its width */ + _viewport_sign_maxwidth = max(_viewport_sign_maxwidth, st->sign.width_normal); + + return item; +} + +ViewportSignKdtreeItem ViewportSignKdtreeItem::MakeWaypoint(StationID id) +{ + ViewportSignKdtreeItem item; + item.type = VKI_WAYPOINT; + item.id.station = id; + + const Waypoint *st = Waypoint::Get(id); + Point pt = RemapCoords2(TileX(st->xy) * TILE_SIZE, TileY(st->xy) * TILE_SIZE); + + pt.y -= 32 * ZOOM_LVL_BASE; + + item.center = pt.x; + item.top = pt.y; + + /* Assume the sign can be a candidate for drawing, so measure its width */ + _viewport_sign_maxwidth = max(_viewport_sign_maxwidth, st->sign.width_normal); + + return item; +} + +ViewportSignKdtreeItem ViewportSignKdtreeItem::MakeTown(TownID id) +{ + ViewportSignKdtreeItem item; + item.type = VKI_TOWN; + item.id.town = id; + + const Town *town = Town::Get(id); + Point pt = RemapCoords2(TileX(town->xy) * TILE_SIZE, TileY(town->xy) * TILE_SIZE); + + pt.y -= 24 * ZOOM_LVL_BASE; + + item.center = pt.x; + item.top = pt.y; + + /* Assume the sign can be a candidate for drawing, so measure its width */ + _viewport_sign_maxwidth = max(_viewport_sign_maxwidth, town->cache.sign.width_normal); + + return item; +} + +ViewportSignKdtreeItem ViewportSignKdtreeItem::MakeSign(SignID id) +{ + ViewportSignKdtreeItem item; + item.type = VKI_SIGN; + item.id.sign = id; + + const Sign *sign = Sign::Get(id); + Point pt = RemapCoords(sign->x, sign->y, sign->z); + + pt.y -= 6 * ZOOM_LVL_BASE; + + item.center = pt.x; + item.top = pt.y; + + /* Assume the sign can be a candidate for drawing, so measure its width */ + _viewport_sign_maxwidth = max(_viewport_sign_maxwidth, sign->sign.width_normal); + + return item; +} + +void RebuildViewportKdtree() +{ + /* Reset biggest size sign seen */ + _viewport_sign_maxwidth = 0; + + std::vector items; + items.reserve(BaseStation::GetNumItems() + Town::GetNumItems() + Sign::GetNumItems()); + + const Station *st; + FOR_ALL_STATIONS(st) { + items.push_back(ViewportSignKdtreeItem::MakeStation(st->index)); } - return false; -} - - -static bool CheckClickOnSign(const ViewPort *vp, int x, int y) -{ - /* Signs are turned off, or they are transparent and invisibility is ON, or company is a spectator */ - if (!HasBit(_display_opt, DO_SHOW_SIGNS) || IsInvisibilitySet(TO_SIGNS) || _local_company == COMPANY_SPECTATOR) return false; - - const Sign *si; - FOR_ALL_SIGNS(si) { - /* If competitor signs are hidden, don't check signs that aren't owned by local company */ - if (!HasBit(_display_opt, DO_SHOW_COMPETITOR_SIGNS) && _local_company != si->owner && si->owner != OWNER_DEITY) continue; - if (si->owner == OWNER_DEITY && _game_mode != GM_EDITOR) continue; - - if (CheckClickOnViewportSign(vp, x, y, &si->sign)) { - HandleClickOnSign(si); - return true; - } + const Waypoint *wp; + FOR_ALL_WAYPOINTS(wp) { + items.push_back(ViewportSignKdtreeItem::MakeWaypoint(wp->index)); } - return false; + const Town *town; + FOR_ALL_TOWNS(town) { + items.push_back(ViewportSignKdtreeItem::MakeTown(town->index)); + } + + const Sign *sign; + FOR_ALL_SIGNS(sign) { + items.push_back(ViewportSignKdtreeItem::MakeSign(sign->index)); + } + + _viewport_sign_kdtree.Build(items.begin(), items.end()); } @@ -2045,9 +2230,7 @@ bool HandleViewportClicked(const ViewPor return true; } - if (CheckClickOnTown(vp, x, y)) return true; - if (CheckClickOnStation(vp, x, y)) return true; - if (CheckClickOnSign(vp, x, y)) return true; + if (CheckClickOnViewportSign(vp, x, y)) return true; bool result = CheckClickOnLandscape(vp, x, y); if (v != NULL) {