Update: Translations from eints
[openttd-github.git] / src / station_gui.cpp
blob4ba355b721ac1f944bd091ad318854ef6ba36121
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 enum AcceptListHeight {
1317 ALH_RATING = 13, ///< Height of the cargo ratings view.
1318 ALH_ACCEPTS = 3, ///< Height of the accepted cargo view.
1321 /** Names of the sorting options in the dropdown. */
1322 static inline const StringID sort_names[] = {
1323 STR_STATION_VIEW_WAITING_STATION,
1324 STR_STATION_VIEW_WAITING_AMOUNT,
1325 STR_STATION_VIEW_PLANNED_STATION,
1326 STR_STATION_VIEW_PLANNED_AMOUNT,
1328 /** Names of the grouping options in the dropdown. */
1329 static inline const StringID group_names[] = {
1330 STR_STATION_VIEW_GROUP_S_V_D,
1331 STR_STATION_VIEW_GROUP_S_D_V,
1332 STR_STATION_VIEW_GROUP_V_S_D,
1333 STR_STATION_VIEW_GROUP_V_D_S,
1334 STR_STATION_VIEW_GROUP_D_S_V,
1335 STR_STATION_VIEW_GROUP_D_V_S,
1339 * Sort types of the different 'columns'.
1340 * In fact only CargoSortType::Count and CargoSortType::AsGrouping are active and you can only
1341 * sort all the columns in the same way. The other options haven't been
1342 * included in the GUI due to lack of space.
1344 CargoSortType sortings[NUM_COLUMNS];
1346 /** Sort order (ascending/descending) for the 'columns'. */
1347 SortOrder sort_orders[NUM_COLUMNS];
1349 int scroll_to_row; ///< If set, scroll the main viewport to the station pointed to by this row.
1350 int grouping_index; ///< Currently selected entry in the grouping drop down.
1351 Mode current_mode; ///< Currently selected display mode of cargo view.
1352 Grouping groupings[NUM_COLUMNS]; ///< Grouping modes for the different columns.
1354 CargoDataEntry expanded_rows; ///< Parent entry of currently expanded rows.
1355 CargoDataEntry cached_destinations; ///< Cache for the flows passing through this station.
1356 CargoDataVector displayed_rows; ///< Parent entry of currently displayed rows (including collapsed ones).
1358 StationViewWindow(WindowDesc &desc, WindowNumber window_number) : Window(desc),
1359 scroll_to_row(INT_MAX), grouping_index(0)
1361 this->rating_lines = ALH_RATING;
1362 this->accepts_lines = ALH_ACCEPTS;
1364 this->CreateNestedTree();
1365 this->vscroll = this->GetScrollbar(WID_SV_SCROLLBAR);
1366 /* Nested widget tree creation is done in two steps to ensure that this->GetWidget<NWidgetCore>(WID_SV_ACCEPTS_RATINGS) exists in UpdateWidgetSize(). */
1367 this->FinishInitNested(window_number);
1369 this->groupings[0] = GR_CARGO;
1370 this->sortings[0] = CargoSortType::AsGrouping;
1371 this->SelectGroupBy(_settings_client.gui.station_gui_group_order);
1372 this->SelectSortBy(_settings_client.gui.station_gui_sort_by);
1373 this->sort_orders[0] = SO_ASCENDING;
1374 this->SelectSortOrder((SortOrder)_settings_client.gui.station_gui_sort_order);
1375 this->owner = Station::Get(window_number)->owner;
1378 void Close([[maybe_unused]] int data = 0) override
1380 CloseWindowById(WC_TRAINS_LIST, VehicleListIdentifier(VL_STATION_LIST, VEH_TRAIN, this->owner, this->window_number).Pack(), false);
1381 CloseWindowById(WC_ROADVEH_LIST, VehicleListIdentifier(VL_STATION_LIST, VEH_ROAD, this->owner, this->window_number).Pack(), false);
1382 CloseWindowById(WC_SHIPS_LIST, VehicleListIdentifier(VL_STATION_LIST, VEH_SHIP, this->owner, this->window_number).Pack(), false);
1383 CloseWindowById(WC_AIRCRAFT_LIST, VehicleListIdentifier(VL_STATION_LIST, VEH_AIRCRAFT, this->owner, this->window_number).Pack(), false);
1385 SetViewportCatchmentStation(Station::Get(this->window_number), false);
1386 this->Window::Close();
1390 * Show a certain cargo entry characterized by source/next/dest station, cargo ID and amount of cargo at the
1391 * right place in the cargo view. I.e. update as many rows as are expanded following that characterization.
1392 * @param data Root entry of the tree.
1393 * @param cargo Cargo ID of the entry to be shown.
1394 * @param source Source station of the entry to be shown.
1395 * @param next Next station the cargo to be shown will visit.
1396 * @param dest Final destination of the cargo to be shown.
1397 * @param count Amount of cargo to be shown.
1399 void ShowCargo(CargoDataEntry *data, CargoID cargo, StationID source, StationID next, StationID dest, uint count)
1401 if (count == 0) return;
1402 bool auto_distributed = _settings_game.linkgraph.GetDistributionType(cargo) != DT_MANUAL;
1403 const CargoDataEntry *expand = &this->expanded_rows;
1404 for (int i = 0; i < NUM_COLUMNS && expand != nullptr; ++i) {
1405 switch (groupings[i]) {
1406 case GR_CARGO:
1407 assert(i == 0);
1408 data = data->InsertOrRetrieve(cargo);
1409 data->SetTransfers(source != this->window_number);
1410 expand = expand->Retrieve(cargo);
1411 break;
1412 case GR_SOURCE:
1413 if (auto_distributed || source != this->window_number) {
1414 data = data->InsertOrRetrieve(source);
1415 expand = expand->Retrieve(source);
1417 break;
1418 case GR_NEXT:
1419 if (auto_distributed) {
1420 data = data->InsertOrRetrieve(next);
1421 expand = expand->Retrieve(next);
1423 break;
1424 case GR_DESTINATION:
1425 if (auto_distributed) {
1426 data = data->InsertOrRetrieve(dest);
1427 expand = expand->Retrieve(dest);
1429 break;
1432 data->Update(count);
1435 void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override
1437 switch (widget) {
1438 case WID_SV_WAITING:
1439 resize.height = GetCharacterHeight(FS_NORMAL);
1440 size.height = 4 * resize.height + padding.height;
1441 this->expand_shrink_width = std::max(GetStringBoundingBox("-").width, GetStringBoundingBox("+").width);
1442 break;
1444 case WID_SV_ACCEPT_RATING_LIST:
1445 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;
1446 break;
1448 case WID_SV_CLOSE_AIRPORT:
1449 if (!(Station::Get(this->window_number)->facilities & FACIL_AIRPORT)) {
1450 /* Hide 'Close Airport' button if no airport present. */
1451 size.width = 0;
1452 resize.width = 0;
1453 fill.width = 0;
1455 break;
1459 void OnPaint() override
1461 const Station *st = Station::Get(this->window_number);
1462 CargoDataEntry cargo;
1463 BuildCargoList(&cargo, st);
1465 this->vscroll->SetCount(cargo.GetNumChildren()); // update scrollbar
1467 /* disable some buttons */
1468 this->SetWidgetDisabledState(WID_SV_RENAME, st->owner != _local_company);
1469 this->SetWidgetDisabledState(WID_SV_TRAINS, !(st->facilities & FACIL_TRAIN));
1470 this->SetWidgetDisabledState(WID_SV_ROADVEHS, !(st->facilities & FACIL_TRUCK_STOP) && !(st->facilities & FACIL_BUS_STOP));
1471 this->SetWidgetDisabledState(WID_SV_SHIPS, !(st->facilities & FACIL_DOCK));
1472 this->SetWidgetDisabledState(WID_SV_PLANES, !(st->facilities & FACIL_AIRPORT));
1473 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
1474 this->SetWidgetLoweredState(WID_SV_CLOSE_AIRPORT, (st->facilities & FACIL_AIRPORT) && (st->airport.flags & AIRPORT_CLOSED_block) != 0);
1476 extern const Station *_viewport_highlight_station;
1477 this->SetWidgetDisabledState(WID_SV_CATCHMENT, st->facilities == FACIL_NONE);
1478 this->SetWidgetLoweredState(WID_SV_CATCHMENT, _viewport_highlight_station == st);
1480 this->DrawWidgets();
1482 if (!this->IsShaded()) {
1483 /* Draw 'accepted cargo' or 'cargo ratings'. */
1484 const NWidgetBase *wid = this->GetWidget<NWidgetBase>(WID_SV_ACCEPT_RATING_LIST);
1485 const Rect r = wid->GetCurrentRect();
1486 if (this->GetWidget<NWidgetCore>(WID_SV_ACCEPTS_RATINGS)->widget_data == STR_STATION_VIEW_RATINGS_BUTTON) {
1487 int lines = this->DrawAcceptedCargo(r);
1488 if (lines > this->accepts_lines) { // Resize the widget, and perform re-initialization of the window.
1489 this->accepts_lines = lines;
1490 this->ReInit();
1491 return;
1493 } else {
1494 int lines = this->DrawCargoRatings(r);
1495 if (lines > this->rating_lines) { // Resize the widget, and perform re-initialization of the window.
1496 this->rating_lines = lines;
1497 this->ReInit();
1498 return;
1502 /* Draw arrow pointing up/down for ascending/descending sorting */
1503 this->DrawSortButtonState(WID_SV_SORT_ORDER, sort_orders[1] == SO_ASCENDING ? SBS_UP : SBS_DOWN);
1505 int pos = this->vscroll->GetPosition();
1507 int maxrows = this->vscroll->GetCapacity();
1509 displayed_rows.clear();
1511 /* Draw waiting cargo. */
1512 NWidgetBase *nwi = this->GetWidget<NWidgetBase>(WID_SV_WAITING);
1513 Rect waiting_rect = nwi->GetCurrentRect().Shrink(WidgetDimensions::scaled.framerect);
1514 this->DrawEntries(&cargo, waiting_rect, pos, maxrows, 0);
1515 scroll_to_row = INT_MAX;
1519 void SetStringParameters(WidgetID widget) const override
1521 if (widget == WID_SV_CAPTION) {
1522 const Station *st = Station::Get(this->window_number);
1523 SetDParam(0, st->index);
1524 SetDParam(1, st->facilities);
1529 * Rebuild the cache for estimated destinations which is used to quickly show the "destination" entries
1530 * even if we actually don't know the destination of a certain packet from just looking at it.
1531 * @param i Cargo to recalculate the cache for.
1533 void RecalcDestinations(CargoID i)
1535 const Station *st = Station::Get(this->window_number);
1536 CargoDataEntry *cargo_entry = cached_destinations.InsertOrRetrieve(i);
1537 cargo_entry->Clear();
1539 for (const auto &it : st->goods[i].flows) {
1540 StationID from = it.first;
1541 CargoDataEntry *source_entry = cargo_entry->InsertOrRetrieve(from);
1542 uint32_t prev_count = 0;
1543 for (const auto &flow_it : *it.second.GetShares()) {
1544 StationID via = flow_it.second;
1545 CargoDataEntry *via_entry = source_entry->InsertOrRetrieve(via);
1546 if (via == this->window_number) {
1547 via_entry->InsertOrRetrieve(via)->Update(flow_it.first - prev_count);
1548 } else {
1549 EstimateDestinations(i, from, via, flow_it.first - prev_count, via_entry);
1551 prev_count = flow_it.first;
1557 * Estimate the amounts of cargo per final destination for a given cargo, source station and next hop and
1558 * save the result as children of the given CargoDataEntry.
1559 * @param cargo ID of the cargo to estimate destinations for.
1560 * @param source Source station of the given batch of cargo.
1561 * @param next Intermediate hop to start the calculation at ("next hop").
1562 * @param count Size of the batch of cargo.
1563 * @param dest CargoDataEntry to save the results in.
1565 void EstimateDestinations(CargoID cargo, StationID source, StationID next, uint count, CargoDataEntry *dest)
1567 if (Station::IsValidID(next) && Station::IsValidID(source)) {
1568 CargoDataEntry tmp;
1569 const FlowStatMap &flowmap = Station::Get(next)->goods[cargo].flows;
1570 FlowStatMap::const_iterator map_it = flowmap.find(source);
1571 if (map_it != flowmap.end()) {
1572 const FlowStat::SharesMap *shares = map_it->second.GetShares();
1573 uint32_t prev_count = 0;
1574 for (FlowStat::SharesMap::const_iterator i = shares->begin(); i != shares->end(); ++i) {
1575 tmp.InsertOrRetrieve(i->second)->Update(i->first - prev_count);
1576 prev_count = i->first;
1580 if (tmp.GetCount() == 0) {
1581 dest->InsertOrRetrieve(INVALID_STATION)->Update(count);
1582 } else {
1583 uint sum_estimated = 0;
1584 while (sum_estimated < count) {
1585 for (CargoDataSet::iterator i = tmp.Begin(); i != tmp.End() && sum_estimated < count; ++i) {
1586 CargoDataEntry *child = *i;
1587 uint estimate = DivideApprox(child->GetCount() * count, tmp.GetCount());
1588 if (estimate == 0) estimate = 1;
1590 sum_estimated += estimate;
1591 if (sum_estimated > count) {
1592 estimate -= sum_estimated - count;
1593 sum_estimated = count;
1596 if (estimate > 0) {
1597 if (child->GetStation() == next) {
1598 dest->InsertOrRetrieve(next)->Update(estimate);
1599 } else {
1600 EstimateDestinations(cargo, source, child->GetStation(), estimate, dest);
1607 } else {
1608 dest->InsertOrRetrieve(INVALID_STATION)->Update(count);
1613 * Build up the cargo view for PLANNED mode and a specific cargo.
1614 * @param i Cargo to show.
1615 * @param flows The current station's flows for that cargo.
1616 * @param cargo The CargoDataEntry to save the results in.
1618 void BuildFlowList(CargoID i, const FlowStatMap &flows, CargoDataEntry *cargo)
1620 const CargoDataEntry *source_dest = this->cached_destinations.Retrieve(i);
1621 for (FlowStatMap::const_iterator it = flows.begin(); it != flows.end(); ++it) {
1622 StationID from = it->first;
1623 const CargoDataEntry *source_entry = source_dest->Retrieve(from);
1624 const FlowStat::SharesMap *shares = it->second.GetShares();
1625 for (FlowStat::SharesMap::const_iterator flow_it = shares->begin(); flow_it != shares->end(); ++flow_it) {
1626 const CargoDataEntry *via_entry = source_entry->Retrieve(flow_it->second);
1627 for (CargoDataSet::iterator dest_it = via_entry->Begin(); dest_it != via_entry->End(); ++dest_it) {
1628 CargoDataEntry *dest_entry = *dest_it;
1629 ShowCargo(cargo, i, from, flow_it->second, dest_entry->GetStation(), dest_entry->GetCount());
1636 * Build up the cargo view for WAITING mode and a specific cargo.
1637 * @param i Cargo to show.
1638 * @param packets The current station's cargo list for that cargo.
1639 * @param cargo The CargoDataEntry to save the result in.
1641 void BuildCargoList(CargoID i, const StationCargoList &packets, CargoDataEntry *cargo)
1643 const CargoDataEntry *source_dest = this->cached_destinations.Retrieve(i);
1644 for (StationCargoList::ConstIterator it = packets.Packets()->begin(); it != packets.Packets()->end(); it++) {
1645 const CargoPacket *cp = *it;
1646 StationID next = it.GetKey();
1648 const CargoDataEntry *source_entry = source_dest->Retrieve(cp->GetFirstStation());
1649 if (source_entry == nullptr) {
1650 this->ShowCargo(cargo, i, cp->GetFirstStation(), next, INVALID_STATION, cp->Count());
1651 continue;
1654 const CargoDataEntry *via_entry = source_entry->Retrieve(next);
1655 if (via_entry == nullptr) {
1656 this->ShowCargo(cargo, i, cp->GetFirstStation(), next, INVALID_STATION, cp->Count());
1657 continue;
1660 uint remaining = cp->Count();
1661 for (CargoDataSet::iterator dest_it = via_entry->Begin(); dest_it != via_entry->End();) {
1662 CargoDataEntry *dest_entry = *dest_it;
1664 /* Advance iterator here instead of in the for statement to test whether this is the last entry */
1665 ++dest_it;
1667 uint val;
1668 if (dest_it == via_entry->End()) {
1669 /* Allocate all remaining waiting cargo to the last destination to avoid
1670 * waiting cargo being "lost", and the displayed total waiting cargo
1671 * not matching GoodsEntry::TotalCount() */
1672 val = remaining;
1673 } else {
1674 val = std::min<uint>(remaining, DivideApprox(cp->Count() * dest_entry->GetCount(), via_entry->GetCount()));
1675 remaining -= val;
1677 this->ShowCargo(cargo, i, cp->GetFirstStation(), next, dest_entry->GetStation(), val);
1680 this->ShowCargo(cargo, i, NEW_STATION, NEW_STATION, NEW_STATION, packets.ReservedCount());
1684 * Build up the cargo view for all cargoes.
1685 * @param cargo The root cargo entry to save all results in.
1686 * @param st The station to calculate the cargo view from.
1688 void BuildCargoList(CargoDataEntry *cargo, const Station *st)
1690 for (CargoID i = 0; i < NUM_CARGO; i++) {
1692 if (this->cached_destinations.Retrieve(i) == nullptr) {
1693 this->RecalcDestinations(i);
1696 if (this->current_mode == MODE_WAITING) {
1697 this->BuildCargoList(i, st->goods[i].cargo, cargo);
1698 } else {
1699 this->BuildFlowList(i, st->goods[i].flows, cargo);
1705 * Mark a specific row, characterized by its CargoDataEntry, as expanded.
1706 * @param data The row to be marked as expanded.
1708 void SetDisplayedRow(const CargoDataEntry *data)
1710 std::list<StationID> stations;
1711 const CargoDataEntry *parent = data->GetParent();
1712 if (parent->GetParent() == nullptr) {
1713 this->displayed_rows.push_back(RowDisplay(&this->expanded_rows, data->GetCargo()));
1714 return;
1717 StationID next = data->GetStation();
1718 while (parent->GetParent()->GetParent() != nullptr) {
1719 stations.push_back(parent->GetStation());
1720 parent = parent->GetParent();
1723 CargoID cargo = parent->GetCargo();
1724 CargoDataEntry *filter = this->expanded_rows.Retrieve(cargo);
1725 while (!stations.empty()) {
1726 filter = filter->Retrieve(stations.back());
1727 stations.pop_back();
1730 this->displayed_rows.push_back(RowDisplay(filter, next));
1734 * Select the correct string for an entry referring to the specified station.
1735 * @param station Station the entry is showing cargo for.
1736 * @param here String to be shown if the entry refers to the same station as this station GUI belongs to.
1737 * @param other_station String to be shown if the entry refers to a specific other station.
1738 * @param any String to be shown if the entry refers to "any station".
1739 * @return One of the three given strings or STR_STATION_VIEW_RESERVED, depending on what station the entry refers to.
1741 StringID GetEntryString(StationID station, StringID here, StringID other_station, StringID any)
1743 if (station == this->window_number) {
1744 return here;
1745 } else if (station == INVALID_STATION) {
1746 return any;
1747 } else if (station == NEW_STATION) {
1748 return STR_STATION_VIEW_RESERVED;
1749 } else {
1750 SetDParam(2, station);
1751 return other_station;
1756 * Determine if we need to show the special "non-stop" string.
1757 * @param cd Entry we are going to show.
1758 * @param station Station the entry refers to.
1759 * @param column The "column" the entry will be shown in.
1760 * @return either STR_STATION_VIEW_VIA or STR_STATION_VIEW_NONSTOP.
1762 StringID SearchNonStop(CargoDataEntry *cd, StationID station, int column)
1764 CargoDataEntry *parent = cd->GetParent();
1765 for (int i = column - 1; i > 0; --i) {
1766 if (this->groupings[i] == GR_DESTINATION) {
1767 if (parent->GetStation() == station) {
1768 return STR_STATION_VIEW_NONSTOP;
1769 } else {
1770 return STR_STATION_VIEW_VIA;
1773 parent = parent->GetParent();
1776 if (this->groupings[column + 1] == GR_DESTINATION) {
1777 CargoDataSet::iterator begin = cd->Begin();
1778 CargoDataSet::iterator end = cd->End();
1779 if (begin != end && ++(cd->Begin()) == end && (*(begin))->GetStation() == station) {
1780 return STR_STATION_VIEW_NONSTOP;
1781 } else {
1782 return STR_STATION_VIEW_VIA;
1786 return STR_STATION_VIEW_VIA;
1790 * Draw the given cargo entries in the station GUI.
1791 * @param entry Root entry for all cargo to be drawn.
1792 * @param r Screen rectangle to draw into.
1793 * @param pos Current row to be drawn to (counted down from 0 to -maxrows, same as vscroll->GetPosition()).
1794 * @param maxrows Maximum row to be drawn.
1795 * @param column Current "column" being drawn.
1796 * @param cargo Current cargo being drawn (if cargo column has been passed).
1797 * @return row (in "pos" counting) after the one we have last drawn to.
1799 int DrawEntries(CargoDataEntry *entry, const Rect &r, int pos, int maxrows, int column, CargoID cargo = INVALID_CARGO)
1801 if (this->sortings[column] == CargoSortType::AsGrouping) {
1802 if (this->groupings[column] != GR_CARGO) {
1803 entry->Resort(CargoSortType::StationString, this->sort_orders[column]);
1805 } else {
1806 entry->Resort(CargoSortType::Count, this->sort_orders[column]);
1808 for (CargoDataSet::iterator i = entry->Begin(); i != entry->End(); ++i) {
1809 CargoDataEntry *cd = *i;
1811 Grouping grouping = this->groupings[column];
1812 if (grouping == GR_CARGO) cargo = cd->GetCargo();
1813 bool auto_distributed = _settings_game.linkgraph.GetDistributionType(cargo) != DT_MANUAL;
1815 if (pos > -maxrows && pos <= 0) {
1816 StringID str = STR_EMPTY;
1817 int y = r.top - pos * GetCharacterHeight(FS_NORMAL);
1818 SetDParam(0, cargo);
1819 SetDParam(1, cd->GetCount());
1821 if (this->groupings[column] == GR_CARGO) {
1822 str = STR_STATION_VIEW_WAITING_CARGO;
1823 DrawCargoIcons(cd->GetCargo(), cd->GetCount(), r.left + this->expand_shrink_width, r.right - this->expand_shrink_width, y);
1824 } else {
1825 if (!auto_distributed) grouping = GR_SOURCE;
1826 StationID station = cd->GetStation();
1828 switch (grouping) {
1829 case GR_SOURCE:
1830 str = this->GetEntryString(station, STR_STATION_VIEW_FROM_HERE, STR_STATION_VIEW_FROM, STR_STATION_VIEW_FROM_ANY);
1831 break;
1832 case GR_NEXT:
1833 str = this->GetEntryString(station, STR_STATION_VIEW_VIA_HERE, STR_STATION_VIEW_VIA, STR_STATION_VIEW_VIA_ANY);
1834 if (str == STR_STATION_VIEW_VIA) str = this->SearchNonStop(cd, station, column);
1835 break;
1836 case GR_DESTINATION:
1837 str = this->GetEntryString(station, STR_STATION_VIEW_TO_HERE, STR_STATION_VIEW_TO, STR_STATION_VIEW_TO_ANY);
1838 break;
1839 default:
1840 NOT_REACHED();
1842 if (pos == -this->scroll_to_row && Station::IsValidID(station)) {
1843 ScrollMainWindowToTile(Station::Get(station)->xy);
1847 bool rtl = _current_text_dir == TD_RTL;
1848 Rect text = r.Indent(column * WidgetDimensions::scaled.hsep_indent, rtl).Indent(this->expand_shrink_width, !rtl);
1849 Rect shrink = r.WithWidth(this->expand_shrink_width, !rtl);
1851 DrawString(text.left, text.right, y, str);
1853 if (column < NUM_COLUMNS - 1) {
1854 const char *sym = nullptr;
1855 if (cd->GetNumChildren() > 0) {
1856 sym = "-";
1857 } else if (auto_distributed && str != STR_STATION_VIEW_RESERVED) {
1858 sym = "+";
1859 } else {
1860 /* Only draw '+' if there is something to be shown. */
1861 const StationCargoList &list = Station::Get(this->window_number)->goods[cargo].cargo;
1862 if (grouping == GR_CARGO && (list.ReservedCount() > 0 || cd->HasTransfers())) {
1863 sym = "+";
1866 if (sym != nullptr) DrawString(shrink.left, shrink.right, y, sym, TC_YELLOW);
1868 this->SetDisplayedRow(cd);
1870 --pos;
1871 if (auto_distributed || column == 0) {
1872 pos = this->DrawEntries(cd, r, pos, maxrows, column + 1, cargo);
1875 return pos;
1879 * Draw accepted cargo in the #WID_SV_ACCEPT_RATING_LIST widget.
1880 * @param r Rectangle of the widget.
1881 * @return Number of lines needed for drawing the accepted cargo.
1883 int DrawAcceptedCargo(const Rect &r) const
1885 const Station *st = Station::Get(this->window_number);
1886 Rect tr = r.Shrink(WidgetDimensions::scaled.framerect);
1888 SetDParam(0, GetAcceptanceMask(st));
1889 int bottom = DrawStringMultiLine(tr.left, tr.right, tr.top, INT32_MAX, STR_STATION_VIEW_ACCEPTS_CARGO);
1890 return CeilDiv(bottom - r.top - WidgetDimensions::scaled.framerect.top, GetCharacterHeight(FS_NORMAL));
1894 * Draw cargo ratings in the #WID_SV_ACCEPT_RATING_LIST widget.
1895 * @param r Rectangle of the widget.
1896 * @return Number of lines needed for drawing the cargo ratings.
1898 int DrawCargoRatings(const Rect &r) const
1900 const Station *st = Station::Get(this->window_number);
1901 bool rtl = _current_text_dir == TD_RTL;
1902 Rect tr = r.Shrink(WidgetDimensions::scaled.framerect);
1904 if (st->town->exclusive_counter > 0) {
1905 SetDParam(0, st->town->exclusivity);
1906 tr.top = DrawStringMultiLine(tr, st->town->exclusivity == st->owner ? STR_STATION_VIEW_EXCLUSIVE_RIGHTS_SELF : STR_STATION_VIEW_EXCLUSIVE_RIGHTS_COMPANY);
1907 tr.top += WidgetDimensions::scaled.vsep_wide;
1910 DrawString(tr, TimerGameEconomy::UsingWallclockUnits() ? STR_STATION_VIEW_SUPPLY_RATINGS_TITLE_MINUTE : STR_STATION_VIEW_SUPPLY_RATINGS_TITLE_MONTH);
1911 tr.top += GetCharacterHeight(FS_NORMAL);
1913 for (const CargoSpec *cs : _sorted_standard_cargo_specs) {
1914 const GoodsEntry *ge = &st->goods[cs->Index()];
1915 if (!ge->HasRating()) continue;
1917 const LinkGraph *lg = LinkGraph::GetIfValid(ge->link_graph);
1918 SetDParam(0, cs->name);
1919 SetDParam(1, lg != nullptr ? lg->Monthly((*lg)[ge->node].supply) : 0);
1920 SetDParam(2, STR_CARGO_RATING_APPALLING + (ge->rating >> 5));
1921 SetDParam(3, ToPercent8(ge->rating));
1922 DrawString(tr.Indent(WidgetDimensions::scaled.hsep_indent, rtl), STR_STATION_VIEW_CARGO_SUPPLY_RATING);
1923 tr.top += GetCharacterHeight(FS_NORMAL);
1925 return CeilDiv(tr.top - r.top - WidgetDimensions::scaled.framerect.top, GetCharacterHeight(FS_NORMAL));
1929 * Expand or collapse a specific row.
1930 * @param filter Parent of the row.
1931 * @param next ID pointing to the row.
1933 template<class Tid>
1934 void HandleCargoWaitingClick(CargoDataEntry *filter, Tid next)
1936 if (filter->Retrieve(next) != nullptr) {
1937 filter->Remove(next);
1938 } else {
1939 filter->InsertOrRetrieve(next);
1944 * Handle a click on a specific row in the cargo view.
1945 * @param row Row being clicked.
1947 void HandleCargoWaitingClick(int row)
1949 if (row < 0 || (uint)row >= this->displayed_rows.size()) return;
1950 if (_ctrl_pressed) {
1951 this->scroll_to_row = row;
1952 } else {
1953 RowDisplay &display = this->displayed_rows[row];
1954 if (display.filter == &this->expanded_rows) {
1955 this->HandleCargoWaitingClick<CargoID>(display.filter, display.next_cargo);
1956 } else {
1957 this->HandleCargoWaitingClick<StationID>(display.filter, display.next_station);
1960 this->SetWidgetDirty(WID_SV_WAITING);
1963 void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override
1965 switch (widget) {
1966 case WID_SV_WAITING:
1967 this->HandleCargoWaitingClick(this->vscroll->GetScrolledRowFromWidget(pt.y, this, WID_SV_WAITING, WidgetDimensions::scaled.framerect.top) - this->vscroll->GetPosition());
1968 break;
1970 case WID_SV_CATCHMENT:
1971 SetViewportCatchmentStation(Station::Get(this->window_number), !this->IsWidgetLowered(WID_SV_CATCHMENT));
1972 break;
1974 case WID_SV_LOCATION:
1975 if (_ctrl_pressed) {
1976 ShowExtraViewportWindow(Station::Get(this->window_number)->xy);
1977 } else {
1978 ScrollMainWindowToTile(Station::Get(this->window_number)->xy);
1980 break;
1982 case WID_SV_ACCEPTS_RATINGS: {
1983 /* Swap between 'accepts' and 'ratings' view. */
1984 int height_change;
1985 NWidgetCore *nwi = this->GetWidget<NWidgetCore>(WID_SV_ACCEPTS_RATINGS);
1986 if (this->GetWidget<NWidgetCore>(WID_SV_ACCEPTS_RATINGS)->widget_data == STR_STATION_VIEW_RATINGS_BUTTON) {
1987 nwi->SetDataTip(STR_STATION_VIEW_ACCEPTS_BUTTON, STR_STATION_VIEW_ACCEPTS_TOOLTIP); // Switch to accepts view.
1988 height_change = this->rating_lines - this->accepts_lines;
1989 } else {
1990 nwi->SetDataTip(STR_STATION_VIEW_RATINGS_BUTTON, STR_STATION_VIEW_RATINGS_TOOLTIP); // Switch to ratings view.
1991 height_change = this->accepts_lines - this->rating_lines;
1993 this->ReInit(0, height_change * GetCharacterHeight(FS_NORMAL));
1994 break;
1997 case WID_SV_RENAME:
1998 SetDParam(0, this->window_number);
1999 ShowQueryString(STR_STATION_NAME, STR_STATION_VIEW_RENAME_STATION_CAPTION, MAX_LENGTH_STATION_NAME_CHARS,
2000 this, CS_ALPHANUMERAL, QSF_ENABLE_DEFAULT | QSF_LEN_IN_CHARS);
2001 break;
2003 case WID_SV_CLOSE_AIRPORT:
2004 Command<CMD_OPEN_CLOSE_AIRPORT>::Post(this->window_number);
2005 break;
2007 case WID_SV_TRAINS: // Show list of scheduled trains to this station
2008 case WID_SV_ROADVEHS: // Show list of scheduled road-vehicles to this station
2009 case WID_SV_SHIPS: // Show list of scheduled ships to this station
2010 case WID_SV_PLANES: { // Show list of scheduled aircraft to this station
2011 Owner owner = Station::Get(this->window_number)->owner;
2012 ShowVehicleListWindow(owner, (VehicleType)(widget - WID_SV_TRAINS), (StationID)this->window_number);
2013 break;
2016 case WID_SV_SORT_BY: {
2017 /* The initial selection is composed of current mode and
2018 * sorting criteria for columns 1, 2, and 3. Column 0 is always
2019 * sorted by cargo ID. The others can theoretically be sorted
2020 * by different things but there is no UI for that. */
2021 ShowDropDownMenu(this, StationViewWindow::sort_names,
2022 this->current_mode * 2 + (this->sortings[1] == CargoSortType::Count ? 1 : 0),
2023 WID_SV_SORT_BY, 0, 0);
2024 break;
2027 case WID_SV_GROUP_BY: {
2028 ShowDropDownMenu(this, StationViewWindow::group_names, this->grouping_index, WID_SV_GROUP_BY, 0, 0);
2029 break;
2032 case WID_SV_SORT_ORDER: { // flip sorting method asc/desc
2033 this->SelectSortOrder(this->sort_orders[1] == SO_ASCENDING ? SO_DESCENDING : SO_ASCENDING);
2034 this->SetTimeout();
2035 this->LowerWidget(WID_SV_SORT_ORDER);
2036 break;
2042 * Select a new sort order for the cargo view.
2043 * @param order New sort order.
2045 void SelectSortOrder(SortOrder order)
2047 this->sort_orders[1] = this->sort_orders[2] = this->sort_orders[3] = order;
2048 _settings_client.gui.station_gui_sort_order = this->sort_orders[1];
2049 this->SetDirty();
2053 * Select a new sort criterium for the cargo view.
2054 * @param index Row being selected in the sort criteria drop down.
2056 void SelectSortBy(int index)
2058 _settings_client.gui.station_gui_sort_by = index;
2059 switch (StationViewWindow::sort_names[index]) {
2060 case STR_STATION_VIEW_WAITING_STATION:
2061 this->current_mode = MODE_WAITING;
2062 this->sortings[1] = this->sortings[2] = this->sortings[3] = CargoSortType::AsGrouping;
2063 break;
2064 case STR_STATION_VIEW_WAITING_AMOUNT:
2065 this->current_mode = MODE_WAITING;
2066 this->sortings[1] = this->sortings[2] = this->sortings[3] = CargoSortType::Count;
2067 break;
2068 case STR_STATION_VIEW_PLANNED_STATION:
2069 this->current_mode = MODE_PLANNED;
2070 this->sortings[1] = this->sortings[2] = this->sortings[3] = CargoSortType::AsGrouping;
2071 break;
2072 case STR_STATION_VIEW_PLANNED_AMOUNT:
2073 this->current_mode = MODE_PLANNED;
2074 this->sortings[1] = this->sortings[2] = this->sortings[3] = CargoSortType::Count;
2075 break;
2076 default:
2077 NOT_REACHED();
2079 /* Display the current sort variant */
2080 this->GetWidget<NWidgetCore>(WID_SV_SORT_BY)->widget_data = StationViewWindow::sort_names[index];
2081 this->SetDirty();
2085 * Select a new grouping mode for the cargo view.
2086 * @param index Row being selected in the grouping drop down.
2088 void SelectGroupBy(int index)
2090 this->grouping_index = index;
2091 _settings_client.gui.station_gui_group_order = index;
2092 this->GetWidget<NWidgetCore>(WID_SV_GROUP_BY)->widget_data = StationViewWindow::group_names[index];
2093 switch (StationViewWindow::group_names[index]) {
2094 case STR_STATION_VIEW_GROUP_S_V_D:
2095 this->groupings[1] = GR_SOURCE;
2096 this->groupings[2] = GR_NEXT;
2097 this->groupings[3] = GR_DESTINATION;
2098 break;
2099 case STR_STATION_VIEW_GROUP_S_D_V:
2100 this->groupings[1] = GR_SOURCE;
2101 this->groupings[2] = GR_DESTINATION;
2102 this->groupings[3] = GR_NEXT;
2103 break;
2104 case STR_STATION_VIEW_GROUP_V_S_D:
2105 this->groupings[1] = GR_NEXT;
2106 this->groupings[2] = GR_SOURCE;
2107 this->groupings[3] = GR_DESTINATION;
2108 break;
2109 case STR_STATION_VIEW_GROUP_V_D_S:
2110 this->groupings[1] = GR_NEXT;
2111 this->groupings[2] = GR_DESTINATION;
2112 this->groupings[3] = GR_SOURCE;
2113 break;
2114 case STR_STATION_VIEW_GROUP_D_S_V:
2115 this->groupings[1] = GR_DESTINATION;
2116 this->groupings[2] = GR_SOURCE;
2117 this->groupings[3] = GR_NEXT;
2118 break;
2119 case STR_STATION_VIEW_GROUP_D_V_S:
2120 this->groupings[1] = GR_DESTINATION;
2121 this->groupings[2] = GR_NEXT;
2122 this->groupings[3] = GR_SOURCE;
2123 break;
2125 this->SetDirty();
2128 void OnDropdownSelect(WidgetID widget, int index) override
2130 if (widget == WID_SV_SORT_BY) {
2131 this->SelectSortBy(index);
2132 } else {
2133 this->SelectGroupBy(index);
2137 void OnQueryTextFinished(std::optional<std::string> str) override
2139 if (!str.has_value()) return;
2141 Command<CMD_RENAME_STATION>::Post(STR_ERROR_CAN_T_RENAME_STATION, this->window_number, *str);
2144 void OnResize() override
2146 this->vscroll->SetCapacityFromWidget(this, WID_SV_WAITING, WidgetDimensions::scaled.framerect.Vertical());
2150 * Some data on this window has become invalid. Invalidate the cache for the given cargo if necessary.
2151 * @param data Information about the changed data. If it's a valid cargo ID, invalidate the cargo data.
2152 * @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.
2154 void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override
2156 if (gui_scope) {
2157 if (data >= 0 && data < NUM_CARGO) {
2158 this->cached_destinations.Remove((CargoID)data);
2159 } else {
2160 this->ReInit();
2166 static WindowDesc _station_view_desc(
2167 WDP_AUTO, "view_station", 249, 117,
2168 WC_STATION_VIEW, WC_NONE,
2170 _nested_station_view_widgets
2174 * Opens StationViewWindow for given station
2176 * @param station station which window should be opened
2178 void ShowStationViewWindow(StationID station)
2180 AllocateWindowDescFront<StationViewWindow>(_station_view_desc, station);
2183 /** Struct containing TileIndex and StationID */
2184 struct TileAndStation {
2185 TileIndex tile; ///< TileIndex
2186 StationID station; ///< StationID
2189 static std::vector<TileAndStation> _deleted_stations_nearby;
2190 static std::vector<StationID> _stations_nearby_list;
2193 * Add station on this tile to _stations_nearby_list if it's fully within the
2194 * station spread.
2195 * @param tile Tile just being checked
2196 * @param user_data Pointer to TileArea context
2197 * @tparam T the station filter type
2199 template <class T>
2200 static bool AddNearbyStation(TileIndex tile, void *user_data)
2202 TileArea *ctx = (TileArea *)user_data;
2204 /* First check if there were deleted stations here */
2205 for (auto it = _deleted_stations_nearby.begin(); it != _deleted_stations_nearby.end(); /* nothing */) {
2206 if (it->tile == tile) {
2207 _stations_nearby_list.push_back(it->station);
2208 it = _deleted_stations_nearby.erase(it);
2209 } else {
2210 ++it;
2214 /* Check if own station and if we stay within station spread */
2215 if (!IsTileType(tile, MP_STATION)) return false;
2217 StationID sid = GetStationIndex(tile);
2219 /* This station is (likely) a waypoint */
2220 if (!T::IsValidID(sid)) return false;
2222 BaseStation *st = BaseStation::Get(sid);
2223 if (st->owner != _local_company || std::find(_stations_nearby_list.begin(), _stations_nearby_list.end(), sid) != _stations_nearby_list.end()) return false;
2225 if (st->rect.BeforeAddRect(ctx->tile, ctx->w, ctx->h, StationRect::ADD_TEST).Succeeded()) {
2226 _stations_nearby_list.push_back(sid);
2229 return false; // We want to include *all* nearby stations
2233 * Circulate around the to-be-built station to find stations we could join.
2234 * Make sure that only stations are returned where joining wouldn't exceed
2235 * station spread and are our own station.
2236 * @param ta Base tile area of the to-be-built station
2237 * @param distant_join Search for adjacent stations (false) or stations fully
2238 * within station spread
2239 * @tparam T the station filter type, for stations to look for
2241 template <class T>
2242 static const BaseStation *FindStationsNearby(TileArea ta, bool distant_join)
2244 TileArea ctx = ta;
2246 _stations_nearby_list.clear();
2247 _stations_nearby_list.push_back(NEW_STATION);
2248 _deleted_stations_nearby.clear();
2250 /* Check the inside, to return, if we sit on another station */
2251 for (TileIndex t : ta) {
2252 if (t < Map::Size() && IsTileType(t, MP_STATION) && T::IsValidID(GetStationIndex(t))) return BaseStation::GetByTile(t);
2255 /* Look for deleted stations */
2256 for (const BaseStation *st : BaseStation::Iterate()) {
2257 if (T::IsValidBaseStation(st) && !st->IsInUse() && st->owner == _local_company) {
2258 /* Include only within station spread (yes, it is strictly less than) */
2259 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) {
2260 _deleted_stations_nearby.push_back({st->xy, st->index});
2262 /* Add the station when it's within where we're going to build */
2263 if (IsInsideBS(TileX(st->xy), TileX(ctx.tile), ctx.w) &&
2264 IsInsideBS(TileY(st->xy), TileY(ctx.tile), ctx.h)) {
2265 AddNearbyStation<T>(st->xy, &ctx);
2271 /* Only search tiles where we have a chance to stay within the station spread.
2272 * The complete check needs to be done in the callback as we don't know the
2273 * extent of the found station, yet. */
2274 if (distant_join && std::min(ta.w, ta.h) >= _settings_game.station.station_spread) return nullptr;
2275 uint max_dist = distant_join ? _settings_game.station.station_spread - std::min(ta.w, ta.h) : 1;
2277 TileIndex tile = TileAddByDir(ctx.tile, DIR_N);
2278 CircularTileSearch(&tile, max_dist, ta.w, ta.h, AddNearbyStation<T>, &ctx);
2280 return nullptr;
2283 static constexpr NWidgetPart _nested_select_station_widgets[] = {
2284 NWidget(NWID_HORIZONTAL),
2285 NWidget(WWT_CLOSEBOX, COLOUR_DARK_GREEN),
2286 NWidget(WWT_CAPTION, COLOUR_DARK_GREEN, WID_JS_CAPTION), SetDataTip(STR_JOIN_STATION_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
2287 NWidget(WWT_DEFSIZEBOX, COLOUR_DARK_GREEN),
2288 EndContainer(),
2289 NWidget(NWID_HORIZONTAL),
2290 NWidget(WWT_PANEL, COLOUR_DARK_GREEN, WID_JS_PANEL), SetResize(1, 0), SetScrollbar(WID_JS_SCROLLBAR), EndContainer(),
2291 NWidget(NWID_VERTICAL),
2292 NWidget(NWID_VSCROLLBAR, COLOUR_DARK_GREEN, WID_JS_SCROLLBAR),
2293 NWidget(WWT_RESIZEBOX, COLOUR_DARK_GREEN),
2294 EndContainer(),
2295 EndContainer(),
2299 * Window for selecting stations/waypoints to (distant) join to.
2300 * @tparam T The station filter type, for stations to join with
2302 template <class T>
2303 struct SelectStationWindow : Window {
2304 StationPickerCmdProc select_station_proc;
2305 TileArea area; ///< Location of new station
2306 Scrollbar *vscroll;
2308 SelectStationWindow(WindowDesc &desc, TileArea ta, StationPickerCmdProc&& proc) :
2309 Window(desc),
2310 select_station_proc(std::move(proc)),
2311 area(ta)
2313 this->CreateNestedTree();
2314 this->vscroll = this->GetScrollbar(WID_JS_SCROLLBAR);
2315 this->GetWidget<NWidgetCore>(WID_JS_CAPTION)->widget_data = T::IsWaypoint() ? STR_JOIN_WAYPOINT_CAPTION : STR_JOIN_STATION_CAPTION;
2316 this->FinishInitNested(0);
2317 this->OnInvalidateData(0);
2319 _thd.freeze = true;
2322 void Close([[maybe_unused]] int data = 0) override
2324 SetViewportCatchmentSpecializedStation<typename T::StationType>(nullptr, true);
2326 _thd.freeze = false;
2327 this->Window::Close();
2330 void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override
2332 if (widget != WID_JS_PANEL) return;
2334 /* Determine the widest string */
2335 Dimension d = GetStringBoundingBox(T::IsWaypoint() ? STR_JOIN_WAYPOINT_CREATE_SPLITTED_WAYPOINT : STR_JOIN_STATION_CREATE_SPLITTED_STATION);
2336 for (const auto &station : _stations_nearby_list) {
2337 if (station == NEW_STATION) continue;
2338 const BaseStation *st = BaseStation::Get(station);
2339 SetDParam(0, st->index);
2340 SetDParam(1, st->facilities);
2341 d = maxdim(d, GetStringBoundingBox(T::IsWaypoint() ? STR_STATION_LIST_WAYPOINT : STR_STATION_LIST_STATION));
2344 resize.height = d.height;
2345 d.height *= 5;
2346 d.width += padding.width;
2347 d.height += padding.height;
2348 size = d;
2351 void DrawWidget(const Rect &r, WidgetID widget) const override
2353 if (widget != WID_JS_PANEL) return;
2355 Rect tr = r.Shrink(WidgetDimensions::scaled.framerect);
2356 auto [first, last] = this->vscroll->GetVisibleRangeIterators(_stations_nearby_list);
2357 for (auto it = first; it != last; ++it, tr.top += this->resize.step_height) {
2358 if (*it == NEW_STATION) {
2359 DrawString(tr, T::IsWaypoint() ? STR_JOIN_WAYPOINT_CREATE_SPLITTED_WAYPOINT : STR_JOIN_STATION_CREATE_SPLITTED_STATION);
2360 } else {
2361 const BaseStation *st = BaseStation::Get(*it);
2362 SetDParam(0, st->index);
2363 SetDParam(1, st->facilities);
2364 DrawString(tr, T::IsWaypoint() ? STR_STATION_LIST_WAYPOINT : STR_STATION_LIST_STATION);
2370 void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override
2372 if (widget != WID_JS_PANEL) return;
2374 auto it = this->vscroll->GetScrolledItemFromWidget(_stations_nearby_list, pt.y, this, WID_JS_PANEL, WidgetDimensions::scaled.framerect.top);
2375 if (it == _stations_nearby_list.end()) return;
2377 /* Execute stored Command */
2378 this->select_station_proc(false, *it);
2380 /* Close Window; this might cause double frees! */
2381 CloseWindowById(WC_SELECT_STATION, 0);
2384 void OnRealtimeTick([[maybe_unused]] uint delta_ms) override
2386 if (_thd.dirty & 2) {
2387 _thd.dirty &= ~2;
2388 this->SetDirty();
2392 void OnResize() override
2394 this->vscroll->SetCapacityFromWidget(this, WID_JS_PANEL, WidgetDimensions::scaled.framerect.Vertical());
2398 * Some data on this window has become invalid.
2399 * @param data Information about the changed data.
2400 * @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.
2402 void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override
2404 if (!gui_scope) return;
2405 FindStationsNearby<T>(this->area, true);
2406 this->vscroll->SetCount(_stations_nearby_list.size());
2407 this->SetDirty();
2410 void OnMouseOver([[maybe_unused]] Point pt, WidgetID widget) override
2412 if (widget != WID_JS_PANEL) {
2413 SetViewportCatchmentSpecializedStation<typename T::StationType>(nullptr, true);
2414 return;
2417 /* Show coverage area of station under cursor */
2418 auto it = this->vscroll->GetScrolledItemFromWidget(_stations_nearby_list, pt.y, this, WID_JS_PANEL, WidgetDimensions::scaled.framerect.top);
2419 const typename T::StationType *st = it == _stations_nearby_list.end() || *it == NEW_STATION ? nullptr : T::StationType::Get(*it);
2420 SetViewportCatchmentSpecializedStation<typename T::StationType>(st, true);
2424 static WindowDesc _select_station_desc(
2425 WDP_AUTO, "build_station_join", 200, 180,
2426 WC_SELECT_STATION, WC_NONE,
2427 WDF_CONSTRUCTION,
2428 _nested_select_station_widgets
2433 * Check whether we need to show the station selection window.
2434 * @param cmd Command to build the station.
2435 * @param ta Tile area of the to-be-built station
2436 * @tparam T the station filter type
2437 * @return whether we need to show the station selection window.
2439 template <class T>
2440 static bool StationJoinerNeeded(TileArea ta, const StationPickerCmdProc &proc)
2442 /* Only show selection if distant join is enabled in the settings */
2443 if (!_settings_game.station.distant_join_stations) return false;
2445 /* If a window is already opened and we didn't ctrl-click,
2446 * return true (i.e. just flash the old window) */
2447 Window *selection_window = FindWindowById(WC_SELECT_STATION, 0);
2448 if (selection_window != nullptr) {
2449 /* Abort current distant-join and start new one */
2450 selection_window->Close();
2451 UpdateTileSelection();
2454 /* only show the popup, if we press ctrl */
2455 if (!_ctrl_pressed) return false;
2457 /* Now check if we could build there */
2458 if (!proc(true, INVALID_STATION)) return false;
2460 /* Test for adjacent station or station below selection.
2461 * If adjacent-stations is disabled and we are building next to a station, do not show the selection window.
2462 * but join the other station immediately. */
2463 const BaseStation *st = FindStationsNearby<T>(ta, false);
2464 return st == nullptr && (_settings_game.station.adjacent_stations || std::any_of(std::begin(_stations_nearby_list), std::end(_stations_nearby_list), [](StationID s) { return s != NEW_STATION; }));
2468 * Show the station selection window when needed. If not, build the station.
2469 * @param cmd Command to build the station.
2470 * @param ta Area to build the station in
2471 * @tparam the class to find stations for
2473 template <class T>
2474 void ShowSelectBaseStationIfNeeded(TileArea ta, StationPickerCmdProc&& proc)
2476 if (StationJoinerNeeded<T>(ta, proc)) {
2477 if (!_settings_client.gui.persistent_buildingtools) ResetObjectToPlace();
2478 new SelectStationWindow<T>(_select_station_desc, ta, std::move(proc));
2479 } else {
2480 proc(false, INVALID_STATION);
2485 * Show the station selection window when needed. If not, build the station.
2486 * @param ta Area to build the station in
2487 * @param proc Function called to execute the build command.
2489 void ShowSelectStationIfNeeded(TileArea ta, StationPickerCmdProc proc)
2491 ShowSelectBaseStationIfNeeded<StationTypeFilter>(ta, std::move(proc));
2495 * Show the rail waypoint selection window when needed. If not, build the waypoint.
2496 * @param ta Area to build the waypoint in
2497 * @param proc Function called to execute the build command.
2499 void ShowSelectRailWaypointIfNeeded(TileArea ta, StationPickerCmdProc proc)
2501 ShowSelectBaseStationIfNeeded<RailWaypointTypeFilter>(ta, std::move(proc));
2505 * Show the road waypoint selection window when needed. If not, build the waypoint.
2506 * @param ta Area to build the waypoint in
2507 * @param proc Function called to execute the build command.
2509 void ShowSelectRoadWaypointIfNeeded(TileArea ta, StationPickerCmdProc proc)
2511 ShowSelectBaseStationIfNeeded<RoadWaypointTypeFilter>(ta, std::move(proc));