]> git.sesse.net Git - pkanalytics/blob - events.cpp
Make soft +/- span both columns.
[pkanalytics] / events.cpp
1 #include <algorithm>
2 #include <string>
3 #include <map>
4 #include <vector>
5 #include <optional>
6 #include <sqlite3.h>
7 #include "events.h"
8
9 using namespace std;
10
11 string format_timestamp(uint64_t pos);
12
13 EventsModel::EventsModel(sqlite3 *db, int match_id) : db(db), match_id(match_id)
14 {
15         load_data();
16 }
17
18 QVariant EventsModel::headerData(int section, Qt::Orientation orientation, int role) const
19 {
20         if (role != Qt::DisplayRole) {
21                 return QVariant();
22         }
23         if (orientation == Qt::Horizontal) {
24                 if (section == 0) {
25                         return "Time";
26                 } else if (section == 1) {
27                         return "Player";
28                 } else {
29                         return "Type";
30                 }
31         } else {
32                 return "";
33         }
34 }
35
36 QVariant EventsModel::data(const QModelIndex &index, int role) const
37 {
38         if (role != Qt::DisplayRole) {
39                 return QVariant();
40         }
41         if (index.column() == 0) {
42                 return QString::fromUtf8(format_timestamp(events[index.row()].t));
43         } else if (index.column() == 1) {
44                 optional<int> player_id = events[index.row()].player_id;
45                 if (player_id) {
46                         auto p_it = players.find(*player_id);
47                         const Player &p = p_it->second;
48                         return QString::fromUtf8(p.name + " (" + p.number + ")");
49                 } else {
50                         return QVariant();
51                 }
52         } else if (index.column() == 2) {
53                 return QString::fromUtf8(events[index.row()].type);
54         }
55         return QVariant();
56 }
57
58 void EventsModel::load_data()
59 {
60         players.clear();
61         events.clear();
62
63         // Read the players. (The ordering is used to build the order map.)
64         sqlite3_stmt *stmt;
65         int ret = sqlite3_prepare_v2(db, "SELECT player, number, name FROM player ORDER BY gender, (number+0), number", -1, &stmt, 0);
66         if (ret != SQLITE_OK) {
67                 fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
68                 abort();
69         }
70         int order = 0;
71         for ( ;; ) {
72                 ret = sqlite3_step(stmt);
73                 if (ret == SQLITE_ROW) {
74                         Player p;
75                         p.player_id = sqlite3_column_int(stmt, 0);
76                         p.number = (const char *)sqlite3_column_text(stmt, 1);
77                         p.name = (const char *) sqlite3_column_text(stmt, 2);
78                         players[p.player_id] = std::move(p);
79                         player_ordering[p.player_id] = order++;
80                 } else if (ret == SQLITE_DONE) {
81                         break;
82                 } else {
83                         fprintf(stderr, "SELECT step: %s\n", sqlite3_errmsg(db));
84                         abort();
85                 }
86         }
87         ret = sqlite3_finalize(stmt);
88         if (ret != SQLITE_OK) {
89                 fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db));
90                 abort();
91         }
92
93         // Read the events.
94         ret = sqlite3_prepare_v2(db, "SELECT event, t, player, type FROM event WHERE match=? ORDER BY t", -1, &stmt, 0);
95         if (ret != SQLITE_OK) {
96                 fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
97                 abort();
98         }
99         sqlite3_bind_int64(stmt, 1, match_id);
100         for ( ;; ) {
101                 ret = sqlite3_step(stmt);
102                 if (ret == SQLITE_ROW) {
103                         Event e;
104                         e.event_id = sqlite3_column_int(stmt, 0);
105                         e.t = sqlite3_column_int(stmt, 1);
106                         if (sqlite3_column_type(stmt, 2) == SQLITE_INTEGER) {  // Non-NULL.
107                                 e.player_id = sqlite3_column_int(stmt, 2);
108                         }
109                         e.type = (const char *)sqlite3_column_text(stmt, 3);
110                         events.push_back(std::move(e));
111                 } else if (ret == SQLITE_DONE) {
112                         break;
113                 } else {
114                         fprintf(stderr, "SELECT step: %s\n", sqlite3_errmsg(db));
115                         abort();
116                 }
117         }
118         ret = sqlite3_finalize(stmt);
119         if (ret != SQLITE_OK) {
120                 fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db));
121                 abort();
122         }
123 }
124
125 unsigned EventsModel::insert_event(uint64_t t, optional<int> player_id, const string &type)
126 {
127         auto it = lower_bound(events.begin(), events.end(), t,
128                 [](const Event &e, uint64_t t) { return e.t < t; });
129         unsigned pos = distance(events.begin(), it);
130         beginInsertRows(QModelIndex(), pos, pos);
131
132         Event e;
133         e.t = t;
134         e.player_id = player_id;
135         e.type = type;
136         events.insert(events.begin() + pos, e);
137
138         endInsertRows();
139
140         // Insert the new row into the database.
141         sqlite3_stmt *stmt;
142         int ret = sqlite3_prepare_v2(db, "INSERT INTO event (match, t, player, type) VALUES (?, ?, ?, ?)", -1, &stmt, 0);
143         if (ret != SQLITE_OK) {
144                 fprintf(stderr, "INSERT prepare: %s\n", sqlite3_errmsg(db));
145                 abort();
146         }
147
148         sqlite3_bind_int64(stmt, 1, match_id);
149         sqlite3_bind_int64(stmt, 2, t);
150         if (player_id) {
151                 sqlite3_bind_int64(stmt, 3, *player_id);
152         } else {
153                 sqlite3_bind_null(stmt, 3);
154         }
155         sqlite3_bind_text(stmt, 4, type.data(), type.size(), SQLITE_STATIC);
156
157         ret = sqlite3_step(stmt);
158         if (ret == SQLITE_ROW) {
159                 fprintf(stderr, "INSERT step: %s\n", sqlite3_errmsg(db));
160                 abort();
161         }
162
163         ret = sqlite3_finalize(stmt);
164         if (ret != SQLITE_OK) {
165                 fprintf(stderr, "INSERT finalize: %s\n", sqlite3_errmsg(db));
166                 abort();
167         }
168
169         events[pos].event_id = sqlite3_last_insert_rowid(db);
170         return pos;
171 }
172
173 void EventsModel::delete_event(unsigned pos)
174 {
175         int event_id = events[pos].event_id;
176
177         beginRemoveRows(QModelIndex(), pos, pos);
178         events.erase(events.begin() + pos);
179         endRemoveRows();
180
181         // Delete the row from the database.
182         sqlite3_stmt *stmt;
183         int ret = sqlite3_prepare_v2(db, "DELETE FROM event WHERE event=?", -1, &stmt, 0);
184         if (ret != SQLITE_OK) {
185                 fprintf(stderr, "DELETE prepare: %s\n", sqlite3_errmsg(db));
186                 abort();
187         }
188
189         sqlite3_bind_int64(stmt, 1, event_id);
190
191         ret = sqlite3_step(stmt);
192         if (ret == SQLITE_ROW) {
193                 fprintf(stderr, "DELETE step: %s\n", sqlite3_errmsg(db));
194                 abort();
195         }
196
197         ret = sqlite3_finalize(stmt);
198         if (ret != SQLITE_OK) {
199                 fprintf(stderr, "DELETE finalize: %s\n", sqlite3_errmsg(db));
200                 abort();
201         }
202 }
203
204 void EventsModel::set_event_type(unsigned pos, const string &type)
205 {
206         events[pos].type = type;
207         emit dataChanged(createIndex(pos, 0), createIndex(pos, 2));
208
209         sqlite3_stmt *stmt;
210         int ret = sqlite3_prepare_v2(db, "UPDATE event SET type=? WHERE event=?", -1, &stmt, 0);
211         if (ret != SQLITE_OK) {
212                 fprintf(stderr, "INSERT prepare: %s\n", sqlite3_errmsg(db));
213                 abort();
214         }
215
216         sqlite3_bind_text(stmt, 1, type.data(), type.size(), SQLITE_STATIC);
217         sqlite3_bind_int64(stmt, 2, events[pos].event_id);
218
219         ret = sqlite3_step(stmt);
220         if (ret == SQLITE_ROW) {
221                 fprintf(stderr, "UPDATE step: %s\n", sqlite3_errmsg(db));
222                 abort();
223         }
224
225         ret = sqlite3_finalize(stmt);
226         if (ret != SQLITE_OK) {
227                 fprintf(stderr, "UPDATE finalize: %s\n", sqlite3_errmsg(db));
228                 abort();
229         }
230 }
231
232 unsigned EventsModel::get_last_event_pos(uint64_t t) const
233 {
234         // upper_bound() gives first where e.t > t,
235         // and the one before that is the one we want.
236         auto it = upper_bound(events.begin(), events.end(), t,
237                 [](uint64_t t, const Event &e) { return t < e.t; });
238         if (it == events.begin()) {
239                 return 0;
240         } else {
241                 return distance(events.begin(), it - 1);
242         }
243 }
244
245 EventsModel::Status EventsModel::get_status_at(uint64_t t)
246 {
247         Status s;
248         s.our_score = 0;
249         s.their_score = 0;
250         s.attack_state = Status::NOT_STARTED;
251         s.stoppage = false;
252         s.pull_state = Status::SHOULD_PULL;
253         uint64_t last_gained_possession = 0;
254         uint64_t last_stoppage = 0;
255         uint64_t time_spent_in_stoppage = 0;
256         unsigned num_touches = 0;
257
258         auto set_offense = [&s] { s.attack_state = Status::OFFENSE; };
259         auto set_defense = [&s] { s.attack_state = Status::DEFENSE; };
260
261         for (const Event &e : events) {
262                 if (e.t > t) {
263                         break;
264                 }
265
266                 if (e.type == "goal" || e.type == "their_goal") {
267                         s.pull_state = Status::SHOULD_PULL;
268                 } else if (e.type == "in" || e.type == "out" || e.type == "stoppage" || e.type == "restart" || e.type == "unknown" || e.type == "set_defense" || e.type == "set_offense") {
269                         // No effect on pull status.
270                 } else if (e.type == "pull") {
271                         s.pull_state = Status::PULL_IN_AIR;
272                 } else {
273                         s.pull_state = Status::NOT_PULLING;  // Includes pull_landed and pull_oob.
274                 }
275
276                 if (e.type == "set_offense") {
277                         set_offense();
278                 } else if (e.type == "set_defense") {
279                         set_defense();
280                 }
281
282                 if (e.type == "goal") {
283                         ++s.our_score;
284                         set_defense();
285                         num_touches = 0;
286                 }
287                 if (e.type == "their_goal") {
288                         ++s.their_score;
289                         set_offense();
290                         num_touches = 0;
291                 }
292                 if (e.type == "catch") {
293                         if (num_touches == 0) {  // Pick up.
294                                 last_gained_possession = e.t;
295                                 time_spent_in_stoppage = 0;
296                         }
297                         ++num_touches;
298                 }
299                 if (e.type == "interception") {
300                         num_touches = 1;
301                         set_offense();
302                         last_gained_possession = e.t;
303                         time_spent_in_stoppage = 0;
304                 }
305                 if (e.type == "defense" || e.type == "their_throwaway") {
306                         set_offense();
307                         num_touches = 0;
308                         time_spent_in_stoppage = 0;
309                 }
310                 if (e.type == "drop" || e.type == "throwaway") {
311                         set_defense();
312                         num_touches = 0;
313                 }
314                 if (e.type == "stoppage") {
315                         s.stoppage = true;
316                         last_stoppage = e.t;
317                 }
318                 if (e.type == "restart") {
319                         s.stoppage = false;
320                         if (last_stoppage != 0) {
321                                 time_spent_in_stoppage += (e.t - last_stoppage);
322                                 last_stoppage = 0;
323                         }
324                 }
325         }
326         if (s.stoppage && last_stoppage != 0) {
327                 time_spent_in_stoppage += (t - last_stoppage);
328         }
329
330         s.num_passes = (num_touches == 0) ? 0 : num_touches - 1;
331         s.possession_sec = (s.attack_state == Status::OFFENSE && last_gained_possession != 0 && num_touches != 0) ? (t - last_gained_possession - time_spent_in_stoppage) / 1000 : 0;
332         s.stoppage_sec = (s.attack_state == Status::OFFENSE && last_gained_possession != 0 && num_touches != 0) ? time_spent_in_stoppage / 1000 : 0;
333         return s;
334 }
335
336 set<int> EventsModel::get_team_at(uint64_t t)
337 {
338         set<int> team;
339         for (const Event &e : events) {
340                 if (e.t > t) {
341                         break;
342                 }
343                 if (e.type == "in") {
344                         team.insert(*e.player_id);
345                 }
346                 if (e.type == "out") {
347                         team.erase(*e.player_id);
348                 }
349         }
350         return team;
351 }
352
353 void EventsModel::set_team_at(uint64_t t, const set<int> &new_team)
354 {
355         // Backdate to the last goal or stoppage, _or_ the last time someone
356         // going out is mentioned. (We don't really track injuries yet;
357         // do we want an explicit injury type? If we had one, it would probably
358         // be the simplest.)
359         uint64_t backdate_point = 0;
360         for (const Event &e : events) {
361                 if (e.t > t) {
362                         break;
363                 }
364                 if (e.type == "goal" || e.type == "their_goal" || e.type == "stoppage" || e.type == "reset" || e.type == "set_offense" || e.type == "set_defense") {
365                         backdate_point = e.t + 1;
366                 }
367                 if (e.player_id.has_value() && !new_team.count(*e.player_id)) {
368                         backdate_point = e.t + 1;
369                 }
370         }
371
372         // Delete all in/outs already at the backdate point.
373         for (unsigned i = 0; i < events.size(); ) {
374                 if (events[i].t > backdate_point) {
375                         break;
376                 }
377                 if (events[i].t == backdate_point && (events[i].type == "in" || events[i].type == "out")) {
378                         delete_event(i);
379                 } else {
380                         ++i;
381                 }
382         }
383
384         // Finally make the subs we need.
385         set<int> old_team = get_team_at(backdate_point);
386         for (int player_id : old_team) {
387                 if (!new_team.count(player_id)) {
388                         insert_event(backdate_point, player_id, "out");
389                 }
390         }
391         for (int player_id : new_team) {
392                 if (!old_team.count(player_id)) {
393                         insert_event(backdate_point, player_id, "in");
394                 }
395         }
396 }
397
398 vector<int> EventsModel::sort_team(const set<int> &team) const
399 {
400         vector<int> ret(team.begin(), team.end());
401         std::sort(ret.begin(), ret.end(), [this](int a, int b) {
402                 return player_ordering.find(a)->second < player_ordering.find(b)->second;
403         });
404         return ret;
405 }