Update: Translations from eints
[openttd-github.git] / src / station_gui.cpp
blob30238c319a3d983bb199160d7605c725494ea8b2
1 /*
2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
6 */
8 /** @file station_gui.cpp The GUI for stations. */
10 #include "stdafx.h"
11 #include "debug.h"
12 #include "gui.h"
13 #include "textbuf_gui.h"
14 #include "company_func.h"
15 #include "command_func.h"
16 #include "vehicle_gui.h"
17 #include "cargotype.h"
18 #include "station_gui.h"
19 #include "strings_func.h"
20 #include "string_func.h"
21 #include "window_func.h"
22 #include "viewport_func.h"
23 #include "dropdown_type.h"
24 #include "dropdown_common_type.h"
25 #include "dropdown_func.h"
26 #include "station_base.h"
27 #include "waypoint_base.h"
28 #include "tilehighlight_func.h"
29 #include "company_base.h"
30 #include "sortlist_type.h"
31 #include "core/geometry_func.hpp"
32 #include "vehiclelist.h"
33 #include "town.h"
34 #include "linkgraph/linkgraph.h"
35 #include "zoom_func.h"
36 #include "station_cmd.h"
38 #include "widgets/station_widget.h"
40 #include "table/strings.h"
42 #include "safeguards.h"
44 struct StationTypeFilter
46 using StationType = Station;
48 static bool IsValidID(StationID id) { return Station::IsValidID(id); }
49 static bool IsValidBaseStation(const BaseStation *st) { return Station::IsExpected(st); }
50 static bool IsAcceptableWaypointTile(TileIndex) { return false; }
51 static constexpr bool IsWaypoint() { return false; }
54 template <bool ROAD, TileType TILE_TYPE>
55 struct GenericWaypointTypeFilter
57 using StationType = Waypoint;
59 static bool IsValidID(StationID id) { return Waypoint::IsValidID(id) && HasBit(Waypoint::Get(id)->waypoint_flags, WPF_ROAD) == ROAD; }
60 static bool IsValidBaseStation(const BaseStation *st) { return Waypoint::IsExpected(st) && HasBit(Waypoint::From(st)->waypoint_flags, WPF_ROAD) == ROAD; }
61 static bool IsAcceptableWaypointTile(TileIndex tile) { return IsTileType(tile, TILE_TYPE); }
62 static constexpr bool IsWaypoint() { return true; }
64 using RailWaypointTypeFilter = GenericWaypointTypeFilter<false, MP_RAILWAY>;
65 using RoadWaypointTypeFilter = GenericWaypointTypeFilter<true, MP_ROAD>;
67 /**
68 * Calculates and draws the accepted or supplied cargo around the selected tile(s)
69 * @param left x position where the string is to be drawn
70 * @param right the right most position to draw on
71 * @param top y position where the string is to be drawn
72 * @param sct which type of cargo is to be displayed (passengers/non-passengers)
73 * @param rad radius around selected tile(s) to be searched
74 * @param supplies if supplied cargoes should be drawn, else accepted cargoes
75 * @return Returns the y value below the string that was drawn
77 int DrawStationCoverageAreaText(int left, int right, int top, StationCoverageType sct, int rad, bool supplies)
79 TileIndex tile = TileVirtXY(_thd.pos.x, _thd.pos.y);
80 CargoTypes cargo_mask = 0;
81 if (_thd.drawstyle == HT_RECT && tile < Map::Size()) {
82 CargoArray cargoes;
83 if (supplies) {
84 cargoes = GetProductionAroundTiles(tile, _thd.size.x / TILE_SIZE, _thd.size.y / TILE_SIZE, rad);
85 } else {
86 cargoes = GetAcceptanceAroundTiles(tile, _thd.size.x / TILE_SIZE, _thd.size.y / TILE_SIZE, rad);
89 /* Convert cargo counts to a set of cargo bits, and draw the result. */
90 for (CargoID i = 0; i < NUM_CARGO; i++) {
91 switch (sct) {
92 case SCT_PASSENGERS_ONLY: if (!IsCargoInClass(i, CC_PASSENGERS)) continue; break;
93 case SCT_NON_PASSENGERS_ONLY: if (IsCargoInClass(i, CC_PASSENGERS)) continue; break;
94 case SCT_ALL: break;
95 default: NOT_REACHED();
97 if (cargoes[i] >= (supplies ? 1U : 8U)) SetBit(cargo_mask, i);
100 SetDParam(0, cargo_mask);
101 return DrawStringMultiLine(left, right, top, INT32_MAX, supplies ? STR_STATION_BUILD_SUPPLIES_CARGO : STR_STATION_BUILD_ACCEPTS_CARGO);
105 * Find stations adjacent to the current tile highlight area, so that existing coverage
106 * area can be drawn.
108 template <typename T>
109 void FindStationsAroundSelection()
111 /* With distant join we don't know which station will be selected, so don't show any */
112 if (_ctrl_pressed) {
113 SetViewportCatchmentSpecializedStation<typename T::StationType>(nullptr, true);
114 return;
117 /* Tile area for TileHighlightData */
118 TileArea location(TileVirtXY(_thd.pos.x, _thd.pos.y), _thd.size.x / TILE_SIZE - 1, _thd.size.y / TILE_SIZE - 1);
120 /* If the current tile is already a station, then it must be the nearest station. */
121 if (IsTileType(location.tile, MP_STATION) && GetTileOwner(location.tile) == _local_company) {
122 typename T::StationType *st = T::StationType::GetByTile(location.tile);
123 if (st != nullptr && T::IsValidBaseStation(st)) {
124 SetViewportCatchmentSpecializedStation<typename T::StationType>(st, true);
125 return;
129 /* Extended area by one tile */
130 uint x = TileX(location.tile);
131 uint y = TileY(location.tile);
133 /* Waypoints can only be built on existing rail/road tiles, so don't extend area if not highlighting a rail tile. */
134 int max_c = T::IsWaypoint() && !T::IsAcceptableWaypointTile(location.tile) ? 0 : 1;
135 TileArea ta(TileXY(std::max<int>(0, x - max_c), std::max<int>(0, y - max_c)), TileXY(std::min<int>(Map::MaxX(), x + location.w + max_c), std::min<int>(Map::MaxY(), y + location.h + max_c)));
137 typename T::StationType *adjacent = nullptr;
139 /* Direct loop instead of ForAllStationsAroundTiles as we are not interested in catchment area */
140 for (TileIndex tile : ta) {
141 if (IsTileType(tile, MP_STATION) && GetTileOwner(tile) == _local_company) {
142 typename T::StationType *st = T::StationType::GetByTile(tile);
143 if (st == nullptr || !T::IsValidBaseStation(st)) continue;
144 if (adjacent != nullptr && st != adjacent) {
145 /* Multiple nearby, distant join is required. */
146 adjacent = nullptr;
147 break;
149 adjacent = st;
152 SetViewportCatchmentSpecializedStation<typename T::StationType>(adjacent, true);
156 * Check whether we need to redraw the station coverage text.
157 * If it is needed actually make the window for redrawing.
158 * @param w the window to check.
160 void CheckRedrawStationCoverage(const Window *w)
162 /* Test if ctrl state changed */
163 static bool _last_ctrl_pressed;
164 if (_ctrl_pressed != _last_ctrl_pressed) {
165 _thd.dirty = 0xff;
166 _last_ctrl_pressed = _ctrl_pressed;
169 if (_thd.dirty & 1) {
170 _thd.dirty &= ~1;
171 w->SetDirty();
173 if (_settings_client.gui.station_show_coverage && _thd.drawstyle == HT_RECT) {
174 FindStationsAroundSelection<StationTypeFilter>();
179 template <typename T>
180 void CheckRedrawWaypointCoverage()
182 /* Test if ctrl state changed */
183 static bool _last_ctrl_pressed;
184 if (_ctrl_pressed != _last_ctrl_pressed) {
185 _thd.dirty = 0xff;
186 _last_ctrl_pressed = _ctrl_pressed;
189 if (_thd.dirty & 1) {
190 _thd.dirty &= ~1;
192 if (_thd.drawstyle == HT_RECT) {
193 FindStationsAroundSelection<T>();
198 void CheckRedrawRailWaypointCoverage(const Window *)
200 CheckRedrawWaypointCoverage<RailWaypointTypeFilter>();
203 void CheckRedrawRoadWaypointCoverage(const Window *)
205 CheckRedrawWaypointCoverage<RoadWaypointTypeFilter>();
209 * Draw small boxes of cargo amount and ratings data at the given
210 * coordinates. If amount exceeds 576 units, it is shown 'full', same
211 * goes for the rating: at above 90% orso (224) it is also 'full'
213 * @param left left most coordinate to draw the box at
214 * @param right right most coordinate to draw the box at
215 * @param y coordinate to draw the box at
216 * @param type Cargo type
217 * @param amount Cargo amount
218 * @param rating ratings data for that particular cargo
220 static void StationsWndShowStationRating(int left, int right, int y, CargoID type, uint amount, uint8_t rating)
222 static const uint units_full = 576; ///< number of units to show station as 'full'
223 static const uint rating_full = 224; ///< rating needed so it is shown as 'full'
225 const CargoSpec *cs = CargoSpec::Get(type);
226 if (!cs->IsValid()) return;
228 int padding = ScaleGUITrad(1);
229 int width = right - left;
230 int colour = cs->rating_colour;
231 TextColour tc = GetContrastColour(colour);
232 uint w = std::min(amount + 5, units_full) * width / units_full;
234 int height = GetCharacterHeight(FS_SMALL) + padding - 1;
236 if (amount > 30) {
237 /* Draw total cargo (limited) on station */
238 GfxFillRect(left, y, left + w - 1, y + height, colour);
239 } else {
240 /* Draw a (scaled) one pixel-wide bar of additional cargo meter, useful
241 * for stations with only a small amount (<=30) */
242 uint rest = ScaleGUITrad(amount) / 5;
243 if (rest != 0) {
244 GfxFillRect(left, y + height - rest, left + padding - 1, y + height, colour);
248 DrawString(left + padding, right, y, cs->abbrev, tc, SA_CENTER, false, FS_SMALL);
250 /* Draw green/red ratings bar (fits under the waiting bar) */
251 y += height + padding + 1;
252 GfxFillRect(left + padding, y, right - padding - 1, y + padding - 1, PC_RED);
253 w = std::min<uint>(rating, rating_full) * (width - padding - padding) / rating_full;
254 if (w != 0) GfxFillRect(left + padding, y, left + w - 1, y + padding - 1, PC_GREEN);
257 typedef GUIList<const Station*, const CargoTypes &> GUIStationList;
260 * The list of stations per company.
262 class CompanyStationsWindow : public Window
264 protected:
265 /* Runtime saved values */
266 struct FilterState {
267 Listing last_sorting;
268 uint8_t facilities; ///< types of stations of interest
269 bool include_no_rating; ///< Whether we should include stations with no cargo rating.
270 CargoTypes cargoes; ///< bitmap of cargo types to include
273 static inline FilterState initial_state = {
274 {false, 0},
275 FACIL_TRAIN | FACIL_TRUCK_STOP | FACIL_BUS_STOP | FACIL_AIRPORT | FACIL_DOCK,
276 true,
277 ALL_CARGOTYPES,
280 /* Constants for sorting stations */
281 static inline const StringID sorter_names[] = {
282 STR_SORT_BY_NAME,
283 STR_SORT_BY_FACILITY,
284 STR_SORT_BY_WAITING_TOTAL,
285 STR_SORT_BY_WAITING_AVAILABLE,
286 STR_SORT_BY_RATING_MAX,
287 STR_SORT_BY_RATING_MIN,
289 static const std::initializer_list<GUIStationList::SortFunction * const> sorter_funcs;
291 FilterState filter;
292 GUIStationList stations{filter.cargoes};
293 Scrollbar *vscroll;
294 uint rating_width;
295 bool filter_expanded;
296 std::array<uint16_t, NUM_CARGO> stations_per_cargo_type; ///< Number of stations with a rating for each cargo type.
297 uint16_t stations_per_cargo_type_no_rating; ///< Number of stations without a rating.
300 * (Re)Build station list
302 * @param owner company whose stations are to be in list
304 void BuildStationsList(const Owner owner)
306 if (!this->stations.NeedRebuild()) return;
308 Debug(misc, 3, "Building station list for company {}", owner);
310 this->stations.clear();
311 this->stations_per_cargo_type.fill(0);
312 this->stations_per_cargo_type_no_rating = 0;
314 for (const Station *st : Station::Iterate()) {
315 if ((this->filter.facilities & st->facilities) != 0) { // only stations with selected facilities
316 if (st->owner == owner || (st->owner == OWNER_NONE && HasStationInUse(st->index, true, owner))) {
317 bool has_rating = false;
318 /* Add to the station/cargo counts. */
319 for (CargoID j = 0; j < NUM_CARGO; j++) {
320 if (st->goods[j].HasRating()) this->stations_per_cargo_type[j]++;
322 for (CargoID j = 0; j < NUM_CARGO; j++) {
323 if (st->goods[j].HasRating()) {
324 has_rating = true;
325 if (HasBit(this->filter.cargoes, j)) {
326 this->stations.push_back(st);
327 break;
331 /* Stations with no cargo rating. */
332 if (!has_rating) {
333 if (this->filter.include_no_rating) this->stations.push_back(st);
334 this->stations_per_cargo_type_no_rating++;
340 this->stations.RebuildDone();
342 this->vscroll->SetCount(this->stations.size()); // Update the scrollbar
345 /** Sort stations by their name */
346 static bool StationNameSorter(const Station * const &a, const Station * const &b, const CargoTypes &)
348 int r = StrNaturalCompare(a->GetCachedName(), b->GetCachedName()); // Sort by name (natural sorting).
349 if (r == 0) return a->index < b->index;
350 return r < 0;
353 /** Sort stations by their type */
354 static bool StationTypeSorter(const Station * const &a, const Station * const &b, const CargoTypes &)
356 return a->facilities < b->facilities;
359 /** Sort stations by their waiting cargo */
360 static bool StationWaitingTotalSorter(const Station * const &a, const Station * const &b, const CargoTypes &cargo_filter)
362 int diff = 0;
364 for (CargoID j : SetCargoBitIterator(cargo_filter)) {
365 diff += a->goods[j].cargo.TotalCount() - b->goods[j].cargo.TotalCount();
368 return diff < 0;
371 /** Sort stations by their available waiting cargo */
372 static bool StationWaitingAvailableSorter(const Station * const &a, const Station * const &b, const CargoTypes &cargo_filter)
374 int diff = 0;
376 for (CargoID j : SetCargoBitIterator(cargo_filter)) {
377 diff += a->goods[j].cargo.AvailableCount() - b->goods[j].cargo.AvailableCount();
380 return diff < 0;
383 /** Sort stations by their rating */
384 static bool StationRatingMaxSorter(const Station * const &a, const Station * const &b, const CargoTypes &cargo_filter)
386 uint8_t maxr1 = 0;
387 uint8_t maxr2 = 0;
389 for (CargoID j : SetCargoBitIterator(cargo_filter)) {
390 if (a->goods[j].HasRating()) maxr1 = std::max(maxr1, a->goods[j].rating);
391 if (b->goods[j].HasRating()) maxr2 = std::max(maxr2, b->goods[j].rating);
394 return maxr1 < maxr2;
397 /** Sort stations by their rating */
398 static bool StationRatingMinSorter(const Station * const &a, const Station * const &b, const CargoTypes &cargo_filter)
400 uint8_t minr1 = 255;
401 uint8_t minr2 = 255;
403 for (CargoID j : SetCargoBitIterator(cargo_filter)) {
404 if (a->goods[j].HasRating()) minr1 = std::min(minr1, a->goods[j].rating);
405 if (b->goods[j].HasRating()) minr2 = std::min(minr2, b->goods[j].rating);
408 return minr1 > minr2;
411 /** Sort the stations list */
412 void SortStationsList()
414 if (!this->stations.Sort()) return;
416 /* Set the modified widget dirty */
417 this->SetWidgetDirty(WID_STL_LIST);
420 public:
421 CompanyStationsWindow(WindowDesc &desc, WindowNumber window_number) : Window(desc)
423 /* Load initial filter state. */
424 this->filter = CompanyStationsWindow::initial_state;
425 if (this->filter.cargoes == ALL_CARGOTYPES) this->filter.cargoes = _cargo_mask;
427 this->stations.SetListing(this->filter.last_sorting);
428 this->stations.SetSortFuncs(CompanyStationsWindow::sorter_funcs);
429 this->stations.ForceRebuild();
430 this->stations.NeedResort();
431 this->SortStationsList();
433 this->CreateNestedTree();
434 this->vscroll = this->GetScrollbar(WID_STL_SCROLLBAR);
435 this->FinishInitNested(window_number);
436 this->owner = (Owner)this->window_number;
438 if (this->filter.cargoes == ALL_CARGOTYPES) this->filter.cargoes = _cargo_mask;
440 for (uint i = 0; i < 5; i++) {
441 if (HasBit(this->filter.facilities, i)) this->LowerWidget(i + WID_STL_TRAIN);
444 this->GetWidget<NWidgetCore>(WID_STL_SORTDROPBTN)->widget_data = CompanyStationsWindow::sorter_names[this->stations.SortType()];
447 ~CompanyStationsWindow()
449 /* Save filter state. */
450 this->filter.last_sorting = this->stations.GetListing();
451 CompanyStationsWindow::initial_state = this->filter;
454 void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override
456 switch (widget) {
457 case WID_STL_SORTBY: {
458 Dimension d = GetStringBoundingBox(this->GetWidget<NWidgetCore>(widget)->widget_data);
459 d.width += padding.width + Window::SortButtonWidth() * 2; // Doubled since the string is centred and it also looks better.
460 d.height += padding.height;
461 size = maxdim(size, d);
462 break;
465 case WID_STL_SORTDROPBTN: {
466 Dimension d = GetStringListBoundingBox(CompanyStationsWindow::sorter_names);
467 d.width += padding.width;
468 d.height += padding.height;
469 size = maxdim(size, d);
470 break;
473 case WID_STL_LIST:
474 resize.height = std::max(GetCharacterHeight(FS_NORMAL), GetCharacterHeight(FS_SMALL) + ScaleGUITrad(3));
475 size.height = padding.height + 5 * resize.height;
477 /* Determine appropriate width for mini station rating graph */
478 this->rating_width = 0;
479 for (const CargoSpec *cs : _sorted_standard_cargo_specs) {
480 this->rating_width = std::max(this->rating_width, GetStringBoundingBox(cs->abbrev, FS_SMALL).width);
482 /* Approximately match original 16 pixel wide rating bars by multiplying string width by 1.6 */
483 this->rating_width = this->rating_width * 16 / 10;
484 break;
488 void OnPaint() override
490 this->BuildStationsList((Owner)this->window_number);
491 this->SortStationsList();
493 this->DrawWidgets();
496 void DrawWidget(const Rect &r, WidgetID widget) const override
498 switch (widget) {
499 case WID_STL_SORTBY:
500 /* draw arrow pointing up/down for ascending/descending sorting */
501 this->DrawSortButtonState(WID_STL_SORTBY, this->stations.IsDescSortOrder() ? SBS_DOWN : SBS_UP);
502 break;
504 case WID_STL_LIST: {
505 bool rtl = _current_text_dir == TD_RTL;
506 Rect tr = r.Shrink(WidgetDimensions::scaled.framerect);
507 uint line_height = this->GetWidget<NWidgetBase>(widget)->resize_y;
508 /* Spacing between station name and first rating graph. */
509 int text_spacing = WidgetDimensions::scaled.hsep_wide;
510 /* Spacing between additional rating graphs. */
511 int rating_spacing = WidgetDimensions::scaled.hsep_normal;
513 auto [first, last] = this->vscroll->GetVisibleRangeIterators(this->stations);
514 for (auto it = first; it != last; ++it) {
515 const Station *st = *it;
516 assert(st->xy != INVALID_TILE);
518 /* Do not do the complex check HasStationInUse here, it may be even false
519 * when the order had been removed and the station list hasn't been removed yet */
520 assert(st->owner == owner || st->owner == OWNER_NONE);
522 SetDParam(0, st->index);
523 SetDParam(1, st->facilities);
524 int x = DrawString(tr.left, tr.right, tr.top + (line_height - GetCharacterHeight(FS_NORMAL)) / 2, STR_STATION_LIST_STATION);
525 x += rtl ? -text_spacing : text_spacing;
527 /* show cargo waiting and station ratings */
528 for (const CargoSpec *cs : _sorted_standard_cargo_specs) {
529 CargoID cid = cs->Index();
530 if (st->goods[cid].HasRating()) {
531 /* For RTL we work in exactly the opposite direction. So
532 * decrement the space needed first, then draw to the left
533 * instead of drawing to the left and then incrementing
534 * the space. */
535 if (rtl) {
536 x -= rating_width + rating_spacing;
537 if (x < tr.left) break;
539 StationsWndShowStationRating(x, x + rating_width, tr.top, cid, st->goods[cid].cargo.TotalCount(), st->goods[cid].rating);
540 if (!rtl) {
541 x += rating_width + rating_spacing;
542 if (x > tr.right) break;
546 tr.top += line_height;
549 if (this->vscroll->GetCount() == 0) { // company has no stations
550 DrawString(tr.left, tr.right, tr.top + (line_height - GetCharacterHeight(FS_NORMAL)) / 2, STR_STATION_LIST_NONE);
551 return;
553 break;
558 void SetStringParameters(WidgetID widget) const override
560 if (widget == WID_STL_CAPTION) {
561 SetDParam(0, this->window_number);
562 SetDParam(1, this->vscroll->GetCount());
565 if (widget == WID_STL_CARGODROPDOWN) {
566 if (this->filter.cargoes == 0) {
567 SetDParam(0, this->filter.include_no_rating ? STR_STATION_LIST_CARGO_FILTER_ONLY_NO_RATING : STR_STATION_LIST_CARGO_FILTER_NO_CARGO_TYPES);
568 } else if (this->filter.cargoes == _cargo_mask) {
569 SetDParam(0, this->filter.include_no_rating ? STR_STATION_LIST_CARGO_FILTER_ALL_AND_NO_RATING : STR_CARGO_TYPE_FILTER_ALL);
570 } else if (CountBits(this->filter.cargoes) == 1 && !this->filter.include_no_rating) {
571 SetDParam(0, CargoSpec::Get(FindFirstBit(this->filter.cargoes))->name);
572 } else {
573 SetDParam(0, STR_STATION_LIST_CARGO_FILTER_MULTIPLE);
578 DropDownList BuildCargoDropDownList(bool expanded) const
580 /* Define a custom item consisting of check mark, count string, icon and name string. */
581 using DropDownListCargoItem = DropDownCheck<DropDownString<DropDownListIconItem, FS_SMALL, true>>;
583 DropDownList list;
584 list.push_back(MakeDropDownListStringItem(STR_STATION_LIST_CARGO_FILTER_SELECT_ALL, CargoFilterCriteria::CF_SELECT_ALL));
585 list.push_back(MakeDropDownListDividerItem());
587 bool any_hidden = false;
589 uint16_t count = this->stations_per_cargo_type_no_rating;
590 if (count == 0 && !expanded) {
591 any_hidden = true;
592 } else {
593 list.push_back(std::make_unique<DropDownString<DropDownListCheckedItem, FS_SMALL, true>>(fmt::format("{}", count), this->filter.include_no_rating, STR_STATION_LIST_CARGO_FILTER_NO_RATING, CargoFilterCriteria::CF_NO_RATING, false, count == 0));
596 Dimension d = GetLargestCargoIconSize();
597 for (const CargoSpec *cs : _sorted_cargo_specs) {
598 count = this->stations_per_cargo_type[cs->Index()];
599 if (count == 0 && !expanded) {
600 any_hidden = true;
601 } else {
602 list.push_back(std::make_unique<DropDownListCargoItem>(HasBit(this->filter.cargoes, cs->Index()), fmt::format("{}", count), d, cs->GetCargoIcon(), PAL_NONE, cs->name, cs->Index(), false, count == 0));
606 if (!expanded && any_hidden) {
607 if (list.size() > 2) list.push_back(MakeDropDownListDividerItem());
608 list.push_back(MakeDropDownListStringItem(STR_STATION_LIST_CARGO_FILTER_EXPAND, CargoFilterCriteria::CF_EXPAND_LIST));
611 return list;
614 void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override
616 switch (widget) {
617 case WID_STL_LIST: {
618 auto it = this->vscroll->GetScrolledItemFromWidget(this->stations, pt.y, this, WID_STL_LIST, WidgetDimensions::scaled.framerect.top);
619 if (it == this->stations.end()) return; // click out of list bound
621 const Station *st = *it;
622 /* do not check HasStationInUse - it is slow and may be invalid */
623 assert(st->owner == (Owner)this->window_number || st->owner == OWNER_NONE);
625 if (_ctrl_pressed) {
626 ShowExtraViewportWindow(st->xy);
627 } else {
628 ScrollMainWindowToTile(st->xy);
630 break;
633 case WID_STL_TRAIN:
634 case WID_STL_TRUCK:
635 case WID_STL_BUS:
636 case WID_STL_AIRPLANE:
637 case WID_STL_SHIP:
638 if (_ctrl_pressed) {
639 ToggleBit(this->filter.facilities, widget - WID_STL_TRAIN);
640 this->ToggleWidgetLoweredState(widget);
641 } else {
642 for (uint i : SetBitIterator(this->filter.facilities)) {
643 this->RaiseWidget(i + WID_STL_TRAIN);
645 this->filter.facilities = 1 << (widget - WID_STL_TRAIN);
646 this->LowerWidget(widget);
648 this->stations.ForceRebuild();
649 this->SetDirty();
650 break;
652 case WID_STL_FACILALL:
653 for (WidgetID i = WID_STL_TRAIN; i <= WID_STL_SHIP; i++) {
654 this->LowerWidget(i);
657 this->filter.facilities = FACIL_TRAIN | FACIL_TRUCK_STOP | FACIL_BUS_STOP | FACIL_AIRPORT | FACIL_DOCK;
658 this->stations.ForceRebuild();
659 this->SetDirty();
660 break;
662 case WID_STL_SORTBY: // flip sorting method asc/desc
663 this->stations.ToggleSortOrder();
664 this->SetDirty();
665 break;
667 case WID_STL_SORTDROPBTN: // select sorting criteria dropdown menu
668 ShowDropDownMenu(this, CompanyStationsWindow::sorter_names, this->stations.SortType(), WID_STL_SORTDROPBTN, 0, 0);
669 break;
671 case WID_STL_CARGODROPDOWN:
672 this->filter_expanded = false;
673 ShowDropDownList(this, this->BuildCargoDropDownList(this->filter_expanded), -1, widget, 0, false, true);
674 break;
678 void OnDropdownSelect(int widget, int index) override
680 if (widget == WID_STL_SORTDROPBTN) {
681 if (this->stations.SortType() != index) {
682 this->stations.SetSortType(index);
684 /* Display the current sort variant */
685 this->GetWidget<NWidgetCore>(WID_STL_SORTDROPBTN)->widget_data = CompanyStationsWindow::sorter_names[this->stations.SortType()];
687 this->SetDirty();
691 if (widget == WID_STL_CARGODROPDOWN) {
692 FilterState oldstate = this->filter;
694 if (index >= 0 && index < NUM_CARGO) {
695 if (_ctrl_pressed) {
696 ToggleBit(this->filter.cargoes, index);
697 } else {
698 this->filter.cargoes = 1ULL << index;
699 this->filter.include_no_rating = false;
701 } else if (index == CargoFilterCriteria::CF_NO_RATING) {
702 if (_ctrl_pressed) {
703 this->filter.include_no_rating = !this->filter.include_no_rating;
704 } else {
705 this->filter.include_no_rating = true;
706 this->filter.cargoes = 0;
708 } else if (index == CargoFilterCriteria::CF_SELECT_ALL) {
709 this->filter.cargoes = _cargo_mask;
710 this->filter.include_no_rating = true;
711 } else if (index == CargoFilterCriteria::CF_EXPAND_LIST) {
712 this->filter_expanded = true;
713 ReplaceDropDownList(this, this->BuildCargoDropDownList(this->filter_expanded));
714 return;
717 if (oldstate.cargoes != this->filter.cargoes || oldstate.include_no_rating != this->filter.include_no_rating) {
718 this->stations.ForceRebuild();
719 this->SetDirty();
721 /* Only refresh the list if it's changed. */
722 if (_ctrl_pressed) ReplaceDropDownList(this, this->BuildCargoDropDownList(this->filter_expanded));
725 /* Always close the list if ctrl is not pressed. */
726 if (!_ctrl_pressed) this->CloseChildWindows(WC_DROPDOWN_MENU);
730 void OnGameTick() override
732 if (this->stations.NeedResort()) {
733 Debug(misc, 3, "Periodic rebuild station list company {}", this->window_number);
734 this->SetDirty();
738 void OnResize() override
740 this->vscroll->SetCapacityFromWidget(this, WID_STL_LIST, WidgetDimensions::scaled.framerect.Vertical());
744 * Some data on this window has become invalid.
745 * @param data Information about the changed data.
746 * @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details.
748 void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override
750 if (data == 0) {
751 /* This needs to be done in command-scope to enforce rebuilding before resorting invalid data */
752 this->stations.ForceRebuild();
753 } else {
754 this->stations.ForceResort();
759 /* Available station sorting functions */
760 const std::initializer_list<GUIStationList::SortFunction * const> CompanyStationsWindow::sorter_funcs = {
761 &StationNameSorter,
762 &StationTypeSorter,
763 &StationWaitingTotalSorter,
764 &StationWaitingAvailableSorter,
765 &StationRatingMaxSorter,
766 &StationRatingMinSorter
769 static constexpr NWidgetPart _nested_company_stations_widgets[] = {
770 NWidget(NWID_HORIZONTAL),
771 NWidget(WWT_CLOSEBOX, COLOUR_GREY),
772 NWidget(WWT_CAPTION, COLOUR_GREY, WID_STL_CAPTION), SetDataTip(STR_STATION_LIST_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
773 NWidget(WWT_SHADEBOX, COLOUR_GREY),
774 NWidget(WWT_DEFSIZEBOX, COLOUR_GREY),
775 NWidget(WWT_STICKYBOX, COLOUR_GREY),
776 EndContainer(),
777 NWidget(NWID_HORIZONTAL),
778 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_STL_TRAIN), SetAspect(WidgetDimensions::ASPECT_VEHICLE_ICON), SetDataTip(STR_TRAIN, STR_STATION_LIST_USE_CTRL_TO_SELECT_MORE), SetFill(0, 1),
779 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_STL_TRUCK), SetAspect(WidgetDimensions::ASPECT_VEHICLE_ICON), SetDataTip(STR_LORRY, STR_STATION_LIST_USE_CTRL_TO_SELECT_MORE), SetFill(0, 1),
780 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_STL_BUS), SetAspect(WidgetDimensions::ASPECT_VEHICLE_ICON), SetDataTip(STR_BUS, STR_STATION_LIST_USE_CTRL_TO_SELECT_MORE), SetFill(0, 1),
781 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_STL_SHIP), SetAspect(WidgetDimensions::ASPECT_VEHICLE_ICON), SetDataTip(STR_SHIP, STR_STATION_LIST_USE_CTRL_TO_SELECT_MORE), SetFill(0, 1),
782 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_STL_AIRPLANE), SetAspect(WidgetDimensions::ASPECT_VEHICLE_ICON), SetDataTip(STR_PLANE, STR_STATION_LIST_USE_CTRL_TO_SELECT_MORE), SetFill(0, 1),
783 NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_STL_FACILALL), SetAspect(WidgetDimensions::ASPECT_VEHICLE_ICON), SetDataTip(STR_ABBREV_ALL, STR_STATION_LIST_SELECT_ALL_FACILITIES), SetTextStyle(TC_BLACK, FS_SMALL), SetFill(0, 1),
784 NWidget(WWT_PANEL, COLOUR_GREY), SetMinimalSize(5, 0), SetFill(0, 1), EndContainer(),
785 NWidget(WWT_DROPDOWN, COLOUR_GREY, WID_STL_CARGODROPDOWN), SetFill(1, 0), SetDataTip(STR_JUST_STRING, STR_STATION_LIST_USE_CTRL_TO_SELECT_MORE),
786 NWidget(WWT_PANEL, COLOUR_GREY), SetResize(1, 0), SetFill(1, 1), EndContainer(),
787 EndContainer(),
788 NWidget(NWID_HORIZONTAL),
789 NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_STL_SORTBY), SetMinimalSize(81, 12), SetDataTip(STR_BUTTON_SORT_BY, STR_TOOLTIP_SORT_ORDER),
790 NWidget(WWT_DROPDOWN, COLOUR_GREY, WID_STL_SORTDROPBTN), SetMinimalSize(163, 12), SetDataTip(STR_SORT_BY_NAME, STR_TOOLTIP_SORT_CRITERIA), // widget_data gets overwritten.
791 NWidget(WWT_PANEL, COLOUR_GREY), SetResize(1, 0), SetFill(1, 1), EndContainer(),
792 EndContainer(),
793 NWidget(NWID_HORIZONTAL),
794 NWidget(WWT_PANEL, COLOUR_GREY, WID_STL_LIST), SetMinimalSize(346, 125), SetResize(1, 10), SetDataTip(0x0, STR_STATION_LIST_TOOLTIP), SetScrollbar(WID_STL_SCROLLBAR), EndContainer(),
795 NWidget(NWID_VERTICAL),
796 NWidget(NWID_VSCROLLBAR, COLOUR_GREY, WID_STL_SCROLLBAR),
797 NWidget(WWT_RESIZEBOX, COLOUR_GREY),
798 EndContainer(),
799 EndContainer(),
802 static WindowDesc _company_stations_desc(
803 WDP_AUTO, "list_stations", 358, 162,
804 WC_STATION_LIST, WC_NONE,
806 _nested_company_stations_widgets
810 * Opens window with list of company's stations
812 * @param company whose stations' list show
814 void ShowCompanyStations(CompanyID company)
816 if (!Company::IsValidID(company)) return;
818 AllocateWindowDescFront<CompanyStationsWindow>(_company_stations_desc, company);
821 static constexpr NWidgetPart _nested_station_view_widgets[] = {
822 NWidget(NWID_HORIZONTAL),
823 NWidget(WWT_CLOSEBOX, COLOUR_GREY),
824 NWidget(WWT_PUSHIMGBTN, COLOUR_GREY, WID_SV_RENAME), SetAspect(WidgetDimensions::ASPECT_RENAME), SetDataTip(SPR_RENAME, STR_STATION_VIEW_RENAME_TOOLTIP),
825 NWidget(WWT_CAPTION, COLOUR_GREY, WID_SV_CAPTION), SetDataTip(STR_STATION_VIEW_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
826 NWidget(WWT_PUSHIMGBTN, COLOUR_GREY, WID_SV_LOCATION), SetAspect(WidgetDimensions::ASPECT_LOCATION), SetDataTip(SPR_GOTO_LOCATION, STR_STATION_VIEW_CENTER_TOOLTIP),
827 NWidget(WWT_SHADEBOX, COLOUR_GREY),
828 NWidget(WWT_DEFSIZEBOX, COLOUR_GREY),
829 NWidget(WWT_STICKYBOX, COLOUR_GREY),
830 EndContainer(),
831 NWidget(NWID_HORIZONTAL),
832 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_SV_GROUP), SetMinimalSize(81, 12), SetFill(1, 1), SetDataTip(STR_STATION_VIEW_GROUP, 0x0),
833 NWidget(WWT_DROPDOWN, COLOUR_GREY, WID_SV_GROUP_BY), SetMinimalSize(168, 12), SetResize(1, 0), SetFill(0, 1), SetDataTip(0x0, STR_TOOLTIP_GROUP_ORDER),
834 EndContainer(),
835 NWidget(NWID_HORIZONTAL),
836 NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SV_SORT_ORDER), SetMinimalSize(81, 12), SetFill(1, 1), SetDataTip(STR_BUTTON_SORT_BY, STR_TOOLTIP_SORT_ORDER),
837 NWidget(WWT_DROPDOWN, COLOUR_GREY, WID_SV_SORT_BY), SetMinimalSize(168, 12), SetResize(1, 0), SetFill(0, 1), SetDataTip(0x0, STR_TOOLTIP_SORT_CRITERIA),
838 EndContainer(),
839 NWidget(NWID_HORIZONTAL),
840 NWidget(WWT_PANEL, COLOUR_GREY, WID_SV_WAITING), SetMinimalSize(237, 44), SetResize(1, 10), SetScrollbar(WID_SV_SCROLLBAR), EndContainer(),
841 NWidget(NWID_VSCROLLBAR, COLOUR_GREY, WID_SV_SCROLLBAR),
842 EndContainer(),
843 NWidget(WWT_PANEL, COLOUR_GREY, WID_SV_ACCEPT_RATING_LIST), SetMinimalSize(249, 23), SetResize(1, 0), EndContainer(),
844 NWidget(NWID_HORIZONTAL, NC_EQUALSIZE),
845 NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SV_ACCEPTS_RATINGS), SetMinimalSize(46, 12), SetResize(1, 0), SetFill(1, 1),
846 SetDataTip(STR_STATION_VIEW_RATINGS_BUTTON, STR_STATION_VIEW_RATINGS_TOOLTIP),
847 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_SV_CLOSE_AIRPORT), SetMinimalSize(45, 12), SetResize(1, 0), SetFill(1, 1),
848 SetDataTip(STR_STATION_VIEW_CLOSE_AIRPORT, STR_STATION_VIEW_CLOSE_AIRPORT_TOOLTIP),
849 NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_SV_CATCHMENT), SetMinimalSize(45, 12), SetResize(1, 0), SetFill(1, 1), SetDataTip(STR_BUTTON_CATCHMENT, STR_TOOLTIP_CATCHMENT),
850 NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SV_TRAINS), SetAspect(WidgetDimensions::ASPECT_VEHICLE_ICON), SetFill(0, 1), SetDataTip(STR_TRAIN, STR_STATION_VIEW_SCHEDULED_TRAINS_TOOLTIP),
851 NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SV_ROADVEHS), SetAspect(WidgetDimensions::ASPECT_VEHICLE_ICON), SetFill(0, 1), SetDataTip(STR_LORRY, STR_STATION_VIEW_SCHEDULED_ROAD_VEHICLES_TOOLTIP),
852 NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SV_SHIPS), SetAspect(WidgetDimensions::ASPECT_VEHICLE_ICON), SetFill(0, 1), SetDataTip(STR_SHIP, STR_STATION_VIEW_SCHEDULED_SHIPS_TOOLTIP),
853 NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SV_PLANES), SetAspect(WidgetDimensions::ASPECT_VEHICLE_ICON), SetFill(0, 1), SetDataTip(STR_PLANE, STR_STATION_VIEW_SCHEDULED_AIRCRAFT_TOOLTIP),
854 NWidget(WWT_RESIZEBOX, COLOUR_GREY),
855 EndContainer(),
859 * Draws icons of waiting cargo in the StationView window
861 * @param i type of cargo
862 * @param waiting number of waiting units
863 * @param left left most coordinate to draw on
864 * @param right right most coordinate to draw on
865 * @param y y coordinate
867 static void DrawCargoIcons(CargoID i, uint waiting, int left, int right, int y)
869 int width = ScaleSpriteTrad(10);
870 uint num = std::min<uint>((waiting + (width / 2)) / width, (right - left) / width); // maximum is width / 10 icons so it won't overflow
871 if (num == 0) return;
873 SpriteID sprite = CargoSpec::Get(i)->GetCargoIcon();
875 int x = _current_text_dir == TD_RTL ? left : right - num * width;
876 do {
877 DrawSprite(sprite, PAL_NONE, x, y);
878 x += width;
879 } while (--num);
882 enum SortOrder {
883 SO_DESCENDING,
884 SO_ASCENDING
887 class CargoDataEntry;
889 enum class CargoSortType : uint8_t {
890 AsGrouping, ///< by the same principle the entries are being grouped
891 Count, ///< by amount of cargo
892 StationString, ///< by station name
893 StationID, ///< by station id
894 CargoID, ///< by cargo id
897 class CargoSorter {
898 public:
899 CargoSorter(CargoSortType t = CargoSortType::StationID, SortOrder o = SO_ASCENDING) : type(t), order(o) {}
900 CargoSortType GetSortType() {return this->type;}
901 bool operator()(const CargoDataEntry *cd1, const CargoDataEntry *cd2) const;
903 private:
904 CargoSortType type;
905 SortOrder order;
907 template<class Tid>
908 bool SortId(Tid st1, Tid st2) const;
909 bool SortCount(const CargoDataEntry *cd1, const CargoDataEntry *cd2) const;
910 bool SortStation (StationID st1, StationID st2) const;
913 typedef std::set<CargoDataEntry *, CargoSorter> CargoDataSet;
916 * A cargo data entry representing one possible row in the station view window's
917 * top part. Cargo data entries form a tree where each entry can have several
918 * children. Parents keep track of the sums of their childrens' cargo counts.
920 class CargoDataEntry {
921 public:
922 CargoDataEntry();
923 ~CargoDataEntry();
926 * Insert a new child or retrieve an existing child using a station ID as ID.
927 * @param station ID of the station for which an entry shall be created or retrieved
928 * @return a child entry associated with the given station.
930 CargoDataEntry *InsertOrRetrieve(StationID station)
932 return this->InsertOrRetrieve<StationID>(station);
936 * Insert a new child or retrieve an existing child using a cargo ID as ID.
937 * @param cargo ID of the cargo for which an entry shall be created or retrieved
938 * @return a child entry associated with the given cargo.
940 CargoDataEntry *InsertOrRetrieve(CargoID cargo)
942 return this->InsertOrRetrieve<CargoID>(cargo);
945 void Update(uint count);
948 * Remove a child associated with the given station.
949 * @param station ID of the station for which the child should be removed.
951 void Remove(StationID station)
953 CargoDataEntry t(station);
954 this->Remove(&t);
958 * Remove a child associated with the given cargo.
959 * @param cargo ID of the cargo for which the child should be removed.
961 void Remove(CargoID cargo)
963 CargoDataEntry t(cargo);
964 this->Remove(&t);
968 * Retrieve a child for the given station. Return nullptr if it doesn't exist.
969 * @param station ID of the station the child we're looking for is associated with.
970 * @return a child entry for the given station or nullptr.
972 CargoDataEntry *Retrieve(StationID station) const
974 CargoDataEntry t(station);
975 return this->Retrieve(this->children->find(&t));
979 * Retrieve a child for the given cargo. Return nullptr if it doesn't exist.
980 * @param cargo ID of the cargo the child we're looking for is associated with.
981 * @return a child entry for the given cargo or nullptr.
983 CargoDataEntry *Retrieve(CargoID cargo) const
985 CargoDataEntry t(cargo);
986 return this->Retrieve(this->children->find(&t));
989 void Resort(CargoSortType type, SortOrder order);
992 * Get the station ID for this entry.
994 StationID GetStation() const { return this->station; }
997 * Get the cargo ID for this entry.
999 CargoID GetCargo() const { return this->cargo; }
1002 * Get the cargo count for this entry.
1004 uint GetCount() const { return this->count; }
1007 * Get the parent entry for this entry.
1009 CargoDataEntry *GetParent() const { return this->parent; }
1012 * Get the number of children for this entry.
1014 uint GetNumChildren() const { return this->num_children; }
1017 * Get an iterator pointing to the begin of the set of children.
1019 CargoDataSet::iterator Begin() const { return this->children->begin(); }
1022 * Get an iterator pointing to the end of the set of children.
1024 CargoDataSet::iterator End() const { return this->children->end(); }
1027 * Has this entry transfers.
1029 bool HasTransfers() const { return this->transfers; }
1032 * Set the transfers state.
1034 void SetTransfers(bool value) { this->transfers = value; }
1036 void Clear();
1037 private:
1039 CargoDataEntry(StationID st, uint c, CargoDataEntry *p);
1040 CargoDataEntry(CargoID car, uint c, CargoDataEntry *p);
1041 CargoDataEntry(StationID st);
1042 CargoDataEntry(CargoID car);
1044 CargoDataEntry *Retrieve(CargoDataSet::iterator i) const;
1046 template<class Tid>
1047 CargoDataEntry *InsertOrRetrieve(Tid s);
1049 void Remove(CargoDataEntry *comp);
1050 void IncrementSize();
1052 CargoDataEntry *parent; ///< the parent of this entry.
1053 const union {
1054 StationID station; ///< ID of the station this entry is associated with.
1055 struct {
1056 CargoID cargo; ///< ID of the cargo this entry is associated with.
1057 bool transfers; ///< If there are transfers for this cargo.
1060 uint num_children; ///< the number of subentries belonging to this entry.
1061 uint count; ///< sum of counts of all children or amount of cargo for this entry.
1062 CargoDataSet *children; ///< the children of this entry.
1065 CargoDataEntry::CargoDataEntry() :
1066 parent(nullptr),
1067 station(INVALID_STATION),
1068 num_children(0),
1069 count(0),
1070 children(new CargoDataSet(CargoSorter(CargoSortType::CargoID)))
1073 CargoDataEntry::CargoDataEntry(CargoID cargo, uint count, CargoDataEntry *parent) :
1074 parent(parent),
1075 cargo(cargo),
1076 num_children(0),
1077 count(count),
1078 children(new CargoDataSet)
1081 CargoDataEntry::CargoDataEntry(StationID station, uint count, CargoDataEntry *parent) :
1082 parent(parent),
1083 station(station),
1084 num_children(0),
1085 count(count),
1086 children(new CargoDataSet)
1089 CargoDataEntry::CargoDataEntry(StationID station) :
1090 parent(nullptr),
1091 station(station),
1092 num_children(0),
1093 count(0),
1094 children(nullptr)
1097 CargoDataEntry::CargoDataEntry(CargoID cargo) :
1098 parent(nullptr),
1099 cargo(cargo),
1100 num_children(0),
1101 count(0),
1102 children(nullptr)
1105 CargoDataEntry::~CargoDataEntry()
1107 this->Clear();
1108 delete this->children;
1112 * Delete all subentries, reset count and num_children and adapt parent's count.
1114 void CargoDataEntry::Clear()
1116 if (this->children != nullptr) {
1117 for (auto &it : *this->children) {
1118 assert(it != this);
1119 delete it;
1121 this->children->clear();
1123 if (this->parent != nullptr) this->parent->count -= this->count;
1124 this->count = 0;
1125 this->num_children = 0;
1129 * Remove a subentry from this one and delete it.
1130 * @param child the entry to be removed. This may also be a synthetic entry
1131 * which only contains the ID of the entry to be removed. In this case child is
1132 * not deleted.
1134 void CargoDataEntry::Remove(CargoDataEntry *child)
1136 CargoDataSet::iterator i = this->children->find(child);
1137 if (i != this->children->end()) {
1138 delete *i;
1139 this->children->erase(i);
1144 * Retrieve a subentry or insert it if it doesn't exist, yet.
1145 * @tparam ID type of ID: either StationID or CargoID
1146 * @param child_id ID of the child to be inserted or retrieved.
1147 * @return the new or retrieved subentry
1149 template<class Tid>
1150 CargoDataEntry *CargoDataEntry::InsertOrRetrieve(Tid child_id)
1152 CargoDataEntry tmp(child_id);
1153 CargoDataSet::iterator i = this->children->find(&tmp);
1154 if (i == this->children->end()) {
1155 IncrementSize();
1156 return *(this->children->insert(new CargoDataEntry(child_id, 0, this)).first);
1157 } else {
1158 CargoDataEntry *ret = *i;
1159 assert(this->children->value_comp().GetSortType() != CargoSortType::Count);
1160 return ret;
1165 * Update the count for this entry and propagate the change to the parent entry
1166 * if there is one.
1167 * @param count the amount to be added to this entry
1169 void CargoDataEntry::Update(uint count)
1171 this->count += count;
1172 if (this->parent != nullptr) this->parent->Update(count);
1176 * Increment
1178 void CargoDataEntry::IncrementSize()
1180 ++this->num_children;
1181 if (this->parent != nullptr) this->parent->IncrementSize();
1184 void CargoDataEntry::Resort(CargoSortType type, SortOrder order)
1186 CargoDataSet *new_subs = new CargoDataSet(this->children->begin(), this->children->end(), CargoSorter(type, order));
1187 delete this->children;
1188 this->children = new_subs;
1191 CargoDataEntry *CargoDataEntry::Retrieve(CargoDataSet::iterator i) const
1193 if (i == this->children->end()) {
1194 return nullptr;
1195 } else {
1196 assert(this->children->value_comp().GetSortType() != CargoSortType::Count);
1197 return *i;
1201 bool CargoSorter::operator()(const CargoDataEntry *cd1, const CargoDataEntry *cd2) const
1203 switch (this->type) {
1204 case CargoSortType::StationID:
1205 return this->SortId<StationID>(cd1->GetStation(), cd2->GetStation());
1206 case CargoSortType::CargoID:
1207 return this->SortId<CargoID>(cd1->GetCargo(), cd2->GetCargo());
1208 case CargoSortType::Count:
1209 return this->SortCount(cd1, cd2);
1210 case CargoSortType::StationString:
1211 return this->SortStation(cd1->GetStation(), cd2->GetStation());
1212 default:
1213 NOT_REACHED();
1217 template<class Tid>
1218 bool CargoSorter::SortId(Tid st1, Tid st2) const
1220 return (this->order == SO_ASCENDING) ? st1 < st2 : st2 < st1;
1223 bool CargoSorter::SortCount(const CargoDataEntry *cd1, const CargoDataEntry *cd2) const
1225 uint c1 = cd1->GetCount();
1226 uint c2 = cd2->GetCount();
1227 if (c1 == c2) {
1228 return this->SortStation(cd1->GetStation(), cd2->GetStation());
1229 } else if (this->order == SO_ASCENDING) {
1230 return c1 < c2;
1231 } else {
1232 return c2 < c1;
1236 bool CargoSorter::SortStation(StationID st1, StationID st2) const
1238 if (!Station::IsValidID(st1)) {
1239 return Station::IsValidID(st2) ? this->order == SO_ASCENDING : this->SortId(st1, st2);
1240 } else if (!Station::IsValidID(st2)) {
1241 return order == SO_DESCENDING;
1244 int res = StrNaturalCompare(Station::Get(st1)->GetCachedName(), Station::Get(st2)->GetCachedName()); // Sort by name (natural sorting).
1245 if (res == 0) {
1246 return this->SortId(st1, st2);
1247 } else {
1248 return (this->order == SO_ASCENDING) ? res < 0 : res > 0;
1253 * The StationView window
1255 struct StationViewWindow : public Window {
1257 * A row being displayed in the cargo view (as opposed to being "hidden" behind a plus sign).
1259 struct RowDisplay {
1260 RowDisplay(CargoDataEntry *f, StationID n) : filter(f), next_station(n) {}
1261 RowDisplay(CargoDataEntry *f, CargoID n) : filter(f), next_cargo(n) {}
1264 * Parent of the cargo entry belonging to the row.
1266 CargoDataEntry *filter;
1267 union {
1269 * ID of the station belonging to the entry actually displayed if it's to/from/via.
1271 StationID next_station;
1274 * ID of the cargo belonging to the entry actually displayed if it's cargo.
1276 CargoID next_cargo;
1280 typedef std::vector<RowDisplay> CargoDataVector;
1282 static const int NUM_COLUMNS = 4; ///< Number of "columns" in the cargo view: cargo, from, via, to
1285 * Type of data invalidation.
1287 enum Invalidation {
1288 INV_FLOWS = 0x100, ///< The planned flows have been recalculated and everything has to be updated.
1289 INV_CARGO = 0x200 ///< Some cargo has been added or removed.
1293 * Type of grouping used in each of the "columns".
1295 enum Grouping {
1296 GR_SOURCE, ///< Group by source of cargo ("from").
1297 GR_NEXT, ///< Group by next station ("via").
1298 GR_DESTINATION, ///< Group by estimated final destination ("to").
1299 GR_CARGO, ///< Group by cargo type.
1303 * Display mode of the cargo view.
1305 enum Mode {
1306 MODE_WAITING, ///< Show cargo waiting at the station.
1307 MODE_PLANNED ///< Show cargo planned to pass through the station.
1310 uint expand_shrink_width; ///< The width allocated to the expand/shrink 'button'
1311 int rating_lines; ///< Number of lines in the cargo ratings view.
1312 int accepts_lines; ///< Number of lines in the accepted cargo view.
1313 Scrollbar *vscroll;
1315 /* Height of the #WID_SV_ACCEPT_RATING_LIST widget for different views. */
1316 static constexpr uint RATING_LINES = 13; ///< Height in lines of the cargo ratings view.
1317 static constexpr uint ACCEPTS_LINES = 3; ///< Height in lines of the accepted cargo view.
1319 /** Names of the sorting options in the dropdown. */
1320 static inline const StringID sort_names[] = {
1321 STR_STATION_VIEW_WAITING_STATION,
1322 STR_STATION_VIEW_WAITING_AMOUNT,
1323 STR_STATION_VIEW_PLANNED_STATION,
1324 STR_STATION_VIEW_PLANNED_AMOUNT,
1326 /** Names of the grouping options in the dropdown. */
1327 static inline const StringID group_names[] = {
1328 STR_STATION_VIEW_GROUP_S_V_D,
1329 STR_STATION_VIEW_GROUP_S_D_V,
1330 STR_STATION_VIEW_GROUP_V_S_D,
1331 STR_STATION_VIEW_GROUP_V_D_S,
1332 STR_STATION_VIEW_GROUP_D_S_V,
1333 STR_STATION_VIEW_GROUP_D_V_S,
1337 * Sort types of the different 'columns'.
1338 * In fact only CargoSortType::Count and CargoSortType::AsGrouping are active and you can only
1339 * sort all the columns in the same way. The other options haven't been
1340 * included in the GUI due to lack of space.
1342 CargoSortType sortings[NUM_COLUMNS];
1344 /** Sort order (ascending/descending) for the 'columns'. */
1345 SortOrder sort_orders[NUM_COLUMNS];
1347 int scroll_to_row; ///< If set, scroll the main viewport to the station pointed to by this row.
1348 int grouping_index; ///< Currently selected entry in the grouping drop down.
1349 Mode current_mode; ///< Currently selected display mode of cargo view.
1350 Grouping groupings[NUM_COLUMNS]; ///< Grouping modes for the different columns.
1352 CargoDataEntry expanded_rows; ///< Parent entry of currently expanded rows.
1353 CargoDataEntry cached_destinations; ///< Cache for the flows passing through this station.
1354 CargoDataVector displayed_rows; ///< Parent entry of currently displayed rows (including collapsed ones).
1356 StationViewWindow(WindowDesc &desc, WindowNumber window_number) : Window(desc),
1357 scroll_to_row(INT_MAX), grouping_index(0)
1359 this->rating_lines = RATING_LINES;
1360 this->accepts_lines = ACCEPTS_LINES;
1362 this->CreateNestedTree();
1363 this->vscroll = this->GetScrollbar(WID_SV_SCROLLBAR);
1364 /* Nested widget tree creation is done in two steps to ensure that this->GetWidget<NWidgetCore>(WID_SV_ACCEPTS_RATINGS) exists in UpdateWidgetSize(). */
1365 this->FinishInitNested(window_number);
1367 this->groupings[0] = GR_CARGO;
1368 this->sortings[0] = CargoSortType::AsGrouping;
1369 this->SelectGroupBy(_settings_client.gui.station_gui_group_order);
1370 this->SelectSortBy(_settings_client.gui.station_gui_sort_by);
1371 this->sort_orders[0] = SO_ASCENDING;
1372 this->SelectSortOrder((SortOrder)_settings_client.gui.station_gui_sort_order);
1373 this->owner = Station::Get(window_number)->owner;
1376 void Close([[maybe_unused]] int data = 0) override
1378 CloseWindowById(WC_TRAINS_LIST, VehicleListIdentifier(VL_STATION_LIST, VEH_TRAIN, this->owner, this->window_number).Pack(), false);
1379 CloseWindowById(WC_ROADVEH_LIST, VehicleListIdentifier(VL_STATION_LIST, VEH_ROAD, this->owner, this->window_number).Pack(), false);
1380 CloseWindowById(WC_SHIPS_LIST, VehicleListIdentifier(VL_STATION_LIST, VEH_SHIP, this->owner, this->window_number).Pack(), false);
1381 CloseWindowById(WC_AIRCRAFT_LIST, VehicleListIdentifier(VL_STATION_LIST, VEH_AIRCRAFT, this->owner, this->window_number).Pack(), false);
1383 SetViewportCatchmentStation(Station::Get(this->window_number), false);
1384 this->Window::Close();
1388 * Show a certain cargo entry characterized by source/next/dest station, cargo ID and amount of cargo at the
1389 * right place in the cargo view. I.e. update as many rows as are expanded following that characterization.
1390 * @param data Root entry of the tree.
1391 * @param cargo Cargo ID of the entry to be shown.
1392 * @param source Source station of the entry to be shown.
1393 * @param next Next station the cargo to be shown will visit.
1394 * @param dest Final destination of the cargo to be shown.
1395 * @param count Amount of cargo to be shown.
1397 void ShowCargo(CargoDataEntry *data, CargoID cargo, StationID source, StationID next, StationID dest, uint count)
1399 if (count == 0) return;
1400 bool auto_distributed = _settings_game.linkgraph.GetDistributionType(cargo) != DT_MANUAL;
1401 const CargoDataEntry *expand = &this->expanded_rows;
1402 for (int i = 0; i < NUM_COLUMNS && expand != nullptr; ++i) {
1403 switch (groupings[i]) {
1404 case GR_CARGO:
1405 assert(i == 0);
1406 data = data->InsertOrRetrieve(cargo);
1407 data->SetTransfers(source != this->window_number);
1408 expand = expand->Retrieve(cargo);
1409 break;
1410 case GR_SOURCE:
1411 if (auto_distributed || source != this->window_number) {
1412 data = data->InsertOrRetrieve(source);
1413 expand = expand->Retrieve(source);
1415 break;
1416 case GR_NEXT:
1417 if (auto_distributed) {
1418 data = data->InsertOrRetrieve(next);
1419 expand = expand->Retrieve(next);
1421 break;
1422 case GR_DESTINATION:
1423 if (auto_distributed) {
1424 data = data->InsertOrRetrieve(dest);
1425 expand = expand->Retrieve(dest);
1427 break;
1430 data->Update(count);
1433 void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override
1435 switch (widget) {
1436 case WID_SV_WAITING:
1437 resize.height = GetCharacterHeight(FS_NORMAL);
1438 size.height = 4 * resize.height + padding.height;
1439 this->expand_shrink_width = std::max(GetStringBoundingBox("-").width, GetStringBoundingBox("+").width);
1440 break;
1442 case WID_SV_ACCEPT_RATING_LIST:
1443 size.height = ((this->GetWidget<NWidgetCore>(WID_SV_ACCEPTS_RATINGS)->widget_data == STR_STATION_VIEW_RATINGS_BUTTON) ? this->accepts_lines : this->rating_lines) * GetCharacterHeight(FS_NORMAL) + padding.height;
1444 break;
1446 case WID_SV_CLOSE_AIRPORT:
1447 if (!(Station::Get(this->window_number)->facilities & FACIL_AIRPORT)) {
1448 /* Hide 'Close Airport' button if no airport present. */
1449 size.width = 0;
1450 resize.width = 0;
1451 fill.width = 0;
1453 break;
1457 void OnPaint() override
1459 const Station *st = Station::Get(this->window_number);
1460 CargoDataEntry cargo;
1461 BuildCargoList(&cargo, st);
1463 this->vscroll->SetCount(cargo.GetNumChildren()); // update scrollbar
1465 /* disable some buttons */
1466 this->SetWidgetDisabledState(WID_SV_RENAME, st->owner != _local_company);
1467 this->SetWidgetDisabledState(WID_SV_TRAINS, !(st->facilities & FACIL_TRAIN));
1468 this->SetWidgetDisabledState(WID_SV_ROADVEHS, !(st->facilities & FACIL_TRUCK_STOP) && !(st->facilities & FACIL_BUS_STOP));
1469 this->SetWidgetDisabledState(WID_SV_SHIPS, !(st->facilities & FACIL_DOCK));
1470 this->SetWidgetDisabledState(WID_SV_PLANES, !(st->facilities & FACIL_AIRPORT));
1471 this->SetWidgetDisabledState(WID_SV_CLOSE_AIRPORT, !(st->facilities & FACIL_AIRPORT) || st->owner != _local_company || st->owner == OWNER_NONE); // Also consider SE, where _local_company == OWNER_NONE
1472 this->SetWidgetLoweredState(WID_SV_CLOSE_AIRPORT, (st->facilities & FACIL_AIRPORT) && (st->airport.flags & AIRPORT_CLOSED_block) != 0);
1474 extern const Station *_viewport_highlight_station;
1475 this->SetWidgetDisabledState(WID_SV_CATCHMENT, st->facilities == FACIL_NONE);
1476 this->SetWidgetLoweredState(WID_SV_CATCHMENT, _viewport_highlight_station == st);
1478 this->DrawWidgets();
1480 if (!this->IsShaded()) {
1481 /* Draw 'accepted cargo' or 'cargo ratings'. */
1482 const NWidgetBase *wid = this->GetWidget<NWidgetBase>(WID_SV_ACCEPT_RATING_LIST);
1483 const Rect r = wid->GetCurrentRect();
1484 if (this->GetWidget<NWidgetCore>(WID_SV_ACCEPTS_RATINGS)->widget_data == STR_STATION_VIEW_RATINGS_BUTTON) {
1485 int lines = this->DrawAcceptedCargo(r);
1486 if (lines > this->accepts_lines) { // Resize the widget, and perform re-initialization of the window.
1487 this->accepts_lines = lines;
1488 this->ReInit();
1489 return;
1491 } else {
1492 int lines = this->DrawCargoRatings(r);
1493 if (lines > this->rating_lines) { // Resize the widget, and perform re-initialization of the window.
1494 this->rating_lines = lines;
1495 this->ReInit();
1496 return;
1500 /* Draw arrow pointing up/down for ascending/descending sorting */
1501 this->DrawSortButtonState(WID_SV_SORT_ORDER, sort_orders[1] == SO_ASCENDING ? SBS_UP : SBS_DOWN);
1503 int pos = this->vscroll->GetPosition();
1505 int maxrows = this->vscroll->GetCapacity();
1507 displayed_rows.clear();
1509 /* Draw waiting cargo. */
1510 NWidgetBase *nwi = this->GetWidget<NWidgetBase>(WID_SV_WAITING);
1511 Rect waiting_rect = nwi->GetCurrentRect().Shrink(WidgetDimensions::scaled.framerect);
1512 this->DrawEntries(&cargo, waiting_rect, pos, maxrows, 0);
1513 scroll_to_row = INT_MAX;
1517 void SetStringParameters(WidgetID widget) const override
1519 if (widget == WID_SV_CAPTION) {
1520 const Station *st = Station::Get(this->window_number);
1521 SetDParam(0, st->index);
1522 SetDParam(1, st->facilities);
1527 * Rebuild the cache for estimated destinations which is used to quickly show the "destination" entries
1528 * even if we actually don't know the destination of a certain packet from just looking at it.
1529 * @param i Cargo to recalculate the cache for.
1531 void RecalcDestinations(CargoID i)
1533 const Station *st = Station::Get(this->window_number);
1534 CargoDataEntry *cargo_entry = cached_destinations.InsertOrRetrieve(i);
1535 cargo_entry->Clear();
1537 for (const auto &it : st->goods[i].flows) {
1538 StationID from = it.first;
1539 CargoDataEntry *source_entry = cargo_entry->InsertOrRetrieve(from);
1540 uint32_t prev_count = 0;
1541 for (const auto &flow_it : *it.second.GetShares()) {
1542 StationID via = flow_it.second;
1543 CargoDataEntry *via_entry = source_entry->InsertOrRetrieve(via);
1544 if (via == this->window_number) {
1545 via_entry->InsertOrRetrieve(via)->Update(flow_it.first - prev_count);
1546 } else {
1547 EstimateDestinations(i, from, via, flow_it.first - prev_count, via_entry);
1549 prev_count = flow_it.first;
1555 * Estimate the amounts of cargo per final destination for a given cargo, source station and next hop and
1556 * save the result as children of the given CargoDataEntry.
1557 * @param cargo ID of the cargo to estimate destinations for.
1558 * @param source Source station of the given batch of cargo.
1559 * @param next Intermediate hop to start the calculation at ("next hop").
1560 * @param count Size of the batch of cargo.
1561 * @param dest CargoDataEntry to save the results in.
1563 void EstimateDestinations(CargoID cargo, StationID source, StationID next, uint count, CargoDataEntry *dest)
1565 if (Station::IsValidID(next) && Station::IsValidID(source)) {
1566 CargoDataEntry tmp;
1567 const FlowStatMap &flowmap = Station::Get(next)->goods[cargo].flows;
1568 FlowStatMap::const_iterator map_it = flowmap.find(source);
1569 if (map_it != flowmap.end()) {
1570 const FlowStat::SharesMap *shares = map_it->second.GetShares();
1571 uint32_t prev_count = 0;
1572 for (FlowStat::SharesMap::const_iterator i = shares->begin(); i != shares->end(); ++i) {
1573 tmp.InsertOrRetrieve(i->second)->Update(i->first - prev_count);
1574 prev_count = i->first;
1578 if (tmp.GetCount() == 0) {
1579 dest->InsertOrRetrieve(INVALID_STATION)->Update(count);
1580 } else {
1581 uint sum_estimated = 0;
1582 while (sum_estimated < count) {
1583 for (CargoDataSet::iterator i = tmp.Begin(); i != tmp.End() && sum_estimated < count; ++i) {
1584 CargoDataEntry *child = *i;
1585 uint estimate = DivideApprox(child->GetCount() * count, tmp.GetCount());
1586 if (estimate == 0) estimate = 1;
1588 sum_estimated += estimate;
1589 if (sum_estimated > count) {
1590 estimate -= sum_estimated - count;
1591 sum_estimated = count;
1594 if (estimate > 0) {
1595 if (child->GetStation() == next) {
1596 dest->InsertOrRetrieve(next)->Update(estimate);
1597 } else {
1598 EstimateDestinations(cargo, source, child->GetStation(), estimate, dest);
1605 } else {
1606 dest->InsertOrRetrieve(INVALID_STATION)->Update(count);
1611 * Build up the cargo view for PLANNED mode and a specific cargo.
1612 * @param i Cargo to show.
1613 * @param flows The current station's flows for that cargo.
1614 * @param cargo The CargoDataEntry to save the results in.
1616 void BuildFlowList(CargoID i, const FlowStatMap &flows, CargoDataEntry *cargo)
1618 const CargoDataEntry *source_dest = this->cached_destinations.Retrieve(i);
1619 for (FlowStatMap::const_iterator it = flows.begin(); it != flows.end(); ++it) {
1620 StationID from = it->first;
1621 const CargoDataEntry *source_entry = source_dest->Retrieve(from);
1622 const FlowStat::SharesMap *shares = it->second.GetShares();
1623 for (FlowStat::SharesMap::const_iterator flow_it = shares->begin(); flow_it != shares->end(); ++flow_it) {
1624 const CargoDataEntry *via_entry = source_entry->Retrieve(flow_it->second);
1625 for (CargoDataSet::iterator dest_it = via_entry->Begin(); dest_it != via_entry->End(); ++dest_it) {
1626 CargoDataEntry *dest_entry = *dest_it;
1627 ShowCargo(cargo, i, from, flow_it->second, dest_entry->GetStation(), dest_entry->GetCount());
1634 * Build up the cargo view for WAITING mode and a specific cargo.
1635 * @param i Cargo to show.
1636 * @param packets The current station's cargo list for that cargo.
1637 * @param cargo The CargoDataEntry to save the result in.
1639 void BuildCargoList(CargoID i, const StationCargoList &packets, CargoDataEntry *cargo)
1641 const CargoDataEntry *source_dest = this->cached_destinations.Retrieve(i);
1642 for (StationCargoList::ConstIterator it = packets.Packets()->begin(); it != packets.Packets()->end(); it++) {
1643 const CargoPacket *cp = *it;
1644 StationID next = it.GetKey();
1646 const CargoDataEntry *source_entry = source_dest->Retrieve(cp->GetFirstStation());
1647 if (source_entry == nullptr) {
1648 this->ShowCargo(cargo, i, cp->GetFirstStation(), next, INVALID_STATION, cp->Count());
1649 continue;
1652 const CargoDataEntry *via_entry = source_entry->Retrieve(next);
1653 if (via_entry == nullptr) {
1654 this->ShowCargo(cargo, i, cp->GetFirstStation(), next, INVALID_STATION, cp->Count());
1655 continue;
1658 uint remaining = cp->Count();
1659 for (CargoDataSet::iterator dest_it = via_entry->Begin(); dest_it != via_entry->End();) {
1660 CargoDataEntry *dest_entry = *dest_it;
1662 /* Advance iterator here instead of in the for statement to test whether this is the last entry */
1663 ++dest_it;
1665 uint val;
1666 if (dest_it == via_entry->End()) {
1667 /* Allocate all remaining waiting cargo to the last destination to avoid
1668 * waiting cargo being "lost", and the displayed total waiting cargo
1669 * not matching GoodsEntry::TotalCount() */
1670 val = remaining;
1671 } else {
1672 val = std::min<uint>(remaining, DivideApprox(cp->Count() * dest_entry->GetCount(), via_entry->GetCount()));
1673 remaining -= val;
1675 this->ShowCargo(cargo, i, cp->GetFirstStation(), next, dest_entry->GetStation(), val);
1678 this->ShowCargo(cargo, i, NEW_STATION, NEW_STATION, NEW_STATION, packets.ReservedCount());
1682 * Build up the cargo view for all cargoes.
1683 * @param cargo The root cargo entry to save all results in.
1684 * @param st The station to calculate the cargo view from.
1686 void BuildCargoList(CargoDataEntry *cargo, const Station *st)
1688 for (CargoID i = 0; i < NUM_CARGO; i++) {
1690 if (this->cached_destinations.Retrieve(i) == nullptr) {
1691 this->RecalcDestinations(i);
1694 if (this->current_mode == MODE_WAITING) {
1695 this->BuildCargoList(i, st->goods[i].cargo, cargo);
1696 } else {
1697 this->BuildFlowList(i, st->goods[i].flows, cargo);
1703 * Mark a specific row, characterized by its CargoDataEntry, as expanded.
1704 * @param data The row to be marked as expanded.
1706 void SetDisplayedRow(const CargoDataEntry *data)
1708 std::list<StationID> stations;
1709 const CargoDataEntry *parent = data->GetParent();
1710 if (parent->GetParent() == nullptr) {
1711 this->displayed_rows.push_back(RowDisplay(&this->expanded_rows, data->GetCargo()));
1712 return;
1715 StationID next = data->GetStation();
1716 while (parent->GetParent()->GetParent() != nullptr) {
1717 stations.push_back(parent->GetStation());
1718 parent = parent->GetParent();
1721 CargoID cargo = parent->GetCargo();
1722 CargoDataEntry *filter = this->expanded_rows.Retrieve(cargo);
1723 while (!stations.empty()) {
1724 filter = filter->Retrieve(stations.back());
1725 stations.pop_back();
1728 this->displayed_rows.push_back(RowDisplay(filter, next));
1732 * Select the correct string for an entry referring to the specified station.
1733 * @param station Station the entry is showing cargo for.
1734 * @param here String to be shown if the entry refers to the same station as this station GUI belongs to.
1735 * @param other_station String to be shown if the entry refers to a specific other station.
1736 * @param any String to be shown if the entry refers to "any station".
1737 * @return One of the three given strings or STR_STATION_VIEW_RESERVED, depending on what station the entry refers to.
1739 StringID GetEntryString(StationID station, StringID here, StringID other_station, StringID any)
1741 if (station == this->window_number) {
1742 return here;
1743 } else if (station == INVALID_STATION) {
1744 return any;
1745 } else if (station == NEW_STATION) {
1746 return STR_STATION_VIEW_RESERVED;
1747 } else {
1748 SetDParam(2, station);
1749 return other_station;
1754 * Determine if we need to show the special "non-stop" string.
1755 * @param cd Entry we are going to show.
1756 * @param station Station the entry refers to.
1757 * @param column The "column" the entry will be shown in.
1758 * @return either STR_STATION_VIEW_VIA or STR_STATION_VIEW_NONSTOP.
1760 StringID SearchNonStop(CargoDataEntry *cd, StationID station, int column)
1762 CargoDataEntry *parent = cd->GetParent();
1763 for (int i = column - 1; i > 0; --i) {
1764 if (this->groupings[i] == GR_DESTINATION) {
1765 if (parent->GetStation() == station) {
1766 return STR_STATION_VIEW_NONSTOP;
1767 } else {
1768 return STR_STATION_VIEW_VIA;
1771 parent = parent->GetParent();
1774 if (this->groupings[column + 1] == GR_DESTINATION) {
1775 CargoDataSet::iterator begin = cd->Begin();
1776 CargoDataSet::iterator end = cd->End();
1777 if (begin != end && ++(cd->Begin()) == end && (*(begin))->GetStation() == station) {
1778 return STR_STATION_VIEW_NONSTOP;
1779 } else {
1780 return STR_STATION_VIEW_VIA;
1784 return STR_STATION_VIEW_VIA;
1788 * Draw the given cargo entries in the station GUI.
1789 * @param entry Root entry for all cargo to be drawn.
1790 * @param r Screen rectangle to draw into.
1791 * @param pos Current row to be drawn to (counted down from 0 to -maxrows, same as vscroll->GetPosition()).
1792 * @param maxrows Maximum row to be drawn.
1793 * @param column Current "column" being drawn.
1794 * @param cargo Current cargo being drawn (if cargo column has been passed).
1795 * @return row (in "pos" counting) after the one we have last drawn to.
1797 int DrawEntries(CargoDataEntry *entry, const Rect &r, int pos, int maxrows, int column, CargoID cargo = INVALID_CARGO)
1799 if (this->sortings[column] == CargoSortType::AsGrouping) {
1800 if (this->groupings[column] != GR_CARGO) {
1801 entry->Resort(CargoSortType::StationString, this->sort_orders[column]);
1803 } else {
1804 entry->Resort(CargoSortType::Count, this->sort_orders[column]);
1806 for (CargoDataSet::iterator i = entry->Begin(); i != entry->End(); ++i) {
1807 CargoDataEntry *cd = *i;
1809 Grouping grouping = this->groupings[column];
1810 if (grouping == GR_CARGO) cargo = cd->GetCargo();
1811 bool auto_distributed = _settings_game.linkgraph.GetDistributionType(cargo) != DT_MANUAL;
1813 if (pos > -maxrows && pos <= 0) {
1814 StringID str = STR_EMPTY;
1815 int y = r.top - pos * GetCharacterHeight(FS_NORMAL);
1816 SetDParam(0, cargo);
1817 SetDParam(1, cd->GetCount());
1819 if (this->groupings[column] == GR_CARGO) {
1820 str = STR_STATION_VIEW_WAITING_CARGO;
1821 DrawCargoIcons(cd->GetCargo(), cd->GetCount(), r.left + this->expand_shrink_width, r.right - this->expand_shrink_width, y);
1822 } else {
1823 if (!auto_distributed) grouping = GR_SOURCE;
1824 StationID station = cd->GetStation();
1826 switch (grouping) {
1827 case GR_SOURCE:
1828 str = this->GetEntryString(station, STR_STATION_VIEW_FROM_HERE, STR_STATION_VIEW_FROM, STR_STATION_VIEW_FROM_ANY);
1829 break;
1830 case GR_NEXT:
1831 str = this->GetEntryString(station, STR_STATION_VIEW_VIA_HERE, STR_STATION_VIEW_VIA, STR_STATION_VIEW_VIA_ANY);
1832 if (str == STR_STATION_VIEW_VIA) str = this->SearchNonStop(cd, station, column);
1833 break;
1834 case GR_DESTINATION:
1835 str = this->GetEntryString(station, STR_STATION_VIEW_TO_HERE, STR_STATION_VIEW_TO, STR_STATION_VIEW_TO_ANY);
1836 break;
1837 default:
1838 NOT_REACHED();
1840 if (pos == -this->scroll_to_row && Station::IsValidID(station)) {
1841 ScrollMainWindowToTile(Station::Get(station)->xy);
1845 bool rtl = _current_text_dir == TD_RTL;
1846 Rect text = r.Indent(column * WidgetDimensions::scaled.hsep_indent, rtl).Indent(this->expand_shrink_width, !rtl);
1847 Rect shrink = r.WithWidth(this->expand_shrink_width, !rtl);
1849 DrawString(text.left, text.right, y, str);
1851 if (column < NUM_COLUMNS - 1) {
1852 const char *sym = nullptr;
1853 if (cd->GetNumChildren() > 0) {
1854 sym = "-";
1855 } else if (auto_distributed && str != STR_STATION_VIEW_RESERVED) {
1856 sym = "+";
1857 } else {
1858 /* Only draw '+' if there is something to be shown. */
1859 const StationCargoList &list = Station::Get(this->window_number)->goods[cargo].cargo;
1860 if (grouping == GR_CARGO && (list.ReservedCount() > 0 || cd->HasTransfers())) {
1861 sym = "+";
1864 if (sym != nullptr) DrawString(shrink.left, shrink.right, y, sym, TC_YELLOW);
1866 this->SetDisplayedRow(cd);
1868 --pos;
1869 if (auto_distributed || column == 0) {
1870 pos = this->DrawEntries(cd, r, pos, maxrows, column + 1, cargo);
1873 return pos;
1877 * Draw accepted cargo in the #WID_SV_ACCEPT_RATING_LIST widget.
1878 * @param r Rectangle of the widget.
1879 * @return Number of lines needed for drawing the accepted cargo.
1881 int DrawAcceptedCargo(const Rect &r) const
1883 const Station *st = Station::Get(this->window_number);
1884 Rect tr = r.Shrink(WidgetDimensions::scaled.framerect);
1886 SetDParam(0, GetAcceptanceMask(st));
1887 int bottom = DrawStringMultiLine(tr.left, tr.right, tr.top, INT32_MAX, STR_STATION_VIEW_ACCEPTS_CARGO);
1888 return CeilDiv(bottom - r.top - WidgetDimensions::scaled.framerect.top, GetCharacterHeight(FS_NORMAL));
1892 * Draw cargo ratings in the #WID_SV_ACCEPT_RATING_LIST widget.
1893 * @param r Rectangle of the widget.
1894 * @return Number of lines needed for drawing the cargo ratings.
1896 int DrawCargoRatings(const Rect &r) const
1898 const Station *st = Station::Get(this->window_number);
1899 bool rtl = _current_text_dir == TD_RTL;
1900 Rect tr = r.Shrink(WidgetDimensions::scaled.framerect);
1902 if (st->town->exclusive_counter > 0) {
1903 SetDParam(0, st->town->exclusivity);
1904 tr.top = DrawStringMultiLine(tr, st->town->exclusivity == st->owner ? STR_STATION_VIEW_EXCLUSIVE_RIGHTS_SELF : STR_STATION_VIEW_EXCLUSIVE_RIGHTS_COMPANY);
1905 tr.top += WidgetDimensions::scaled.vsep_wide;
1908 DrawString(tr, TimerGameEconomy::UsingWallclockUnits() ? STR_STATION_VIEW_SUPPLY_RATINGS_TITLE_MINUTE : STR_STATION_VIEW_SUPPLY_RATINGS_TITLE_MONTH);
1909 tr.top += GetCharacterHeight(FS_NORMAL);
1911 for (const CargoSpec *cs : _sorted_standard_cargo_specs) {
1912 const GoodsEntry *ge = &st->goods[cs->Index()];
1913 if (!ge->HasRating()) continue;
1915 const LinkGraph *lg = LinkGraph::GetIfValid(ge->link_graph);
1916 SetDParam(0, cs->name);
1917 SetDParam(1, lg != nullptr ? lg->Monthly((*lg)[ge->node].supply) : 0);
1918 SetDParam(2, STR_CARGO_RATING_APPALLING + (ge->rating >> 5));
1919 SetDParam(3, ToPercent8(ge->rating));
1920 DrawString(tr.Indent(WidgetDimensions::scaled.hsep_indent, rtl), STR_STATION_VIEW_CARGO_SUPPLY_RATING);
1921 tr.top += GetCharacterHeight(FS_NORMAL);
1923 return CeilDiv(tr.top - r.top - WidgetDimensions::scaled.framerect.top, GetCharacterHeight(FS_NORMAL));
1927 * Expand or collapse a specific row.
1928 * @param filter Parent of the row.
1929 * @param next ID pointing to the row.
1931 template<class Tid>
1932 void HandleCargoWaitingClick(CargoDataEntry *filter, Tid next)
1934 if (filter->Retrieve(next) != nullptr) {
1935 filter->Remove(next);
1936 } else {
1937 filter->InsertOrRetrieve(next);
1942 * Handle a click on a specific row in the cargo view.
1943 * @param row Row being clicked.
1945 void HandleCargoWaitingClick(int row)
1947 if (row < 0 || (uint)row >= this->displayed_rows.size()) return;
1948 if (_ctrl_pressed) {
1949 this->scroll_to_row = row;
1950 } else {
1951 RowDisplay &display = this->displayed_rows[row];
1952 if (display.filter == &this->expanded_rows) {
1953 this->HandleCargoWaitingClick<CargoID>(display.filter, display.next_cargo);
1954 } else {
1955 this->HandleCargoWaitingClick<StationID>(display.filter, display.next_station);
1958 this->SetWidgetDirty(WID_SV_WAITING);
1961 void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override
1963 switch (widget) {
1964 case WID_SV_WAITING:
1965 this->HandleCargoWaitingClick(this->vscroll->GetScrolledRowFromWidget(pt.y, this, WID_SV_WAITING, WidgetDimensions::scaled.framerect.top) - this->vscroll->GetPosition());
1966 break;
1968 case WID_SV_CATCHMENT:
1969 SetViewportCatchmentStation(Station::Get(this->window_number), !this->IsWidgetLowered(WID_SV_CATCHMENT));
1970 break;
1972 case WID_SV_LOCATION:
1973 if (_ctrl_pressed) {
1974 ShowExtraViewportWindow(Station::Get(this->window_number)->xy);
1975 } else {
1976 ScrollMainWindowToTile(Station::Get(this->window_number)->xy);
1978 break;
1980 case WID_SV_ACCEPTS_RATINGS: {
1981 /* Swap between 'accepts' and 'ratings' view. */
1982 int height_change;
1983 NWidgetCore *nwi = this->GetWidget<NWidgetCore>(WID_SV_ACCEPTS_RATINGS);
1984 if (this->GetWidget<NWidgetCore>(WID_SV_ACCEPTS_RATINGS)->widget_data == STR_STATION_VIEW_RATINGS_BUTTON) {
1985 nwi->SetDataTip(STR_STATION_VIEW_ACCEPTS_BUTTON, STR_STATION_VIEW_ACCEPTS_TOOLTIP); // Switch to accepts view.
1986 height_change = this->rating_lines - this->accepts_lines;
1987 } else {
1988 nwi->SetDataTip(STR_STATION_VIEW_RATINGS_BUTTON, STR_STATION_VIEW_RATINGS_TOOLTIP); // Switch to ratings view.
1989 height_change = this->accepts_lines - this->rating_lines;
1991 this->ReInit(0, height_change * GetCharacterHeight(FS_NORMAL));
1992 break;
1995 case WID_SV_RENAME:
1996 SetDParam(0, this->window_number);
1997 ShowQueryString(STR_STATION_NAME, STR_STATION_VIEW_RENAME_STATION_CAPTION, MAX_LENGTH_STATION_NAME_CHARS,
1998 this, CS_ALPHANUMERAL, QSF_ENABLE_DEFAULT | QSF_LEN_IN_CHARS);
1999 break;
2001 case WID_SV_CLOSE_AIRPORT:
2002 Command<CMD_OPEN_CLOSE_AIRPORT>::Post(this->window_number);
2003 break;
2005 case WID_SV_TRAINS: // Show list of scheduled trains to this station
2006 case WID_SV_ROADVEHS: // Show list of scheduled road-vehicles to this station
2007 case WID_SV_SHIPS: // Show list of scheduled ships to this station
2008 case WID_SV_PLANES: { // Show list of scheduled aircraft to this station
2009 Owner owner = Station::Get(this->window_number)->owner;
2010 ShowVehicleListWindow(owner, (VehicleType)(widget - WID_SV_TRAINS), (StationID)this->window_number);
2011 break;
2014 case WID_SV_SORT_BY: {
2015 /* The initial selection is composed of current mode and
2016 * sorting criteria for columns 1, 2, and 3. Column 0 is always
2017 * sorted by cargo ID. The others can theoretically be sorted
2018 * by different things but there is no UI for that. */
2019 ShowDropDownMenu(this, StationViewWindow::sort_names,
2020 this->current_mode * 2 + (this->sortings[1] == CargoSortType::Count ? 1 : 0),
2021 WID_SV_SORT_BY, 0, 0);
2022 break;
2025 case WID_SV_GROUP_BY: {
2026 ShowDropDownMenu(this, StationViewWindow::group_names, this->grouping_index, WID_SV_GROUP_BY, 0, 0);
2027 break;
2030 case WID_SV_SORT_ORDER: { // flip sorting method asc/desc
2031 this->SelectSortOrder(this->sort_orders[1] == SO_ASCENDING ? SO_DESCENDING : SO_ASCENDING);
2032 this->SetTimeout();
2033 this->LowerWidget(WID_SV_SORT_ORDER);
2034 break;
2040 * Select a new sort order for the cargo view.
2041 * @param order New sort order.
2043 void SelectSortOrder(SortOrder order)
2045 this->sort_orders[1] = this->sort_orders[2] = this->sort_orders[3] = order;
2046 _settings_client.gui.station_gui_sort_order = this->sort_orders[1];
2047 this->SetDirty();
2051 * Select a new sort criterium for the cargo view.
2052 * @param index Row being selected in the sort criteria drop down.
2054 void SelectSortBy(int index)
2056 _settings_client.gui.station_gui_sort_by = index;
2057 switch (StationViewWindow::sort_names[index]) {
2058 case STR_STATION_VIEW_WAITING_STATION:
2059 this->current_mode = MODE_WAITING;
2060 this->sortings[1] = this->sortings[2] = this->sortings[3] = CargoSortType::AsGrouping;
2061 break;
2062 case STR_STATION_VIEW_WAITING_AMOUNT:
2063 this->current_mode = MODE_WAITING;
2064 this->sortings[1] = this->sortings[2] = this->sortings[3] = CargoSortType::Count;
2065 break;
2066 case STR_STATION_VIEW_PLANNED_STATION:
2067 this->current_mode = MODE_PLANNED;
2068 this->sortings[1] = this->sortings[2] = this->sortings[3] = CargoSortType::AsGrouping;
2069 break;
2070 case STR_STATION_VIEW_PLANNED_AMOUNT:
2071 this->current_mode = MODE_PLANNED;
2072 this->sortings[1] = this->sortings[2] = this->sortings[3] = CargoSortType::Count;
2073 break;
2074 default:
2075 NOT_REACHED();
2077 /* Display the current sort variant */
2078 this->GetWidget<NWidgetCore>(WID_SV_SORT_BY)->widget_data = StationViewWindow::sort_names[index];
2079 this->SetDirty();
2083 * Select a new grouping mode for the cargo view.
2084 * @param index Row being selected in the grouping drop down.
2086 void SelectGroupBy(int index)
2088 this->grouping_index = index;
2089 _settings_client.gui.station_gui_group_order = index;
2090 this->GetWidget<NWidgetCore>(WID_SV_GROUP_BY)->widget_data = StationViewWindow::group_names[index];
2091 switch (StationViewWindow::group_names[index]) {
2092 case STR_STATION_VIEW_GROUP_S_V_D:
2093 this->groupings[1] = GR_SOURCE;
2094 this->groupings[2] = GR_NEXT;
2095 this->groupings[3] = GR_DESTINATION;
2096 break;
2097 case STR_STATION_VIEW_GROUP_S_D_V:
2098 this->groupings[1] = GR_SOURCE;
2099 this->groupings[2] = GR_DESTINATION;
2100 this->groupings[3] = GR_NEXT;
2101 break;
2102 case STR_STATION_VIEW_GROUP_V_S_D:
2103 this->groupings[1] = GR_NEXT;
2104 this->groupings[2] = GR_SOURCE;
2105 this->groupings[3] = GR_DESTINATION;
2106 break;
2107 case STR_STATION_VIEW_GROUP_V_D_S:
2108 this->groupings[1] = GR_NEXT;
2109 this->groupings[2] = GR_DESTINATION;
2110 this->groupings[3] = GR_SOURCE;
2111 break;
2112 case STR_STATION_VIEW_GROUP_D_S_V:
2113 this->groupings[1] = GR_DESTINATION;
2114 this->groupings[2] = GR_SOURCE;
2115 this->groupings[3] = GR_NEXT;
2116 break;
2117 case STR_STATION_VIEW_GROUP_D_V_S:
2118 this->groupings[1] = GR_DESTINATION;
2119 this->groupings[2] = GR_NEXT;
2120 this->groupings[3] = GR_SOURCE;
2121 break;
2123 this->SetDirty();
2126 void OnDropdownSelect(WidgetID widget, int index) override
2128 if (widget == WID_SV_SORT_BY) {
2129 this->SelectSortBy(index);
2130 } else {
2131 this->SelectGroupBy(index);
2135 void OnQueryTextFinished(std::optional<std::string> str) override
2137 if (!str.has_value()) return;
2139 Command<CMD_RENAME_STATION>::Post(STR_ERROR_CAN_T_RENAME_STATION, this->window_number, *str);
2142 void OnResize() override
2144 this->vscroll->SetCapacityFromWidget(this, WID_SV_WAITING, WidgetDimensions::scaled.framerect.Vertical());
2148 * Some data on this window has become invalid. Invalidate the cache for the given cargo if necessary.
2149 * @param data Information about the changed data. If it's a valid cargo ID, invalidate the cargo data.
2150 * @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details.
2152 void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override
2154 if (gui_scope) {
2155 if (data >= 0 && data < NUM_CARGO) {
2156 this->cached_destinations.Remove((CargoID)data);
2157 } else {
2158 this->ReInit();
2164 static WindowDesc _station_view_desc(
2165 WDP_AUTO, "view_station", 249, 117,
2166 WC_STATION_VIEW, WC_NONE,
2168 _nested_station_view_widgets
2172 * Opens StationViewWindow for given station
2174 * @param station station which window should be opened
2176 void ShowStationViewWindow(StationID station)
2178 AllocateWindowDescFront<StationViewWindow>(_station_view_desc, station);
2181 /** Struct containing TileIndex and StationID */
2182 struct TileAndStation {
2183 TileIndex tile; ///< TileIndex
2184 StationID station; ///< StationID
2187 static std::vector<TileAndStation> _deleted_stations_nearby;
2188 static std::vector<StationID> _stations_nearby_list;
2191 * Add station on this tile to _stations_nearby_list if it's fully within the
2192 * station spread.
2193 * @param tile Tile just being checked
2194 * @param user_data Pointer to TileArea context
2195 * @tparam T the station filter type
2197 template <class T>
2198 static bool AddNearbyStation(TileIndex tile, void *user_data)
2200 TileArea *ctx = (TileArea *)user_data;
2202 /* First check if there were deleted stations here */
2203 for (auto it = _deleted_stations_nearby.begin(); it != _deleted_stations_nearby.end(); /* nothing */) {
2204 if (it->tile == tile) {
2205 _stations_nearby_list.push_back(it->station);
2206 it = _deleted_stations_nearby.erase(it);
2207 } else {
2208 ++it;
2212 /* Check if own station and if we stay within station spread */
2213 if (!IsTileType(tile, MP_STATION)) return false;
2215 StationID sid = GetStationIndex(tile);
2217 /* This station is (likely) a waypoint */
2218 if (!T::IsValidID(sid)) return false;
2220 BaseStation *st = BaseStation::Get(sid);
2221 if (st->owner != _local_company || std::ranges::find(_stations_nearby_list, sid) != _stations_nearby_list.end()) return false;
2223 if (st->rect.BeforeAddRect(ctx->tile, ctx->w, ctx->h, StationRect::ADD_TEST).Succeeded()) {
2224 _stations_nearby_list.push_back(sid);
2227 return false; // We want to include *all* nearby stations
2231 * Circulate around the to-be-built station to find stations we could join.
2232 * Make sure that only stations are returned where joining wouldn't exceed
2233 * station spread and are our own station.
2234 * @param ta Base tile area of the to-be-built station
2235 * @param distant_join Search for adjacent stations (false) or stations fully
2236 * within station spread
2237 * @tparam T the station filter type, for stations to look for
2239 template <class T>
2240 static const BaseStation *FindStationsNearby(TileArea ta, bool distant_join)
2242 TileArea ctx = ta;
2244 _stations_nearby_list.clear();
2245 _stations_nearby_list.push_back(NEW_STATION);
2246 _deleted_stations_nearby.clear();
2248 /* Check the inside, to return, if we sit on another station */
2249 for (TileIndex t : ta) {
2250 if (t < Map::Size() && IsTileType(t, MP_STATION) && T::IsValidID(GetStationIndex(t))) return BaseStation::GetByTile(t);
2253 /* Look for deleted stations */
2254 for (const BaseStation *st : BaseStation::Iterate()) {
2255 if (T::IsValidBaseStation(st) && !st->IsInUse() && st->owner == _local_company) {
2256 /* Include only within station spread (yes, it is strictly less than) */
2257 if (std::max(DistanceMax(ta.tile, st->xy), DistanceMax(TileAddXY(ta.tile, ta.w - 1, ta.h - 1), st->xy)) < _settings_game.station.station_spread) {
2258 _deleted_stations_nearby.push_back({st->xy, st->index});
2260 /* Add the station when it's within where we're going to build */
2261 if (IsInsideBS(TileX(st->xy), TileX(ctx.tile), ctx.w) &&
2262 IsInsideBS(TileY(st->xy), TileY(ctx.tile), ctx.h)) {
2263 AddNearbyStation<T>(st->xy, &ctx);
2269 /* Only search tiles where we have a chance to stay within the station spread.
2270 * The complete check needs to be done in the callback as we don't know the
2271 * extent of the found station, yet. */
2272 if (distant_join && std::min(ta.w, ta.h) >= _settings_game.station.station_spread) return nullptr;
2273 uint max_dist = distant_join ? _settings_game.station.station_spread - std::min(ta.w, ta.h) : 1;
2275 TileIndex tile = TileAddByDir(ctx.tile, DIR_N);
2276 CircularTileSearch(&tile, max_dist, ta.w, ta.h, AddNearbyStation<T>, &ctx);
2278 return nullptr;
2281 static constexpr NWidgetPart _nested_select_station_widgets[] = {
2282 NWidget(NWID_HORIZONTAL),
2283 NWidget(WWT_CLOSEBOX, COLOUR_DARK_GREEN),
2284 NWidget(WWT_CAPTION, COLOUR_DARK_GREEN, WID_JS_CAPTION), SetDataTip(STR_JOIN_STATION_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
2285 NWidget(WWT_DEFSIZEBOX, COLOUR_DARK_GREEN),
2286 EndContainer(),
2287 NWidget(NWID_HORIZONTAL),
2288 NWidget(WWT_PANEL, COLOUR_DARK_GREEN, WID_JS_PANEL), SetResize(1, 0), SetScrollbar(WID_JS_SCROLLBAR), EndContainer(),
2289 NWidget(NWID_VERTICAL),
2290 NWidget(NWID_VSCROLLBAR, COLOUR_DARK_GREEN, WID_JS_SCROLLBAR),
2291 NWidget(WWT_RESIZEBOX, COLOUR_DARK_GREEN),
2292 EndContainer(),
2293 EndContainer(),
2297 * Window for selecting stations/waypoints to (distant) join to.
2298 * @tparam T The station filter type, for stations to join with
2300 template <class T>
2301 struct SelectStationWindow : Window {
2302 StationPickerCmdProc select_station_proc;
2303 TileArea area; ///< Location of new station
2304 Scrollbar *vscroll;
2306 SelectStationWindow(WindowDesc &desc, TileArea ta, StationPickerCmdProc&& proc) :
2307 Window(desc),
2308 select_station_proc(std::move(proc)),
2309 area(ta)
2311 this->CreateNestedTree();
2312 this->vscroll = this->GetScrollbar(WID_JS_SCROLLBAR);
2313 this->GetWidget<NWidgetCore>(WID_JS_CAPTION)->widget_data = T::IsWaypoint() ? STR_JOIN_WAYPOINT_CAPTION : STR_JOIN_STATION_CAPTION;
2314 this->FinishInitNested(0);
2315 this->OnInvalidateData(0);
2317 _thd.freeze = true;
2320 void Close([[maybe_unused]] int data = 0) override
2322 SetViewportCatchmentSpecializedStation<typename T::StationType>(nullptr, true);
2324 _thd.freeze = false;
2325 this->Window::Close();
2328 void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override
2330 if (widget != WID_JS_PANEL) return;
2332 /* Determine the widest string */
2333 Dimension d = GetStringBoundingBox(T::IsWaypoint() ? STR_JOIN_WAYPOINT_CREATE_SPLITTED_WAYPOINT : STR_JOIN_STATION_CREATE_SPLITTED_STATION);
2334 for (const auto &station : _stations_nearby_list) {
2335 if (station == NEW_STATION) continue;
2336 const BaseStation *st = BaseStation::Get(station);
2337 SetDParam(0, st->index);
2338 SetDParam(1, st->facilities);
2339 d = maxdim(d, GetStringBoundingBox(T::IsWaypoint() ? STR_STATION_LIST_WAYPOINT : STR_STATION_LIST_STATION));
2342 resize.height = d.height;
2343 d.height *= 5;
2344 d.width += padding.width;
2345 d.height += padding.height;
2346 size = d;
2349 void DrawWidget(const Rect &r, WidgetID widget) const override
2351 if (widget != WID_JS_PANEL) return;
2353 Rect tr = r.Shrink(WidgetDimensions::scaled.framerect);
2354 auto [first, last] = this->vscroll->GetVisibleRangeIterators(_stations_nearby_list);
2355 for (auto it = first; it != last; ++it, tr.top += this->resize.step_height) {
2356 if (*it == NEW_STATION) {
2357 DrawString(tr, T::IsWaypoint() ? STR_JOIN_WAYPOINT_CREATE_SPLITTED_WAYPOINT : STR_JOIN_STATION_CREATE_SPLITTED_STATION);
2358 } else {
2359 const BaseStation *st = BaseStation::Get(*it);
2360 SetDParam(0, st->index);
2361 SetDParam(1, st->facilities);
2362 DrawString(tr, T::IsWaypoint() ? STR_STATION_LIST_WAYPOINT : STR_STATION_LIST_STATION);
2368 void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override
2370 if (widget != WID_JS_PANEL) return;
2372 auto it = this->vscroll->GetScrolledItemFromWidget(_stations_nearby_list, pt.y, this, WID_JS_PANEL, WidgetDimensions::scaled.framerect.top);
2373 if (it == _stations_nearby_list.end()) return;
2375 /* Execute stored Command */
2376 this->select_station_proc(false, *it);
2378 /* Close Window; this might cause double frees! */
2379 CloseWindowById(WC_SELECT_STATION, 0);
2382 void OnRealtimeTick([[maybe_unused]] uint delta_ms) override
2384 if (_thd.dirty & 2) {
2385 _thd.dirty &= ~2;
2386 this->SetDirty();
2390 void OnResize() override
2392 this->vscroll->SetCapacityFromWidget(this, WID_JS_PANEL, WidgetDimensions::scaled.framerect.Vertical());
2396 * Some data on this window has become invalid.
2397 * @param data Information about the changed data.
2398 * @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details.
2400 void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override
2402 if (!gui_scope) return;
2403 FindStationsNearby<T>(this->area, true);
2404 this->vscroll->SetCount(_stations_nearby_list.size());
2405 this->SetDirty();
2408 void OnMouseOver([[maybe_unused]] Point pt, WidgetID widget) override
2410 if (widget != WID_JS_PANEL) {
2411 SetViewportCatchmentSpecializedStation<typename T::StationType>(nullptr, true);
2412 return;
2415 /* Show coverage area of station under cursor */
2416 auto it = this->vscroll->GetScrolledItemFromWidget(_stations_nearby_list, pt.y, this, WID_JS_PANEL, WidgetDimensions::scaled.framerect.top);
2417 const typename T::StationType *st = it == _stations_nearby_list.end() || *it == NEW_STATION ? nullptr : T::StationType::Get(*it);
2418 SetViewportCatchmentSpecializedStation<typename T::StationType>(st, true);
2422 static WindowDesc _select_station_desc(
2423 WDP_AUTO, "build_station_join", 200, 180,
2424 WC_SELECT_STATION, WC_NONE,
2425 WDF_CONSTRUCTION,
2426 _nested_select_station_widgets
2431 * Check whether we need to show the station selection window.
2432 * @param cmd Command to build the station.
2433 * @param ta Tile area of the to-be-built station
2434 * @tparam T the station filter type
2435 * @return whether we need to show the station selection window.
2437 template <class T>
2438 static bool StationJoinerNeeded(TileArea ta, const StationPickerCmdProc &proc)
2440 /* Only show selection if distant join is enabled in the settings */
2441 if (!_settings_game.station.distant_join_stations) return false;
2443 /* If a window is already opened and we didn't ctrl-click,
2444 * return true (i.e. just flash the old window) */
2445 Window *selection_window = FindWindowById(WC_SELECT_STATION, 0);
2446 if (selection_window != nullptr) {
2447 /* Abort current distant-join and start new one */
2448 selection_window->Close();
2449 UpdateTileSelection();
2452 /* only show the popup, if we press ctrl */
2453 if (!_ctrl_pressed) return false;
2455 /* Now check if we could build there */
2456 if (!proc(true, INVALID_STATION)) return false;
2458 return FindStationsNearby<T>(ta, false) == nullptr;
2462 * Show the station selection window when needed. If not, build the station.
2463 * @param cmd Command to build the station.
2464 * @param ta Area to build the station in
2465 * @tparam the class to find stations for
2467 template <class T>
2468 void ShowSelectBaseStationIfNeeded(TileArea ta, StationPickerCmdProc&& proc)
2470 if (StationJoinerNeeded<T>(ta, proc)) {
2471 if (!_settings_client.gui.persistent_buildingtools) ResetObjectToPlace();
2472 new SelectStationWindow<T>(_select_station_desc, ta, std::move(proc));
2473 } else {
2474 proc(false, INVALID_STATION);
2479 * Show the station selection window when needed. If not, build the station.
2480 * @param ta Area to build the station in
2481 * @param proc Function called to execute the build command.
2483 void ShowSelectStationIfNeeded(TileArea ta, StationPickerCmdProc proc)
2485 ShowSelectBaseStationIfNeeded<StationTypeFilter>(ta, std::move(proc));
2489 * Show the rail waypoint selection window when needed. If not, build the waypoint.
2490 * @param ta Area to build the waypoint in
2491 * @param proc Function called to execute the build command.
2493 void ShowSelectRailWaypointIfNeeded(TileArea ta, StationPickerCmdProc proc)
2495 ShowSelectBaseStationIfNeeded<RailWaypointTypeFilter>(ta, std::move(proc));
2499 * Show the road waypoint selection window when needed. If not, build the waypoint.
2500 * @param ta Area to build the waypoint in
2501 * @param proc Function called to execute the build command.
2503 void ShowSelectRoadWaypointIfNeeded(TileArea ta, StationPickerCmdProc proc)
2505 ShowSelectBaseStationIfNeeded<RoadWaypointTypeFilter>(ta, std::move(proc));