|
|
/* $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 <http://www.gnu.org/licenses/>.
|
|
|
*/
|
|
|
|
|
|
/** @file graph_gui.cpp GUI that shows performance graphs. */
|
|
|
|
|
|
#include "stdafx.h"
|
|
|
#include "openttd.h"
|
|
|
#include "graph_gui.h"
|
|
|
#include "window_gui.h"
|
|
|
#include "company_base.h"
|
|
|
#include "company_gui.h"
|
|
|
#include "economy_func.h"
|
|
|
#include "cargotype.h"
|
|
|
#include "strings_func.h"
|
|
|
#include "window_func.h"
|
|
|
#include "date_func.h"
|
|
|
#include "gfx_func.h"
|
|
|
#include "sortlist_type.h"
|
|
|
#include "core/geometry_func.hpp"
|
|
|
#include <math.h>
|
|
|
|
|
|
#include "table/strings.h"
|
|
|
#include "table/sprites.h"
|
|
|
|
|
|
/* Bitmasks of company and cargo indices that shouldn't be drawn. */
|
|
|
static uint _legend_excluded_companies;
|
|
|
static uint _legend_excluded_cargo;
|
|
|
|
|
|
/* Apparently these don't play well with enums. */
|
|
|
static const OverflowSafeInt64 INVALID_DATAPOINT(INT64_MAX); // Value used for a datapoint that shouldn't be drawn.
|
|
|
static const uint INVALID_DATAPOINT_POS = UINT_MAX; // Used to determine if the previous point was drawn.
|
|
|
|
|
|
/****************/
|
|
|
/* GRAPH LEGEND */
|
|
|
/****************/
|
|
|
|
|
|
/** Widget numbers of the graph legend window. */
|
|
|
enum GraphLegendWidgetNumbers {
|
|
|
GLW_BACKGROUND,
|
|
|
|
|
|
GLW_FIRST_COMPANY,
|
|
|
GLW_LAST_COMPANY = GLW_FIRST_COMPANY + MAX_COMPANIES - 1,
|
|
|
};
|
|
|
|
|
|
struct GraphLegendWindow : Window {
|
|
|
GraphLegendWindow(const WindowDesc *desc, WindowNumber window_number) : Window()
|
|
|
{
|
|
|
this->InitNested(desc, window_number);
|
|
|
|
|
|
for (CompanyID c = COMPANY_FIRST; c < MAX_COMPANIES; c++) {
|
|
|
if (!HasBit(_legend_excluded_companies, c)) this->LowerWidget(c + GLW_FIRST_COMPANY);
|
|
|
|
|
|
this->OnInvalidateData(c);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
virtual void OnPaint()
|
|
|
{
|
|
|
this->DrawWidgets();
|
|
|
}
|
|
|
|
|
|
virtual void DrawWidget(const Rect &r, int widget) const
|
|
|
{
|
|
|
if (!IsInsideMM(widget, GLW_FIRST_COMPANY, MAX_COMPANIES + GLW_FIRST_COMPANY)) return;
|
|
|
|
|
|
CompanyID cid = (CompanyID)(widget - GLW_FIRST_COMPANY);
|
|
|
|
|
|
if (!Company::IsValidID(cid)) return;
|
|
@@ -195,129 +194,112 @@ protected:
|
|
|
byte month;
|
|
|
Year year;
|
|
|
|
|
|
/* These values are used if the graph is being plotted against values
|
|
|
* rather than the dates specified by month and year. */
|
|
|
uint16 x_values_start;
|
|
|
uint16 x_values_increment;
|
|
|
|
|
|
int graph_widget;
|
|
|
StringID format_str_y_axis;
|
|
|
byte colours[GRAPH_MAX_DATASETS];
|
|
|
OverflowSafeInt64 cost[GRAPH_MAX_DATASETS][GRAPH_NUM_MONTHS]; ///< Stored costs for the last #GRAPH_NUM_MONTHS months
|
|
|
|
|
|
/**
|
|
|
* Get the interval that contains the graph's data. Excluded data is ignored to show smaller values in
|
|
|
* better detail when disabling higher ones.
|
|
|
* @param num_hori_lines Number of horizontal lines to be drawn.
|
|
|
* @return Highest and lowest values of the graph (ignoring disabled data).
|
|
|
*/
|
|
|
ValuesInterval GetValuesInterval(int num_hori_lines) const
|
|
|
{
|
|
|
ValuesInterval current_interval;
|
|
|
current_interval.highest = INT64_MIN;
|
|
|
current_interval.lowest = INT64_MAX;
|
|
|
|
|
|
for (int i = 0; i < this->num_dataset; i++) {
|
|
|
if (HasBit(this->excluded_data, i)) continue;
|
|
|
for (int j = 0; j < this->num_on_x_axis; j++) {
|
|
|
OverflowSafeInt64 datapoint = this->cost[i][j];
|
|
|
|
|
|
if (datapoint != INVALID_DATAPOINT) {
|
|
|
current_interval.highest = max(current_interval.highest, datapoint);
|
|
|
current_interval.lowest = min(current_interval.lowest, datapoint);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* Prevent showing values too close to the graph limits. */
|
|
|
current_interval.highest = (11 * current_interval.highest) / 10;
|
|
|
current_interval.lowest = (11 * current_interval.lowest) / 10;
|
|
|
|
|
|
/* Always include zero in the shown range. */
|
|
|
OverflowSafeInt64 abs_lower = (current_interval.lowest > 0) ? (OverflowSafeInt64)0 : abs(current_interval.lowest);
|
|
|
OverflowSafeInt64 abs_higher = (current_interval.highest < 0) ? (OverflowSafeInt64)0 : current_interval.highest;
|
|
|
|
|
|
int num_pos_grids;
|
|
|
int grid_size;
|
|
|
|
|
|
if (abs_lower == 0) {
|
|
|
if (abs_higher == 0) {
|
|
|
/* If both values are zero, show an empty graph. */
|
|
|
num_pos_grids = num_hori_lines / 2;
|
|
|
grid_size = 1;
|
|
|
} else {
|
|
|
/* The positive part of the graph can use all grids. */
|
|
|
num_pos_grids = num_hori_lines;
|
|
|
grid_size = ceil((float)abs_higher / num_hori_lines);
|
|
|
}
|
|
|
if (abs_lower != 0 || abs_higher != 0) {
|
|
|
/* The number of grids to reserve for the positive part is: */
|
|
|
num_pos_grids = RoundDivSU(abs_higher * num_hori_lines, abs_higher + abs_lower);
|
|
|
|
|
|
/* If there are any positive or negative values, force that they have at least one grid. */
|
|
|
if (num_pos_grids == 0 && abs_higher != 0) num_pos_grids++;
|
|
|
if (num_pos_grids == num_hori_lines && abs_lower != 0) num_pos_grids--;
|
|
|
|
|
|
/* Get the required grid size for each side and use the maximum one. */
|
|
|
int grid_size_higher = (abs_higher > 0) ? (int)(abs_higher + num_pos_grids - 1) / num_pos_grids : 0;
|
|
|
int grid_size_lower = (abs_lower > 0) ? (int)(abs_lower + num_hori_lines - num_pos_grids - 1) / (num_hori_lines - num_pos_grids) : 0;
|
|
|
grid_size = max(grid_size_higher, grid_size_lower);
|
|
|
} else {
|
|
|
if (abs_higher == 0) {
|
|
|
/* The negative part of the graph can use all grids. */
|
|
|
num_pos_grids = 0;
|
|
|
grid_size = ceil((float)abs_lower / num_hori_lines);
|
|
|
} else {
|
|
|
/* Get the smallest grid size required and the number of grids for each part of the graph. */
|
|
|
int min_pos_grids = 1;
|
|
|
int min_grid_size = INT_MAX;
|
|
|
for (num_pos_grids = 1; num_pos_grids <= num_hori_lines - 1; num_pos_grids++) {
|
|
|
/* Size required for each part of the graph given this number of grids. */
|
|
|
int pos_grid_size = ceil((float)abs_higher / num_pos_grids);
|
|
|
int neg_grid_size = ceil((float)abs_lower / (num_hori_lines - num_pos_grids));
|
|
|
grid_size = max(pos_grid_size, neg_grid_size);
|
|
|
if (grid_size < min_grid_size) {
|
|
|
min_pos_grids = num_pos_grids;
|
|
|
min_grid_size = grid_size;
|
|
|
}
|
|
|
}
|
|
|
grid_size = min_grid_size;
|
|
|
num_pos_grids = min_pos_grids;
|
|
|
}
|
|
|
/* If both values are zero, show an empty graph. */
|
|
|
num_pos_grids = num_hori_lines / 2;
|
|
|
grid_size = 1;
|
|
|
}
|
|
|
|
|
|
current_interval.highest = num_pos_grids * grid_size;
|
|
|
current_interval.lowest = -(num_hori_lines - num_pos_grids) * grid_size;
|
|
|
return current_interval;
|
|
|
}
|
|
|
|
|
|
/** Get width for Y labels.
|
|
|
* @param current_interval Interval that contains all of the graph data.
|
|
|
* @param num_hori_lines Number of horizontal lines to be drawn.
|
|
|
*/
|
|
|
uint GetYLabelWidth(ValuesInterval current_interval, int num_hori_lines) const
|
|
|
{
|
|
|
/* draw text strings on the y axis */
|
|
|
int64 y_label = current_interval.highest;
|
|
|
int64 y_label_separation = (current_interval.highest - current_interval.lowest) / num_hori_lines;
|
|
|
|
|
|
uint max_width = 0;
|
|
|
|
|
|
for (int i = 0; i < (num_hori_lines + 1); i++) {
|
|
|
SetDParam(0, this->format_str_y_axis);
|
|
|
SetDParam(1, y_label);
|
|
|
Dimension d = GetStringBoundingBox(STR_GRAPH_Y_LABEL);
|
|
|
if (d.width > max_width) max_width = d.width;
|
|
|
|
|
|
y_label -= y_label_separation;
|
|
|
}
|
|
|
|
|
|
return max_width;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Actually draw the graph.
|
|
|
* @param r the rectangle of the data field of the graph
|
|
|
*/
|
|
|
void DrawGraph(Rect r) const
|
|
|
{
|
|
|
uint x, y; ///< Reused whenever x and y coordinates are needed.
|
|
|
ValuesInterval interval; ///< Interval that contains all of the graph data.
|
|
|
int x_axis_offset; ///< Distance from the top of the graph to the x axis.
|
|
|
|
|
|
/* the colours and cost array of GraphDrawer must accomodate
|
|
|
* both values for cargo and companies. So if any are higher, quit */
|
|
|
assert_compile(GRAPH_MAX_DATASETS >= (int)NUM_CARGO && GRAPH_MAX_DATASETS >= (int)MAX_COMPANIES);
|
|
|
assert(this->num_vert_lines > 0);
|
|
|
|
|
|
byte grid_colour = _colour_gradient[COLOUR_GREY][4];
|
|
|
|