Line data Source code
1 : /* Copyright (C) 2022 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
4 : * 0 A.D. is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 2 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * 0 A.D. is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "precompiled.h"
19 :
20 : #include "CChart.h"
21 :
22 : #include "graphics/Canvas2D.h"
23 : #include "gui/SettingTypes/CGUIList.h"
24 : #include "gui/SettingTypes/CGUISeries.h"
25 : #include "gui/SettingTypes/CGUIString.h"
26 : #include "ps/CLogger.h"
27 : #include "ps/CStrInternStatic.h"
28 : #include "ps/Profile.h"
29 :
30 : #include <cmath>
31 :
32 0 : CChart::CChart(CGUI& pGUI)
33 : : IGUIObject(pGUI),
34 : IGUITextOwner(*static_cast<IGUIObject*>(this)),
35 : m_AxisColor(this, "axis_color"),
36 : m_AxisWidth(this, "axis_width"),
37 : m_BufferZone(this, "buffer_zone"),
38 : m_Font(this, "font"),
39 : m_FormatX(this, "format_x"),
40 : m_FormatY(this, "format_y"),
41 : m_SeriesColor(this, "series_color"),
42 0 : m_SeriesSetting(this, "series")
43 : {
44 0 : }
45 :
46 0 : CChart::~CChart()
47 : {
48 0 : }
49 :
50 0 : void CChart::UpdateCachedSize()
51 : {
52 0 : IGUIObject::UpdateCachedSize();
53 0 : IGUITextOwner::UpdateCachedSize();
54 0 : }
55 :
56 0 : void CChart::HandleMessage(SGUIMessage& Message)
57 : {
58 0 : IGUIObject::HandleMessage(Message);
59 : // IGUITextOwner::HandleMessage(Message); performed in UpdateSeries
60 :
61 : // TODO: implement zoom
62 0 : if(Message.type == GUIM_SETTINGS_UPDATED)
63 0 : UpdateSeries();
64 0 : }
65 :
66 0 : void CChart::DrawAxes(CCanvas2D& canvas) const
67 : {
68 0 : canvas.DrawRect(CRect(
69 0 : m_CachedActualSize.TopLeft(),
70 0 : m_CachedActualSize.BottomLeft() + CVector2D(m_AxisWidth, 0.0f)), m_AxisColor);
71 0 : canvas.DrawRect(CRect(
72 0 : m_CachedActualSize.BottomLeft() - CVector2D(0.0f, m_AxisWidth),
73 0 : m_CachedActualSize.BottomRight()), m_AxisColor);
74 0 : }
75 :
76 0 : void CChart::Draw(CCanvas2D& canvas)
77 : {
78 0 : PROFILE3("render chart");
79 :
80 0 : if (m_Series.empty())
81 0 : return;
82 :
83 0 : CRect rect = GetChartRect();
84 0 : const float width = rect.GetWidth();
85 0 : const float height = rect.GetHeight();
86 :
87 0 : CVector2D scale(width / (m_RightTop.X - m_LeftBottom.X), height / (m_RightTop.Y - m_LeftBottom.Y));
88 0 : std::vector<CVector2D> linePoints;
89 0 : for (const CChartData& data : m_Series)
90 : {
91 0 : if (data.m_Points.empty())
92 0 : continue;
93 :
94 0 : linePoints.clear();
95 0 : for (const CVector2D& point : data.m_Points)
96 : {
97 0 : if (fabs(point.X) != std::numeric_limits<float>::infinity() && fabs(point.Y) != std::numeric_limits<float>::infinity())
98 : {
99 0 : linePoints.emplace_back(
100 0 : rect.left + (point.X - m_LeftBottom.X) * scale.X,
101 0 : rect.bottom - (point.Y - m_LeftBottom.Y) * scale.Y);
102 : }
103 : else
104 : {
105 0 : canvas.DrawLine(linePoints, 2.0f, data.m_Color);
106 0 : linePoints.clear();
107 : }
108 : }
109 0 : if (!linePoints.empty())
110 0 : canvas.DrawLine(linePoints, 2.0f, data.m_Color);
111 : }
112 :
113 0 : if (m_AxisWidth > 0)
114 0 : DrawAxes(canvas);
115 :
116 0 : for (size_t i = 0; i < m_TextPositions.size(); ++i)
117 0 : DrawText(canvas, i, CGUIColor(1.f, 1.f, 1.f, 1.f), m_TextPositions[i]);
118 : }
119 :
120 0 : CRect CChart::GetChartRect() const
121 : {
122 : return CRect(
123 0 : m_CachedActualSize.TopLeft() + CVector2D(m_AxisWidth, m_AxisWidth),
124 0 : m_CachedActualSize.BottomRight() - CVector2D(m_AxisWidth, m_AxisWidth)
125 0 : );
126 : }
127 :
128 0 : void CChart::UpdateSeries()
129 : {
130 0 : m_Series.clear();
131 0 : m_Series.resize(m_SeriesSetting->m_Series.size());
132 :
133 0 : for (size_t i = 0; i < m_SeriesSetting->m_Series.size(); ++i)
134 : {
135 0 : CChartData& data = m_Series[i];
136 :
137 0 : if (i < m_SeriesColor->m_Items.size() && !data.m_Color.ParseString(m_pGUI, m_SeriesColor->m_Items[i].GetOriginalString().ToUTF8(), 0))
138 0 : LOGWARNING("GUI: Error parsing 'series_color' (\"%s\")", utf8_from_wstring(m_SeriesColor->m_Items[i].GetOriginalString()));
139 :
140 0 : data.m_Points = m_SeriesSetting->m_Series[i];
141 : }
142 0 : UpdateBounds();
143 :
144 0 : SetupText();
145 0 : }
146 :
147 0 : void CChart::SetupText()
148 : {
149 0 : m_GeneratedTexts.clear();
150 0 : m_TextPositions.clear();
151 :
152 0 : if (m_Series.empty())
153 0 : return;
154 :
155 : // Add Y-axis
156 0 : const float height = GetChartRect().GetHeight();
157 : // TODO: split values depend on the format;
158 0 : if (m_EqualY)
159 : {
160 : // We don't need to generate many items for equal values
161 0 : AddFormattedValue(m_FormatY, m_RightTop.Y, m_Font, m_BufferZone);
162 0 : m_TextPositions.emplace_back(GetChartRect().TopLeft());
163 : }
164 : else
165 0 : for (int i = 0; i < 3; ++i)
166 : {
167 0 : AddFormattedValue(m_FormatY, m_RightTop.Y - (m_RightTop.Y - m_LeftBottom.Y) / 3.f * i, m_Font, m_BufferZone);
168 0 : m_TextPositions.emplace_back(GetChartRect().TopLeft() + CVector2D(0.f, height / 3.f * i));
169 : }
170 :
171 : // Add X-axis
172 0 : const float width = GetChartRect().GetWidth();
173 0 : if (m_EqualX)
174 : {
175 0 : CSize2D text_size = AddFormattedValue(m_FormatX, m_RightTop.X, m_Font, m_BufferZone);
176 0 : m_TextPositions.emplace_back(GetChartRect().BottomRight() - text_size);
177 : }
178 : else
179 0 : for (int i = 0; i < 3; ++i)
180 : {
181 0 : CSize2D text_size = AddFormattedValue(m_FormatX, m_RightTop.X - (m_RightTop.X - m_LeftBottom.X) / 3 * i, m_Font, m_BufferZone);
182 0 : m_TextPositions.emplace_back(GetChartRect().BottomRight() - text_size - CVector2D(width / 3 * i, 0.f));
183 : }
184 : }
185 :
186 0 : CSize2D CChart::AddFormattedValue(const CStrW& format, const float value, const CStrW& font, const float buffer_zone)
187 : {
188 : // TODO: we need to catch cases with equal formatted values.
189 0 : CGUIString gui_str;
190 0 : if (format == L"DECIMAL2")
191 : {
192 : wchar_t buffer[64];
193 0 : swprintf(buffer, 64, L"%.2f", value);
194 0 : gui_str.SetValue(buffer);
195 : }
196 0 : else if (format == L"INTEGER")
197 : {
198 : wchar_t buffer[64];
199 0 : swprintf(buffer, 64, L"%d", std::lround(value));
200 0 : gui_str.SetValue(buffer);
201 : }
202 0 : else if (format == L"DURATION_SHORT")
203 : {
204 0 : const int seconds = value;
205 : wchar_t buffer[64];
206 0 : swprintf(buffer, 64, L"%d:%02d", seconds / 60, seconds % 60);
207 0 : gui_str.SetValue(buffer);
208 : }
209 0 : else if (format == L"PERCENTAGE")
210 : {
211 : wchar_t buffer[64];
212 0 : swprintf(buffer, 64, L"%d%%", std::lround(value));
213 0 : gui_str.SetValue(buffer);
214 : }
215 : else
216 : {
217 0 : LOGERROR("Unsupported chart format: " + format.EscapeToPrintableASCII());
218 0 : return CSize2D();
219 : }
220 :
221 0 : return AddText(gui_str, font, 0, buffer_zone).GetSize();
222 : }
223 :
224 0 : void CChart::UpdateBounds()
225 : {
226 0 : if (m_Series.empty() || m_Series[0].m_Points.empty())
227 : {
228 0 : m_LeftBottom = m_RightTop = CVector2D(0.f, 0.f);
229 0 : return;
230 : }
231 :
232 0 : m_LeftBottom = m_RightTop = m_Series[0].m_Points[0];
233 0 : for (const CChartData& data : m_Series)
234 0 : for (const CVector2D& point : data.m_Points)
235 : {
236 0 : if (fabs(point.X) != std::numeric_limits<float>::infinity() && point.X < m_LeftBottom.X)
237 0 : m_LeftBottom.X = point.X;
238 0 : if (fabs(point.Y) != std::numeric_limits<float>::infinity() && point.Y < m_LeftBottom.Y)
239 0 : m_LeftBottom.Y = point.Y;
240 :
241 0 : if (fabs(point.X) != std::numeric_limits<float>::infinity() && point.X > m_RightTop.X)
242 0 : m_RightTop.X = point.X;
243 0 : if (fabs(point.Y) != std::numeric_limits<float>::infinity() && point.Y > m_RightTop.Y)
244 0 : m_RightTop.Y = point.Y;
245 : }
246 :
247 0 : m_EqualY = m_RightTop.Y == m_LeftBottom.Y;
248 0 : if (m_EqualY)
249 0 : m_RightTop.Y += 1;
250 0 : m_EqualX = m_RightTop.X == m_LeftBottom.X;
251 0 : if (m_EqualX)
252 0 : m_RightTop.X += 1;
253 0 : }
|