diff --git a/projects/openttd_vs80.vcproj b/projects/openttd_vs80.vcproj index 17c2d8d4f1..f44e7cff6d 100644 --- a/projects/openttd_vs80.vcproj +++ b/projects/openttd_vs80.vcproj @@ -510,6 +510,10 @@ RelativePath=".\..\src\dedicated.cpp" > + + @@ -1002,6 +1006,18 @@ RelativePath=".\..\src\video\dedicated_v.h" > + + + + + + @@ -2074,6 +2090,10 @@ RelativePath=".\..\src\date_gui.cpp" > + + diff --git a/projects/openttd_vs90.vcproj b/projects/openttd_vs90.vcproj index 666760ce78..dca08811be 100644 --- a/projects/openttd_vs90.vcproj +++ b/projects/openttd_vs90.vcproj @@ -507,6 +507,10 @@ RelativePath=".\..\src\dedicated.cpp" > + + @@ -999,6 +1003,18 @@ RelativePath=".\..\src\video\dedicated_v.h" > + + + + + + @@ -2071,6 +2087,10 @@ RelativePath=".\..\src\date_gui.cpp" > + + diff --git a/source.list b/source.list index 3b66ab9fca..51fc5e5257 100644 --- a/source.list +++ b/source.list @@ -18,6 +18,7 @@ currency.cpp date.cpp debug.cpp dedicated.cpp +departures.cpp depot.cpp disaster_vehicle.cpp driver.cpp @@ -171,6 +172,9 @@ date_gui.h date_type.h debug.h video/dedicated_v.h +departures_func.h +departures_gui.h +departures_type.h depot_base.h depot_func.h depot_map.h @@ -456,6 +460,7 @@ cheat_gui.cpp company_gui.cpp console_gui.cpp date_gui.cpp +departures_gui.cpp depot_gui.cpp dock_gui.cpp engine_gui.cpp diff --git a/src/base_consist.h b/src/base_consist.h index 3679afd351..474ab97415 100644 --- a/src/base_consist.h +++ b/src/base_consist.h @@ -14,6 +14,7 @@ #include "order_type.h" #include "date_type.h" +#include "timetable.h" /** Various front vehicle properties that are preserved when autoreplacing, using order-backup or switching front engines within a consist. */ struct BaseConsist { @@ -22,7 +23,11 @@ struct BaseConsist { /* Used for timetabling. */ uint32 current_order_time; ///< How many ticks have passed since this order started. int32 lateness_counter; ///< How many ticks late (or early if negative) this vehicle is. +#if WALLCLOCK_NETWORK_COMPATIBLE Date timetable_start; ///< When the vehicle is supposed to start the timetable. +#else + DateTicks timetable_start; ///< When the vehicle is supposed to start the timetable. +#endif uint16 service_interval; ///< The interval for (automatic) servicing; either in days or %. diff --git a/src/date_func.h b/src/date_func.h index 6bbde59556..9ac87af61b 100644 --- a/src/date_func.h +++ b/src/date_func.h @@ -24,6 +24,8 @@ void SetDate(Date date, DateFract fract); void ConvertDateToYMD(Date date, YearMonthDay *ymd); Date ConvertYMDToDate(Year year, Month month, Day day); +#define YMD_TO_DATE(ymd) (ConvertYMDToDate(ymd.year, ymd.month, ymd.day)) + /** * Checks whether the given year is a leap year or not. * @param yr The year to check. diff --git a/src/date_gui.cpp b/src/date_gui.cpp index 468a74db99..b5b5bce4a2 100644 --- a/src/date_gui.cpp +++ b/src/date_gui.cpp @@ -16,6 +16,7 @@ #include "window_gui.h" #include "date_gui.h" #include "core/geometry_func.hpp" +#include "settings_type.h" #include "widgets/dropdown_type.h" #include "widgets/date_widget.h" @@ -65,7 +66,7 @@ struct SetDateWindow : Window { * Helper function to construct the dropdown. * @param widget the dropdown widget to create the dropdown for */ - void ShowDateDropDown(int widget) + virtual void ShowDateDropDown(int widget) { int selected; DropDownList *list = new DropDownList(); @@ -146,9 +147,8 @@ struct SetDateWindow : Window { case WID_SD_YEAR: ShowDateDropDown(widget); break; - case WID_SD_SET_DATE: - if (this->callback != NULL) this->callback(this, ConvertYMDToDate(this->date.year, this->date.month, this->date.day)); + if (this->callback != NULL) this->callback(this, ConvertYMDToDate(this->date.year, this->date.month, this->date.day) * DAY_TICKS); delete this; break; } @@ -173,6 +173,122 @@ struct SetDateWindow : Window { } }; +struct SetMinutesWindow : SetDateWindow +{ + Minutes minutes; + + /** Constructor. */ + SetMinutesWindow(WindowDesc *desc, WindowNumber window_number, Window *parent, DateTicks initial_date, Year min_year, Year max_year, SetDateCallback *callback) : + SetDateWindow(desc, window_number, parent, initial_date, min_year, max_year, callback), + minutes(initial_date / _settings_client.gui.ticks_per_minute) + { + } + + /** + * Helper function to construct the dropdown. + * @param widget the dropdown widget to create the dropdown for + */ + virtual void ShowDateDropDown(int widget) + { + int selected; + DropDownList *list = new DropDownList(); + + switch (widget) { + default: NOT_REACHED(); + + case WID_SD_DAY: + for (uint i = 0; i < 60; i++) { + DropDownListParamStringItem *item = new DropDownListParamStringItem(STR_JUST_INT, i, false); + item->SetParam(0, i); + *list->Append() = item; + } + selected = MINUTES_MINUTE(minutes); + break; + + case WID_SD_MONTH: + for (uint i = 0; i < 24; i++) { + DropDownListParamStringItem *item = new DropDownListParamStringItem(STR_JUST_INT, i, false); + item->SetParam(0, i); + *list->Append() = item; + } + selected = MINUTES_HOUR(minutes); + + break; + } + + ShowDropDownList(this, list, selected, widget); + } + + virtual void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) + { + Dimension d = {0, 0}; + switch (widget) { + default: return; + + case WID_SD_DAY: + for (uint i = 0; i < 60; i++) { + SetDParam(0, i); + d = maxdim(d, GetStringBoundingBox(STR_JUST_INT)); + } + break; + + case WID_SD_MONTH: + for (uint i = 0; i < 24; i++) { + SetDParam(0, i); + d = maxdim(d, GetStringBoundingBox(STR_JUST_INT)); + } + break; + } + + d.width += padding.width; + d.height += padding.height; + *size = d; + } + + virtual void SetStringParameters(int widget) const + { + switch (widget) { + case WID_SD_DAY: SetDParam(0, MINUTES_MINUTE(minutes)); break; + case WID_SD_MONTH: SetDParam(0, MINUTES_HOUR(minutes)); break; + } + } + + virtual void OnClick(Point pt, int widget, int click_count) + { + switch (widget) { + case WID_SD_DAY: + case WID_SD_MONTH: + case WID_SD_YEAR: + ShowDateDropDown(widget); + break; + + case WID_SD_SET_DATE: + if (this->callback != NULL) this->callback(this->parent, ((DateTicks)minutes - _settings_client.gui.clock_offset) * _settings_client.gui.ticks_per_minute); + delete this; + break; + } + } + + virtual void OnDropdownSelect(int widget, int index) + { + Minutes current = 0; + switch (widget) { + case WID_SD_DAY: + current = MINUTES_DATE(MINUTES_DAY(CURRENT_MINUTE), MINUTES_HOUR(minutes), index); + break; + + case WID_SD_MONTH: + current = MINUTES_DATE(MINUTES_DAY(CURRENT_MINUTE), index, MINUTES_MINUTE(minutes)); + break; + } + + if (current < (CURRENT_MINUTE - 60)) current += 60 * 24; + minutes = current; + + this->SetDirty(); + } +}; + /** Widgets for the date setting window. */ static const NWidgetPart _nested_set_date_widgets[] = { NWidget(NWID_HORIZONTAL), @@ -195,6 +311,26 @@ static const NWidgetPart _nested_set_date_widgets[] = { EndContainer() }; +static const NWidgetPart _nested_set_minutes_widgets[] = { + NWidget(NWID_HORIZONTAL), + NWidget(WWT_CLOSEBOX, COLOUR_BROWN), + NWidget(WWT_CAPTION, COLOUR_BROWN), SetDataTip(STR_DATE_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS), + EndContainer(), + NWidget(WWT_PANEL, COLOUR_BROWN), + NWidget(NWID_VERTICAL), SetPIP(6, 6, 6), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), SetPIP(6, 6, 6), + NWidget(WWT_DROPDOWN, COLOUR_ORANGE, WID_SD_MONTH), SetFill(1, 0), SetDataTip(STR_JUST_INT, STR_DATE_MINUTES_MONTH_TOOLTIP), + NWidget(WWT_DROPDOWN, COLOUR_ORANGE, WID_SD_DAY), SetFill(1, 0), SetDataTip(STR_JUST_INT, STR_DATE_MINUTES_DAY_TOOLTIP), + EndContainer(), + NWidget(NWID_HORIZONTAL), + NWidget(NWID_SPACER), SetFill(1, 0), + NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_SD_SET_DATE), SetMinimalSize(100, 12), SetDataTip(STR_DATE_SET_DATE, STR_DATE_SET_DATE_TOOLTIP), + NWidget(NWID_SPACER), SetFill(1, 0), + EndContainer(), + EndContainer(), + EndContainer() +}; + /** Description of the date setting window. */ static WindowDesc _set_date_desc( WDP_CENTER, NULL, 0, 0, @@ -203,6 +339,13 @@ static WindowDesc _set_date_desc( _nested_set_date_widgets, lengthof(_nested_set_date_widgets) ); +static WindowDesc _set_minutes_desc( + WDP_CENTER, NULL, 0, 0, + WC_SET_DATE, WC_NONE, + 0, + _nested_set_minutes_widgets, lengthof(_nested_set_minutes_widgets) +); + /** * Create the new 'set date' window * @param window_number number for the window @@ -212,8 +355,13 @@ static WindowDesc _set_date_desc( * @param max_year the maximum year (inclusive) to show in the year dropdown * @param callback the callback to call once a date has been selected */ -void ShowSetDateWindow(Window *parent, int window_number, Date initial_date, Year min_year, Year max_year, SetDateCallback *callback) +void ShowSetDateWindow(Window *parent, int window_number, DateTicks initial_date, Year min_year, Year max_year, SetDateCallback *callback) { DeleteWindowByClass(WC_SET_DATE); - new SetDateWindow(&_set_date_desc, window_number, parent, initial_date, min_year, max_year, callback); + + if (!_settings_client.gui.time_in_minutes) { + new SetDateWindow(&_set_date_desc, window_number, parent, initial_date / DAY_TICKS, min_year, max_year, callback); + } else { + new SetMinutesWindow(&_set_minutes_desc, window_number, parent, initial_date + (_settings_client.gui.clock_offset * _settings_client.gui.ticks_per_minute), min_year, max_year, callback); + } } diff --git a/src/date_gui.h b/src/date_gui.h index 314baba3ca..0c8dcba865 100644 --- a/src/date_gui.h +++ b/src/date_gui.h @@ -20,8 +20,8 @@ * @param w the window that sends the callback * @param date the date that has been chosen */ -typedef void SetDateCallback(const Window *w, Date date); +typedef void SetDateCallback(const Window *w, DateTicks date); -void ShowSetDateWindow(Window *parent, int window_number, Date initial_date, Year min_year, Year max_year, SetDateCallback *callback); +void ShowSetDateWindow(Window *parent, int window_number, DateTicks initial_date, Year min_year, Year max_year, SetDateCallback *callback); #endif /* DATE_GUI_H */ diff --git a/src/date_type.h b/src/date_type.h index b20ace91ec..505b289605 100644 --- a/src/date_type.h +++ b/src/date_type.h @@ -12,10 +12,11 @@ #ifndef DATE_TYPE_H #define DATE_TYPE_H - typedef int32 Date; ///< The type to store our dates in typedef uint16 DateFract; ///< The fraction of a date we're in, i.e. the number of ticks since the last date changeover typedef int32 Ticks; ///< The type to store ticks in +typedef int32 DateTicks; ///< The type to store dates in when tick-precision is required +typedef int32 Minutes; ///< The type to store minutes in typedef int32 Year; ///< Type for the year, note: 0 based, i.e. starts at the year 0. typedef uint8 Month; ///< Type for the month, note: 0 based, i.e. 0 = January, 11 = December. @@ -31,6 +32,8 @@ static const int DAY_TICKS = 74; ///< ticks per day static const int DAYS_IN_YEAR = 365; ///< days per year static const int DAYS_IN_LEAP_YEAR = 366; ///< sometimes, you need one day more... +#define DATE_UNIT_SIZE (_settings_client.gui.time_in_minutes ? _settings_client.gui.ticks_per_minute : DAY_TICKS) + static const int STATION_RATING_TICKS = 185; ///< cycle duration for updating station rating static const int STATION_ACCEPTANCE_TICKS = 250; ///< cycle duration for updating station acceptance static const int STATION_LINKGRAPH_TICKS = 504; ///< cycle duration for cleaning dead links @@ -39,7 +42,6 @@ static const int INDUSTRY_PRODUCE_TICKS = 256; ///< cycle duration for industr static const int TOWN_GROWTH_TICKS = 70; ///< cycle duration for towns trying to grow. (this originates from the size of the town array in TTD static const int INDUSTRY_CUT_TREE_TICKS = INDUSTRY_PRODUCE_TICKS * 2; ///< cycle duration for lumber mill's extra action - /* * ORIGINAL_BASE_YEAR, ORIGINAL_MAX_YEAR and DAYS_TILL_ORIGINAL_BASE_YEAR are * primarily used for loading newgrf and savegame data and returning some @@ -96,6 +98,21 @@ static const Year MAX_YEAR = 5000000; /** The number of days till the last day */ #define MAX_DAY (DAYS_TILL(MAX_YEAR + 1) - 1) +/** The day when converting to minutes */ +#define MINUTES_DAY(minutes) (minutes / 1440) + +/** The hour when converting to minutes */ +#define MINUTES_HOUR(minutes) ((minutes / 60) % 24) + +/** The day when converting to minutes */ +#define MINUTES_MINUTE(minutes) (minutes % 60) + +/** Convert minutes to a date */ +#define MINUTES_DATE(day, hour, minute) ((day * 1440) + (hour * 60) + minute) + +/** Get the current date in minutes */ +#define CURRENT_MINUTE ((((DateTicks)_date * DAY_TICKS) + _date_fract) / _settings_client.gui.ticks_per_minute) + /** * Data structure to convert between Date and triplet (year, month, and day). * @see ConvertDateToYMD(), ConvertYMDToDate() diff --git a/src/departures.cpp b/src/departures.cpp new file mode 100644 index 0000000000..ab0ebb231c --- /dev/null +++ b/src/departures.cpp @@ -0,0 +1,644 @@ +/* $Id$ */ + +/* + * This file is part of OpenTTD. + * 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. + * 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. + * 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 . + */ + +/** @file departures.cpp Scheduled departures from a station. */ + +#include "stdafx.h" +#include "debug.h" +#include "gui.h" +#include "textbuf_gui.h" +#include "strings_func.h" +#include "window_func.h" +#include "vehicle_func.h" +#include "string_func.h" +#include "window_gui.h" +#include "timetable.h" +#include "vehiclelist.h" +#include "company_base.h" +#include "date_func.h" +#include "departures_gui.h" +#include "station_base.h" +#include "vehicle_gui_base.h" +#include "vehicle_base.h" +#include "vehicle_gui.h" +#include "order_base.h" +#include "settings_type.h" +#include "core/smallvec_type.hpp" +#include "date_type.h" +#include "company_type.h" +#include "cargo_type.h" +#include "departures_func.h" +#include "departures_type.h" + +/** A scheduled order. */ +typedef struct OrderDate +{ + const Order *order; ///< The order + const Vehicle *v; ///< The vehicle carrying out the order + DateTicks expected_date;///< The date on which the order is expected to complete + Ticks lateness; ///< How late this order is expected to finish + DepartureStatus status; ///< Whether the vehicle has arrived to carry out the order yet +} OrderDate; + +static bool IsDeparture(const Order *order, StationID station) { + return (order->GetType() == OT_GOTO_STATION && + (StationID)order->GetDestination() == station && + (order->GetLoadType() != OLFB_NO_LOAD || + _settings_client.gui.departure_show_all_stops) && + order->GetWaitTime() != 0); +} + +static bool IsVia(const Order *order, StationID station) { + return ((order->GetType() == OT_GOTO_STATION || + order->GetType() == OT_GOTO_WAYPOINT) && + (StationID)order->GetDestination() == station && + (order->GetNonStopType() == ONSF_NO_STOP_AT_ANY_STATION || + order->GetNonStopType() == ONSF_NO_STOP_AT_DESTINATION_STATION)); +} + +static bool IsArrival(const Order *order, StationID station) { + return (order->GetType() == OT_GOTO_STATION && + (StationID)order->GetDestination() == station && + (order->GetUnloadType() != OUFB_NO_UNLOAD || + _settings_client.gui.departure_show_all_stops) && + order->GetWaitTime() != 0); +} + +/** + * Compute an up-to-date list of departures for a station. + * @param station the station to compute the departures of + * @param show_vehicle_types the types of vehicles to include in the departure list + * @param type the type of departures to get (departures or arrivals) + * @param show_vehicles_via whether to include vehicles that have this station in their orders but do not stop at it + * @return a list of departures, which is empty if an error occurred + */ +DepartureList* MakeDepartureList(StationID station, bool show_vehicle_types[5], DepartureType type, bool show_vehicles_via) +{ + /* This function is the meat of the departure boards functionality. */ + /* As an overview, it works by repeatedly considering the best possible next departure to show. */ + /* By best possible we mean the one expected to arrive at the station first. */ + /* However, we do not consider departures whose scheduled time is too far in the future, even if they are expected before some delayed ones. */ + /* This code can probably be made more efficient. I haven't done so in order to keep both its (relative) simplicity and my (relative) sanity. */ + /* Having written that, it's not exactly slow at the moment. */ + + /* The list of departures which will be returned as a result. */ + SmallVector *result = new SmallVector(); + + /* A list of the next scheduled orders to be considered for inclusion in the departure list. */ + SmallVector next_orders; + + /* The maximum possible date for departures to be scheduled to occur. */ + DateTicks max_date = _settings_client.gui.max_departure_time * DAY_TICKS; + + /* The scheduled order in next_orders with the earliest expected_date field. */ + OrderDate *least_order = NULL; + + /* Get all the vehicles stopping at this station. */ + /* We do this to get the order which is the first time they will stop at this station. */ + /* This order is stored along with some more information. */ + /* We keep a pointer to the `least' order (the one with the soonest expected completion time). */ + for (uint i = 0; i < 4; ++i) { + VehicleList vehicles; + + if (!show_vehicle_types[i]) { + /* Don't show vehicles whose type we're not interested in. */ + continue; + } + + /* MAX_COMPANIES is probably the wrong thing to put here, but it works. GenerateVehicleSortList doesn't check the company when the type of list is VL_STATION_LIST (r20801). */ + if (!GenerateVehicleSortList(&vehicles, VehicleListIdentifier(VL_STATION_LIST, (VehicleType)(VEH_TRAIN + i), MAX_COMPANIES, station).Pack())) { + /* Something went wrong: panic! */ + return result; + } + + /* Get the first order for each vehicle for the station we're interested in that doesn't have No Loading set. */ + /* We find the least order while we're at it. */ + for (const Vehicle **v = vehicles.Begin(); v != vehicles.End(); v++) { + if (_settings_client.gui.departure_only_passengers) { + bool carries_passengers = false; + + const Vehicle *u = *v; + while (u != NULL) { + if (u->cargo_type == CT_PASSENGERS && u->cargo_cap > 0) { + carries_passengers = true; + break; + } + u = u->Next(); + } + + if (carries_passengers == false) { + continue; + } + } + + const Order *order = (*v)->GetOrder((*v)->cur_implicit_order_index % (*v)->GetNumOrders()); + DateTicks start_date = (DateTicks)_date_fract - (*v)->current_order_time; + DepartureStatus status = D_TRAVELLING; + + /* If the vehicle is stopped in a depot, ignore it. */ + if ((*v)->IsStoppedInDepot()) { + continue; + } + + /* If the vehicle is heading for a depot to stop there, then its departures are cancelled. */ + if ((*v)->current_order.IsType(OT_GOTO_DEPOT) && (*v)->current_order.GetDepotActionType() & ODATFB_HALT) { + status = D_CANCELLED; + } + + if ((*v)->current_order.IsType(OT_LOADING)) { + /* Account for the vehicle having reached the current order and being in the loading phase. */ + status = D_ARRIVED; + start_date -= order->GetTravelTime() + (((*v)->lateness_counter < 0) ? (*v)->lateness_counter : 0); + } + + /* Loop through the vehicle's orders until we've found a suitable order or we've determined that no such order exists. */ + /* We only need to consider each order at most once. */ + for (int i = (*v)->GetNumOrders(); i > 0; --i) { + start_date += order->GetTravelTime() + order->GetWaitTime(); + + /* If the scheduled departure date is too far in the future, stop. */ + if (start_date - (*v)->lateness_counter > max_date) { + break; + } + + /* If the order is a conditional branch, handle it. */ + if (order->IsType(OT_CONDITIONAL)) { + switch(_settings_client.gui.departure_conditionals) { + case 0: { + /* Give up */ + break; + } + case 1: { + /* Take the branch */ + if (status != D_CANCELLED) { + status = D_TRAVELLING; + } + order = (*v)->GetOrder(order->GetConditionSkipToOrder()); + if (order == NULL) { + break; + } + + start_date -= order->GetTravelTime(); + + continue; + } + case 2: { + /* Do not take the branch */ + if (status != D_CANCELLED) { + status = D_TRAVELLING; + } + order = (order->next == NULL) ? (*v)->GetFirstOrder() : order->next; + continue; + } + } + } + + /* Skip it if it's an automatic order. */ + if (order->IsType(OT_IMPLICIT)) { + order = (order->next == NULL) ? (*v)->GetFirstOrder() : order->next; + continue; + } + + /* If an order doesn't have a travel time set, then stop. */ + if (order->GetTravelTime() == 0) { + break; + } + + /* If the vehicle will be stopping at and loading from this station, and its wait time is not zero, then it is a departure. */ + /* If the vehicle will be stopping at and unloading at this station, and its wait time is not zero, then it is an arrival. */ + if ((type == D_DEPARTURE && IsDeparture(order, station)) || + (type == D_DEPARTURE && show_vehicles_via && IsVia(order, station)) || + (type == D_ARRIVAL && IsArrival(order, station))) { + /* If the departure was scheduled to have already begun and has been cancelled, do not show it. */ + if (start_date < 0 && status == D_CANCELLED) { + break; + } + + OrderDate *od = new OrderDate(); + od->order = order; + od->v = *v; + /* We store the expected date for now, so that vehicles will be shown in order of expected time. */ + od->expected_date = start_date; + od->lateness = (*v)->lateness_counter > 0 ? (*v)->lateness_counter : 0; + od->status = status; + + /* If we are early, use the scheduled date as the expected date. We also take lateness to be zero. */ + if ((*v)->lateness_counter < 0 && !(*v)->current_order.IsType(OT_LOADING)) { + od->expected_date -= (*v)->lateness_counter; + } + + /* Update least_order if this is the current least order. */ + if (least_order == NULL) { + least_order = od; + } else if (least_order->expected_date - least_order->lateness - (type == D_ARRIVAL ? least_order->order->GetWaitTime() : 0) > od->expected_date - od->lateness - (type == D_ARRIVAL ? od->order->GetWaitTime() : 0)) { + least_order = od; + } + + *(next_orders.Append(1)) = od; + + /* We're done with this vehicle. */ + break; + } else { + /* Go to the next order in the list. */ + if (status != D_CANCELLED) { + status = D_TRAVELLING; + } + order = (order->next == NULL) ? (*v)->GetFirstOrder() : order->next; + } + } + } + } + + /* No suitable orders found? Then stop. */ + if (next_orders.Length() == 0) { + return result; + } + + /* We now find as many departures as we can. It's a little involved so I'll try to explain each major step. */ + /* The countdown from 10000 is a safeguard just in case something nasty happens. 10000 seemed large enough. */ + for(int i = 10000; i > 0; --i) { + /* I should probably try to convince you that this loop always terminates regardless of the safeguard. */ + /* 1. next_orders contains at least one element. */ + /* 2. The loop terminates if result->Length() exceeds a fixed (for this loop) value, or if the least order's scheduled date is later than max_date. */ + /* (We ignore the case that the least order's scheduled date has overflown, as it is a relative rather than absolute date.) */ + /* 3. Every time we loop round, either result->Length() will have increased -OR- we will have increased the expected_date of one of the elements of next_orders. */ + /* 4. Therefore the loop must eventually terminate. */ + + /* least_order is the best candidate for the next departure. */ + + /* First, we check if we can stop looking for departures yet. */ + if (result->Length() >= _settings_client.gui.max_departures || + least_order->expected_date - least_order->lateness > max_date) { + break; + } + + /* We already know the least order and that it's a suitable departure, so make it into a departure. */ + Departure *d = new Departure(); + d->scheduled_date = (DateTicks)_date * DAY_TICKS + least_order->expected_date - least_order->lateness; + d->lateness = least_order->lateness; + d->status = least_order->status; + d->vehicle = least_order->v; + d->type = type; + d->order = least_order->order; + + /* We'll be going through the order list later, so we need a separate variable for it. */ + const Order *order = least_order->order; + + if (type == D_DEPARTURE) { + /* Computing departures: */ + /* We want to find out where it will terminate, making a list of the stations it calls at along the way. */ + /* We only count stations where unloading happens as being called at - i.e. pickup-only stations are ignored. */ + /* Where the vehicle terminates is defined as the last unique station called at by the vehicle from the current order. */ + + /* If the vehicle loops round to the current order without a terminus being found, then it terminates upon reaching its current order again. */ + + /* We also determine which station this departure is going via, if any. */ + /* A departure goes via a station if it is the first station for which the vehicle has an order to go via or non-stop via. */ + /* Multiple departures on the same journey may go via different stations. That a departure can go via at most one station is intentional. */ + + /* We keep track of potential via stations along the way. If we call at a station immediately after going via it, then it is the via station. */ + StationID candidate_via = INVALID_STATION; + + /* Go through the order list, looping if necessary, to find a terminus. */ + /* Get the next order, which may be the vehicle's first order. */ + order = (order->next == NULL) ? least_order->v->GetFirstOrder() : order->next; + /* We only need to consider each order at most once. */ + bool found_terminus = false; + CallAt c = CallAt((StationID)order->GetDestination(), d->scheduled_date); + for (int i = least_order->v->GetNumOrders(); i > 0; --i) { + /* If we reach the order at which the departure occurs again, then use the departure station as the terminus. */ + if (order == least_order->order) { + /* If we're not calling anywhere, then skip this departure. */ + found_terminus = (d->calling_at.Length() > 0); + break; + } + + /* If the order is a conditional branch, handle it. */ + if (order->IsType(OT_CONDITIONAL)) { + switch(_settings_client.gui.departure_conditionals) { + case 0: { + /* Give up */ + break; + } + case 1: { + /* Take the branch */ + order = least_order->v->GetOrder(order->GetConditionSkipToOrder()); + if (order == NULL) { + break; + } + continue; + } + case 2: { + /* Do not take the branch */ + order = (order->next == NULL) ? least_order->v->GetFirstOrder() : order->next; + continue; + } + } + } + + /* If we reach the original station again, then use it as the terminus. */ + if (order->GetType() == OT_GOTO_STATION && + (StationID)order->GetDestination() == station && + (order->GetUnloadType() != OUFB_NO_UNLOAD || + _settings_client.gui.departure_show_all_stops) && + order->GetNonStopType() != ONSF_NO_STOP_AT_ANY_STATION && + order->GetNonStopType() != ONSF_NO_STOP_AT_DESTINATION_STATION) { + /* If we're not calling anywhere, then skip this departure. */ + found_terminus = (d->calling_at.Length() > 0); + break; + } + + /* Check if we're going via this station. */ + if ((order->GetNonStopType() == ONSF_NO_STOP_AT_ANY_STATION || + order->GetNonStopType() == ONSF_NO_STOP_AT_DESTINATION_STATION) && + order->GetType() == OT_GOTO_STATION && + d->via == INVALID_STATION) { + candidate_via = (StationID)order->GetDestination(); + } + + if (c.scheduled_date != 0 && order->GetTravelTime() != 0) { + c.scheduled_date += order->GetTravelTime(); + } else { + c.scheduled_date = 0; + } + + c.station = (StationID)order->GetDestination(); + + /* We're not interested in this order any further if we're not calling at it. */ + if ((order->GetUnloadType() == OUFB_NO_UNLOAD && + !_settings_client.gui.departure_show_all_stops) || + (order->GetType() != OT_GOTO_STATION && + order->GetType() != OT_IMPLICIT) || + order->GetNonStopType() == ONSF_NO_STOP_AT_ANY_STATION || + order->GetNonStopType() == ONSF_NO_STOP_AT_DESTINATION_STATION) { + c.scheduled_date += order->GetWaitTime(); + order = (order->next == NULL) ? least_order->v->GetFirstOrder() : order->next; + continue; + } + + /* If this order's station is already in the calling, then the previous called at station is the terminus. */ + if (d->calling_at.Contains(c)) { + found_terminus = true; + break; + } + + /* If appropriate, add the station to the calling at list and make it the candidate terminus. */ + if ((order->GetType() == OT_GOTO_STATION || + order->GetType() == OT_IMPLICIT) && + order->GetNonStopType() != ONSF_NO_STOP_AT_ANY_STATION && + order->GetNonStopType() != ONSF_NO_STOP_AT_DESTINATION_STATION) { + if (d->via == INVALID_STATION && candidate_via == (StationID)order->GetDestination()) { + d->via = (StationID)order->GetDestination(); + } + d->terminus = c; + *(d->calling_at.Append(1)) = c; + } + + /* If we unload all at this station, then it is the terminus. */ + if (order->GetType() == OT_GOTO_STATION && + order->GetUnloadType() == OUFB_UNLOAD) { + if (d->calling_at.Length() > 0) { + found_terminus = true; + } + break; + } + + c.scheduled_date += order->GetWaitTime(); + + /* Get the next order, which may be the vehicle's first order. */ + order = (order->next == NULL) ? least_order->v->GetFirstOrder() : order->next; + } + + if (found_terminus) { + /* Add the departure to the result list. */ + bool duplicate = false; + + if (_settings_client.gui.departure_merge_identical) { + for (uint i = 0; i < result->Length(); ++i) { + if (*d == **(result->Get(i))) { + duplicate = true; + break; + } + } + } + + if (!duplicate) { + *(result->Append(1)) = d; + + if (_settings_client.gui.departure_smart_terminus && type == D_DEPARTURE) { + for (uint i = 0; i < result->Length()-1; ++i) { + Departure *d_first = *(result->Get(i)); + uint k = d_first->calling_at.Length()-2; + for (uint j = d->calling_at.Length(); j > 0; --j) { + CallAt c = CallAt(*(d->calling_at.Get(j-1))); + + if (d_first->terminus >= c && d_first->calling_at.Length() >= 2) { + d_first->terminus = CallAt(*(d_first->calling_at.Get(k))); + + if (k == 0) break; + + k--; + } + } + } + } + + /* If the vehicle is expected to be late, we want to know what time it will arrive rather than depart. */ + /* This is done because it looked silly to me to have a vehicle not be expected for another few days, yet it be at the same time pulling into the station. */ + if (d->status != D_ARRIVED && + d->lateness > 0) { + d->lateness -= least_order->order->GetWaitTime(); + } + } + } + } else { + /* Computing arrivals: */ + /* First we need to find the origin of the order. This is somewhat like finding a terminus, but a little more involved since order lists are singly linked. */ + /* The next stage is simpler. We just need to add all the stations called at on the way to the current station. */ + /* Again, we define a station as being called at if the vehicle loads from it. */ + + /* However, the very first thing we do is use the arrival time as the scheduled time instead of the departure time. */ + d->scheduled_date -= order->GetWaitTime(); + + const Order *candidate_origin = (order->next == NULL) ? least_order->v->GetFirstOrder() : order->next; + bool found_origin = false; + + while (candidate_origin != least_order->order) { + if ((candidate_origin->GetLoadType() != OLFB_NO_LOAD || + _settings_client.gui.departure_show_all_stops) && + (candidate_origin->GetType() == OT_GOTO_STATION || + candidate_origin->GetType() == OT_IMPLICIT) && + candidate_origin->GetDestination() != station) { + const Order *o = (candidate_origin->next == NULL) ? least_order->v->GetFirstOrder() : candidate_origin->next; + bool found_collision = false; + + /* Check if the candidate origin's destination appears again before the original order or the station does. */ + while (o != least_order->order) { + if (o->GetUnloadType() == OUFB_UNLOAD) { + found_collision = true; + break; + } + + if ((o->GetType() == OT_GOTO_STATION || + o->GetType() == OT_IMPLICIT) && + (o->GetDestination() == candidate_origin->GetDestination() || + o->GetDestination() == station)) { + found_collision = true; + break; + } + + o = (o->next == NULL) ? least_order->v->GetFirstOrder() : o->next; + } + + /* If it doesn't, then we have found the origin. */ + if (!found_collision) { + found_origin = true; + break; + } + } + + candidate_origin = (candidate_origin->next == NULL) ? least_order->v->GetFirstOrder() : candidate_origin->next; + } + + order = (candidate_origin->next == NULL) ? least_order->v->GetFirstOrder() : candidate_origin->next; + + while (order != least_order->order) { + if (order->GetType() == OT_GOTO_STATION && + (order->GetLoadType() != OLFB_NO_LOAD || + _settings_client.gui.departure_show_all_stops)) { + *(d->calling_at.Append(1)) = CallAt((StationID)order->GetDestination()); + } + + order = (order->next == NULL) ? least_order->v->GetFirstOrder() : order->next; + } + + d->terminus = CallAt((StationID)candidate_origin->GetDestination()); + + if (found_origin) { + bool duplicate = false; + + if (_settings_client.gui.departure_merge_identical) { + for (uint i = 0; i < result->Length(); ++i) { + if (*d == **(result->Get(i))) { + duplicate = true; + break; + } + } + } + + if (!duplicate) { + *(result->Append(1)) = d; + } + } + } + + /* Save on pointer dereferences in the coming loop. */ + order = least_order->order; + + /* Now we find the next suitable order for being a departure for this vehicle. */ + /* We do this in a similar way to finding the first suitable order for the vehicle. */ + + /* Go to the next order so we don't add the current order again. */ + order = (order->next == NULL) ? least_order->v->GetFirstOrder() : order->next; + least_order->expected_date += order->GetTravelTime() + order->GetWaitTime(); + + /* Go through the order list to find the next candidate departure. */ + /* We only need to consider each order at most once. */ + bool found_next_order = false; + for (int i = least_order->v->GetNumOrders(); i > 0; --i) { + /* If the order is a conditional branch, handle it. */ + if (order->IsType(OT_CONDITIONAL)) { + switch(_settings_client.gui.departure_conditionals) { + case 0: { + /* Give up */ + break; + } + case 1: { + /* Take the branch */ + order = least_order->v->GetOrder(order->GetConditionSkipToOrder()); + if (order == NULL) { + break; + } + + least_order->expected_date += order->GetWaitTime(); + + continue; + } + case 2: { + /* Do not take the branch */ + order = (order->next == NULL) ? least_order->v->GetFirstOrder() : order->next; + least_order->expected_date += order->GetTravelTime() + order->GetWaitTime(); + continue; + } + } + } + + /* Skip it if it's an automatic order. */ + if (order->IsType(OT_IMPLICIT)) { + order = (order->next == NULL) ? least_order->v->GetFirstOrder() : order->next; + continue; + } + + /* If an order doesn't have a travel time set, then stop. */ + if (order->GetTravelTime() == 0) { + break; + } + + /* If the departure is scheduled to be too late, then stop. */ + if (least_order->expected_date - least_order->lateness > max_date) { + break; + } + + /* If the order loads from this station (or unloads if we're computing arrivals) and has a wait time set, then it is suitable for being a departure. */ + if ((type == D_DEPARTURE && IsDeparture(order, station)) || + (type == D_DEPARTURE && show_vehicles_via && IsVia(order, station)) || + (type == D_ARRIVAL && IsArrival(order, station))) { + least_order->order = order; + found_next_order = true; + break; + } + + order = (order->next == NULL) ? least_order->v->GetFirstOrder() : order->next; + least_order->expected_date += order->GetTravelTime() + order->GetWaitTime(); + } + + /* If we didn't find a suitable order for being a departure, then we can ignore this vehicle from now on. */ + if (!found_next_order) { + /* Make sure we don't try to get departures out of this order. */ + /* This is cheaper than deleting it from next_orders. */ + /* If we ever get to a state where _date * DAY_TICKS is close to INT_MAX, then we'll have other problems anyway as departures' scheduled dates will wrap around. */ + least_order->expected_date = INT32_MAX; + } + + /* The vehicle can't possibly have arrived at its next candidate departure yet. */ + if (least_order->status == D_ARRIVED) { + least_order->status = D_TRAVELLING; + } + + /* Find the new least order. */ + for (uint i = 0; i < next_orders.Length(); ++i) { + OrderDate *od = *(next_orders.Get(i)); + + DateTicks lod = least_order->expected_date - least_order->lateness; + DateTicks odd = od->expected_date - od->lateness; + + if (type == D_ARRIVAL) { + lod -= least_order->order->GetWaitTime(); + odd -= od->order->GetWaitTime(); + } + + if (lod > odd && od->expected_date - od->lateness < max_date) { + least_order = od; + } + } + } + + /* Done. Phew! */ + return result; +} diff --git a/src/departures_func.h b/src/departures_func.h new file mode 100644 index 0000000000..a5fb42bb9f --- /dev/null +++ b/src/departures_func.h @@ -0,0 +1,21 @@ +/* $Id: departures_func.h $ */ + +/* + * This file is part of OpenTTD. + * 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. + * 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. + * 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 . + */ + +/** @file departures_func.h Functions related to departures. */ + +#ifndef DEPARTURES_FUNC_H +#define DEPARTURES_FUNC_H + +#include "station_base.h" +#include "core/smallvec_type.hpp" +#include "departures_type.h" + +DepartureList* MakeDepartureList(StationID station, bool show_vehicle_types[4], DepartureType type = D_DEPARTURE, bool show_vehicles_via = false); + +#endif /* DEPARTURES_FUNC_H */ diff --git a/src/departures_gui.cpp b/src/departures_gui.cpp new file mode 100644 index 0000000000..c88326d2f3 --- /dev/null +++ b/src/departures_gui.cpp @@ -0,0 +1,856 @@ +/* $Id$ */ + +/* + * This file is part of OpenTTD. + * 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. + * 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. + * 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 . + */ + +/** @file departures_gui.cpp Scheduled departures from a station. */ + +#include "stdafx.h" +#include "debug.h" +#include "gui.h" +#include "textbuf_gui.h" +#include "strings_func.h" +#include "window_func.h" +#include "vehicle_func.h" +#include "string_func.h" +#include "window_gui.h" +#include "timetable.h" +#include "vehiclelist.h" +#include "company_base.h" +#include "date_func.h" +#include "departures_gui.h" +#include "station_base.h" +#include "vehicle_gui_base.h" +#include "vehicle_base.h" +#include "vehicle_gui.h" +#include "order_base.h" +#include "settings_type.h" +#include "core/smallvec_type.hpp" +#include "date_type.h" +#include "company_type.h" +#include "departures_func.h" +#include "cargotype.h" + +#include "table/sprites.h" +#include "table/strings.h" + +static const NWidgetPart _nested_departures_list[] = { + NWidget(NWID_HORIZONTAL), + NWidget(WWT_CLOSEBOX, COLOUR_GREY), + NWidget(WWT_CAPTION, COLOUR_GREY, WID_DB_CAPTION), SetDataTip(STR_DEPARTURES_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS), + NWidget(WWT_SHADEBOX, COLOUR_GREY), + NWidget(WWT_STICKYBOX, COLOUR_GREY), + EndContainer(), + + NWidget(NWID_HORIZONTAL), + NWidget(WWT_MATRIX, COLOUR_GREY, WID_DB_LIST), SetMinimalSize(0, 0), SetFill(1, 0), SetResize(1, 1), + NWidget(NWID_VSCROLLBAR, COLOUR_GREY, WID_DB_SCROLLBAR), + EndContainer(), + + NWidget(NWID_HORIZONTAL), + NWidget(WWT_PANEL, COLOUR_GREY), SetMinimalSize(0, 12), SetResize(1, 0), SetFill(1, 1), EndContainer(), + NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_DB_SHOW_ARRS), SetMinimalSize(6, 12), SetFill(0, 1), SetDataTip(STR_DEPARTURES_ARRIVALS, STR_DEPARTURES_ARRIVALS_TOOLTIP), + NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_DB_SHOW_DEPS), SetMinimalSize(6, 12), SetFill(0, 1), SetDataTip(STR_DEPARTURES_DEPARTURES, STR_DEPARTURES_DEPARTURES_TOOLTIP), + NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_DB_SHOW_VIA), SetMinimalSize(11, 12), SetFill(0, 1), SetDataTip(STR_DEPARTURES_VIA_BUTTON, STR_DEPARTURES_VIA_TOOLTIP), + NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_DB_SHOW_TRAINS), SetMinimalSize(14, 12), SetFill(0, 1), SetDataTip(STR_TRAIN, STR_STATION_VIEW_SCHEDULED_TRAINS_TOOLTIP), + NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_DB_SHOW_ROADVEHS), SetMinimalSize(14, 12), SetFill(0, 1), SetDataTip(STR_LORRY, STR_STATION_VIEW_SCHEDULED_ROAD_VEHICLES_TOOLTIP), + NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_DB_SHOW_SHIPS), SetMinimalSize(14, 12), SetFill(0, 1), SetDataTip(STR_SHIP, STR_STATION_VIEW_SCHEDULED_SHIPS_TOOLTIP), + NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_DB_SHOW_PLANES), SetMinimalSize(14, 12), SetFill(0, 1), SetDataTip(STR_PLANE, STR_STATION_VIEW_SCHEDULED_AIRCRAFT_TOOLTIP), + NWidget(WWT_RESIZEBOX, COLOUR_GREY), + EndContainer(), +}; + +static WindowDesc _departures_desc( + WDP_AUTO, NULL, 260, 246, + WC_DEPARTURES_BOARD, WC_NONE, + 0, + _nested_departures_list, lengthof(_nested_departures_list) +); + +static uint cached_date_width = 0; ///< The cached maximum width required to display a date. +static uint cached_status_width = 0; ///< The cached maximum width required to show the status field. +static uint cached_date_arrow_width = 0; ///< The cached width of the red/green arrows that may be displayed alongside times. +static bool cached_date_display_method; ///< Whether the above cached values refers to original (d,m,y) dates or the 24h clock. +static bool cached_arr_dep_display_method; ///< Whether to show departures and arrivals on a single line. + +template +struct DeparturesWindow : public Window { +protected: + StationID station; ///< The station whose departures we're showing. + DepartureList *departures; ///< The current list of departures from this station. + DepartureList *arrivals; ///< The current list of arrivals from this station. + uint entry_height; ///< The height of an entry in the departures list. + uint tick_count; ///< The number of ticks that have elapsed since the window was created. Used for scrolling text. + int calc_tick_countdown; ///< The number of ticks to wait until recomputing the departure list. Signed in case it goes below zero. + bool show_types[4]; ///< The vehicle types to show in the departure list. + bool departure_types[3]; ///< The types of departure to show in the departure list. + uint min_width; ///< The minimum width of this window. + Scrollbar *vscroll; + + virtual uint GetMinWidth() const; + static void RecomputeDateWidth(); + virtual void DrawDeparturesListItems(const Rect &r) const; + void DeleteDeparturesList(DepartureList* list); +public: + + DeparturesWindow(WindowDesc *desc, WindowNumber window_number) : Window(desc), + station(window_number), + departures(new DepartureList()), + arrivals(new DepartureList()), + entry_height(1 + FONT_HEIGHT_NORMAL + 1 + (_settings_client.gui.departure_larger_font ? FONT_HEIGHT_NORMAL : FONT_HEIGHT_SMALL) + 1 + 1), + tick_count(0), + calc_tick_countdown(0), + min_width(400) + { + this->CreateNestedTree(); + this->vscroll = this->GetScrollbar(WID_DB_SCROLLBAR); + this->FinishInitNested(window_number); + + /* By default, only show departures. */ + departure_types[0] = true; + departure_types[1] = false; + departure_types[2] = false; + this->LowerWidget(WID_DB_SHOW_DEPS); + this->RaiseWidget(WID_DB_SHOW_ARRS); + this->RaiseWidget(WID_DB_SHOW_VIA); + + for (uint i = 0; i < 4; ++i) { + show_types[i] = true; + this->LowerWidget(WID_DB_SHOW_TRAINS + i); + } + + if (Twaypoint) { + this->GetWidget(WID_DB_CAPTION)->SetDataTip(STR_DEPARTURES_CAPTION_WAYPOINT, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS); + + for (uint i = 0; i < 4; ++i) { + this->DisableWidget(WID_DB_SHOW_TRAINS + i); + } + + this->DisableWidget(WID_DB_SHOW_ARRS); + this->DisableWidget(WID_DB_SHOW_DEPS); + this->DisableWidget(WID_DB_SHOW_VIA); + + departure_types[2] = true; + + this->LowerWidget(WID_DB_SHOW_VIA); + } + } + + virtual ~DeparturesWindow() + { + this->DeleteDeparturesList(departures); + this->DeleteDeparturesList(this->arrivals); + } + + virtual void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) + { + switch (widget) { + case WID_DB_LIST: + resize->height = DeparturesWindow::entry_height; + size->height = 2 * resize->height; + break; + } + } + + virtual void SetStringParameters(int widget) const + { + if (widget == WID_DB_CAPTION) { + const Station *st = Station::Get(this->station); + SetDParam(0, st->index); + } + } + + virtual void OnClick(Point pt, int widget, int click_count) + { + switch (widget) { + case WID_DB_SHOW_TRAINS: // Show trains to this station + case WID_DB_SHOW_ROADVEHS: // Show road vehicles to this station + case WID_DB_SHOW_SHIPS: // Show ships to this station + case WID_DB_SHOW_PLANES: // Show aircraft to this station + this->show_types[widget - WID_DB_SHOW_TRAINS] = !this->show_types[widget - WID_DB_SHOW_TRAINS]; + if (this->show_types[widget - WID_DB_SHOW_TRAINS]) { + this->LowerWidget(widget); + } else { + this->RaiseWidget(widget); + } + /* We need to recompute the departures list. */ + this->calc_tick_countdown = 0; + /* We need to redraw the button that was pressed. */ + this->SetWidgetDirty(widget); + break; + + case WID_DB_SHOW_DEPS: + case WID_DB_SHOW_ARRS: + if (_settings_client.gui.departure_show_both) break; + /* FALL THROUGH */ + + case WID_DB_SHOW_VIA: + + this->departure_types[widget - WID_DB_SHOW_DEPS] = !this->departure_types[widget - WID_DB_SHOW_DEPS]; + if (this->departure_types[widget - WID_DB_SHOW_DEPS]) { + this->LowerWidget(widget); + } else { + this->RaiseWidget(widget); + } + + if (!this->departure_types[0]) { + this->RaiseWidget(WID_DB_SHOW_VIA); + this->DisableWidget(WID_DB_SHOW_VIA); + } else { + this->EnableWidget(WID_DB_SHOW_VIA); + + if (this->departure_types[2]) { + this->LowerWidget(WID_DB_SHOW_VIA); + } + } + /* We need to recompute the departures list. */ + this->calc_tick_countdown = 0; + /* We need to redraw the button that was pressed. */ + this->SetWidgetDirty(widget); + break; + + case WID_DB_LIST: // Matrix to show departures + /* We need to find the departure corresponding to where the user clicked. */ + uint32 id_v = (pt.y - this->GetWidget(WID_DB_LIST)->pos_y) / this->entry_height; + + if (id_v >= this->vscroll->GetCapacity()) return; // click out of bounds + + id_v += this->vscroll->GetPosition(); + + if (id_v >= (this->departures->Length() + this->arrivals->Length())) return; // click out of list bound + + uint departure = 0; + uint arrival = 0; + + /* Draw each departure. */ + for (uint i = 0; i <= id_v; ++i) { + const Departure *d; + + if (arrival == this->arrivals->Length()) { + d = (*(this->departures))[departure++]; + } else if (departure == this->departures->Length()) { + d = (*(this->arrivals))[arrival++]; + } else { + d = (*(this->departures))[departure]; + const Departure *a = (*(this->arrivals))[arrival]; + + if (a->scheduled_date < d->scheduled_date) { + d = a; + arrival++; + } else { + departure++; + } + } + + if (i == id_v) { + ShowVehicleViewWindow(d->vehicle); + break; + } + } + + break; + } + } + + virtual void OnTick() + { + if (_pause_mode == PM_UNPAUSED) { + this->tick_count += 1; + this->calc_tick_countdown -= 1; + } + + /* Recompute the minimum date display width if the cached one is no longer valid. */ + if (cached_date_width == 0 || + _settings_client.gui.time_in_minutes != cached_date_display_method || + _settings_client.gui.departure_show_both != cached_arr_dep_display_method) { + this->RecomputeDateWidth(); + } + + /* We need to redraw the scrolling text in its new position. */ + this->SetWidgetDirty(WID_DB_LIST); + + /* Recompute the list of departures if we're due to. */ + if (this->calc_tick_countdown <= 0) { + this->calc_tick_countdown = _settings_client.gui.departure_calc_frequency; + this->DeleteDeparturesList(this->departures); + this->DeleteDeparturesList(this->arrivals); + this->departures = (this->departure_types[0] ? MakeDepartureList(this->station, this->show_types, D_DEPARTURE, Twaypoint || this->departure_types[2]) : new DepartureList()); + this->arrivals = (this->departure_types[1] && !_settings_client.gui.departure_show_both ? MakeDepartureList(this->station, this->show_types, D_ARRIVAL ) : new DepartureList()); + this->SetWidgetDirty(WID_DB_LIST); + } + + uint new_width = this->GetMinWidth(); + + if (new_width != this->min_width) { + NWidgetCore *n = this->GetWidget(WID_DB_LIST); + n->SetMinimalSize(new_width, 0); + this->ReInit(); + this->min_width = new_width; + } + + uint new_height = 1 + FONT_HEIGHT_NORMAL + 1 + (_settings_client.gui.departure_larger_font ? FONT_HEIGHT_NORMAL : FONT_HEIGHT_SMALL) + 1 + 1; + + if (new_height != this->entry_height) { + this->entry_height = new_height; + this->SetWidgetDirty(WID_DB_LIST); + this->ReInit(); + } + } + + virtual void OnPaint() + { + if (Twaypoint || _settings_client.gui.departure_show_both) { + this->DisableWidget(WID_DB_SHOW_ARRS); + this->DisableWidget(WID_DB_SHOW_DEPS); + } else { + this->EnableWidget(WID_DB_SHOW_ARRS); + this->EnableWidget(WID_DB_SHOW_DEPS); + } + + this->vscroll->SetCount(min(_settings_client.gui.max_departures, this->departures->Length() + this->arrivals->Length())); + this->DrawWidgets(); + } + + virtual void DrawWidget(const Rect &r, int widget) const + { + switch (widget) { + case WID_DB_LIST: + this->DrawDeparturesListItems(r); + break; + } + } + + virtual void OnResize() + { + this->vscroll->SetCapacityFromWidget(this, WID_DB_LIST); + this->GetWidget(WID_DB_LIST)->widget_data = (this->vscroll->GetCapacity() << MAT_ROW_START) + (1 << MAT_COL_START); + } +}; + +/** + * Shows a window of scheduled departures for a station. + * @param station the station to show a departures window for + */ +void ShowStationDepartures(StationID station) +{ + AllocateWindowDescFront >(&_departures_desc, station); +} + +/** + * Shows a window of scheduled departures for a station. + * @param station the station to show a departures window for + */ +void ShowWaypointDepartures(StationID waypoint) +{ + AllocateWindowDescFront >(&_departures_desc, waypoint); +} + +template +void DeparturesWindow::RecomputeDateWidth() +{ + cached_date_width = 0; + cached_status_width = 0; + cached_date_display_method = _settings_client.gui.time_in_minutes; + cached_arr_dep_display_method = _settings_client.gui.departure_show_both; + + cached_status_width = max((GetStringBoundingBox(STR_DEPARTURES_ON_TIME)).width, cached_status_width); + cached_status_width = max((GetStringBoundingBox(STR_DEPARTURES_DELAYED)).width, cached_status_width); + cached_status_width = max((GetStringBoundingBox(STR_DEPARTURES_CANCELLED)).width, cached_status_width); + + uint interval = cached_date_display_method ? _settings_client.gui.ticks_per_minute : DAY_TICKS; + uint count = cached_date_display_method ? 24*60 : 365; + + for (uint i = 0; i < count; ++i) { + SetDParam(0, INT_MAX - (i*interval)); + SetDParam(1, INT_MAX - (i*interval)); + cached_date_width = max(GetStringBoundingBox(cached_arr_dep_display_method ? STR_DEPARTURES_TIME_BOTH : STR_DEPARTURES_TIME_DEP).width, cached_date_width); + cached_status_width = max((GetStringBoundingBox(STR_DEPARTURES_EXPECTED)).width, cached_status_width); + } + + SetDParam(0, 0); + cached_date_arrow_width = GetStringBoundingBox(STR_DEPARTURES_TIME_DEP).width - GetStringBoundingBox(STR_DEPARTURES_TIME).width; + + if (!_settings_client.gui.departure_show_both) { + cached_date_width -= cached_date_arrow_width; + } +} + +template +uint DeparturesWindow::GetMinWidth() const +{ + uint result = 0; + + /* Time */ + result = cached_date_width; + + /* Vehicle type icon */ + result += _settings_client.gui.departure_show_vehicle_type ? (GetStringBoundingBox(STR_DEPARTURES_TYPE_PLANE)).width : 0; + + /* Status */ + result += cached_status_width; + + /* Find the maximum company name width. */ + int toc_width = 0; + + /* Find the maximum company name width. */ + int group_width = 0; + + /* Find the maximum vehicle name width. */ + int veh_width = 0; + + if (_settings_client.gui.departure_show_vehicle || _settings_client.gui.departure_show_company || _settings_client.gui.departure_show_group) { + for (uint i = 0; i < 4; ++i) { + VehicleList vehicles; + + /* MAX_COMPANIES is probably the wrong thing to put here, but it works. GenerateVehicleSortList doesn't check the company when the type of list is VL_STATION_LIST (r20801). */ + if (!GenerateVehicleSortList(&vehicles, VehicleListIdentifier(VL_STATION_LIST, (VehicleType)(VEH_TRAIN + i), MAX_COMPANIES, station).Pack())) { + /* Something went wrong: panic! */ + continue; + } + + for (const Vehicle **v = vehicles.Begin(); v != vehicles.End(); v++) { + SetDParam(0, (uint64)((*v)->index)); + int width = (GetStringBoundingBox(STR_DEPARTURES_VEH)).width; + if (_settings_client.gui.departure_show_vehicle && width > veh_width) veh_width = width; + + if ((*v)->group_id != INVALID_GROUP && (*v)->group_id != DEFAULT_GROUP) { + SetDParam(0, (uint64)((*v)->group_id)); + width = (GetStringBoundingBox(STR_DEPARTURES_GROUP)).width; + if (_settings_client.gui.departure_show_group && width > group_width) group_width = width; + } + + SetDParam(0, (uint64)((*v)->owner)); + width = (GetStringBoundingBox(STR_DEPARTURES_TOC)).width; + if (_settings_client.gui.departure_show_company && width > toc_width) toc_width = width; + } + } + } + + result += toc_width + veh_width + group_width; + + return result + 140; +} + +/** + * Deletes this window's departure list. + */ +template +void DeparturesWindow::DeleteDeparturesList(DepartureList *list) +{ + /* SmallVector uses free rather than delete on its contents (which doesn't invoke the destructor), so we need to delete each departure manually. */ + for (uint i = 0; i < list->Length(); ++i) { + Departure **d = list->Get(i); + delete *d; + /* Make sure a double free doesn't happen. */ + *d = NULL; + } + list->Reset(); + delete list; + list = NULL; +} + +/** + * Draws a list of departures. + */ +template +void DeparturesWindow::DrawDeparturesListItems(const Rect &r) const +{ + int left = r.left + WD_MATRIX_LEFT; + int right = r.right - WD_MATRIX_RIGHT; + + bool rtl = _current_text_dir == TD_RTL; + bool ltr = !rtl; + + int text_offset = WD_FRAMERECT_RIGHT; + int text_left = left + (rtl ? 0 : text_offset); + int text_right = right - (rtl ? text_offset : 0); + + int y = r.top + 1; + uint max_departures = min(this->vscroll->GetPosition() + this->vscroll->GetCapacity(), this->departures->Length() + this->arrivals->Length()); + + if (max_departures > _settings_client.gui.max_departures) { + max_departures = _settings_client.gui.max_departures; + } + + byte small_font_size = _settings_client.gui.departure_larger_font ? FONT_HEIGHT_NORMAL : FONT_HEIGHT_SMALL; + + /* Draw the black background. */ + GfxFillRect(r.left + 1, r.top, r.right - 1, r.bottom, PC_BLACK); + + /* Nothing selected? Then display the information text. */ + bool none_selected[2] = {true, true}; + for (uint i = 0; i < 4; ++i) + { + if (this->show_types[i]) { + none_selected[0] = false; + break; + } + } + + for (uint i = 0; i < 2; ++i) + { + if (this->departure_types[i]) { + none_selected[1] = false; + break; + } + } + + if (none_selected[0] || none_selected[1]) { + DrawString(text_left, text_right, y + 1, STR_DEPARTURES_NONE_SELECTED); + return; + } + + /* No scheduled departures? Then display the information text. */ + if (max_departures == 0) { + DrawString(text_left, text_right, y + 1, STR_DEPARTURES_EMPTY); + return; + } + + /* Find the maximum possible width of the departure time and "Expt