11 string format_timestamp(uint64_t pos);
13 static string event_type_to_string(EventType type)
16 case EventType::CATCH:
18 case EventType::DEFENSE:
20 case EventType::DEFENSIVE_SOFT_MINUS:
21 return "defensive_soft_minus";
22 case EventType::DEFENSIVE_SOFT_PLUS:
23 return "defensive_soft_plus";
24 case EventType::FORMATION_DEFENSE:
25 return "formation_defense";
26 case EventType::FORMATION_OFFENSE:
27 return "formation_offense";
32 case EventType::SWAP_IN:
34 case EventType::INTERCEPTION:
35 return "interception";
36 case EventType::OFFENSIVE_SOFT_MINUS:
37 return "offensive_soft_minus";
38 case EventType::OFFENSIVE_SOFT_PLUS:
39 return "offensive_soft_plus";
40 case EventType::SWAP_OUT:
44 case EventType::PULL_LANDED:
46 case EventType::PULL_OOB:
48 case EventType::RESTART:
50 case EventType::SET_DEFENSE:
52 case EventType::SET_OFFENSE:
54 case EventType::STALLOUT:
56 case EventType::STOPPAGE:
58 case EventType::THEIR_GOAL:
60 case EventType::THEIR_PULL:
62 case EventType::THEIR_THROWAWAY:
63 return "their_throwaway";
64 case EventType::THROWAWAY:
66 case EventType::UNKNOWN:
68 case EventType::WAS_D:
74 static EventType string_to_event_type(const string &type)
76 if (type == "catch") {
77 return EventType::CATCH;
78 } else if (type == "defense") {
79 return EventType::DEFENSE;
80 } else if (type == "defensive_soft_minus") {
81 return EventType::DEFENSIVE_SOFT_MINUS;
82 } else if (type == "defensive_soft_plus") {
83 return EventType::DEFENSIVE_SOFT_PLUS;
84 } else if (type == "formation_defense") {
85 return EventType::FORMATION_DEFENSE;
86 } else if (type == "formation_offense") {
87 return EventType::FORMATION_OFFENSE;
88 } else if (type == "drop") {
89 return EventType::DROP;
90 } else if (type == "goal") {
91 return EventType::GOAL;
92 } else if (type == "in") {
93 return EventType::SWAP_IN;
94 } else if (type == "interception") {
95 return EventType::INTERCEPTION;
96 } else if (type == "offensive_soft_minus") {
97 return EventType::OFFENSIVE_SOFT_MINUS;
98 } else if (type == "offensive_soft_plus") {
99 return EventType::OFFENSIVE_SOFT_PLUS;
100 } else if (type == "out") {
101 return EventType::SWAP_OUT;
102 } else if (type == "pull") {
103 return EventType::PULL;
104 } else if (type == "pull_landed") {
105 return EventType::PULL_LANDED;
106 } else if (type == "pull_oob") {
107 return EventType::PULL_OOB;
108 } else if (type == "restart") {
109 return EventType::RESTART;
110 } else if (type == "set_defense") {
111 return EventType::SET_DEFENSE;
112 } else if (type == "set_offense") {
113 return EventType::SET_OFFENSE;
114 } else if (type == "stallout") {
115 return EventType::STALLOUT;
116 } else if (type == "stoppage") {
117 return EventType::STOPPAGE;
118 } else if (type == "their_goal") {
119 return EventType::THEIR_GOAL;
120 } else if (type == "their_pull") {
121 return EventType::THEIR_PULL;
122 } else if (type == "their_throwaway") {
123 return EventType::THEIR_THROWAWAY;
124 } else if (type == "throwaway") {
125 return EventType::THROWAWAY;
126 } else if (type == "unknown") {
127 return EventType::UNKNOWN;
128 } else if (type == "was_d") {
129 return EventType::WAS_D;
131 fprintf(stderr, "Unknown event type ā%sā\n", type.c_str());
136 EventsModel::EventsModel(sqlite3 *db, int match_id) : db(db), match_id(match_id)
141 QVariant EventsModel::headerData(int section, Qt::Orientation orientation, int role) const
143 if (role != Qt::DisplayRole) {
146 if (orientation == Qt::Horizontal) {
149 } else if (section == 1) {
159 QVariant EventsModel::data(const QModelIndex &index, int role) const
161 if (role != Qt::DisplayRole) {
164 const Event &e = events[index.row()];
165 if (index.column() == 0) {
166 return QString::fromUtf8(format_timestamp(e.t));
167 } else if (index.column() == 1) {
168 optional<int> player_id = e.player_id;
169 optional<int> formation_id = e.formation_id;
171 auto p_it = players.find(*player_id);
172 const Player &p = p_it->second;
173 return QString::fromUtf8(p.name + " (" + p.number + ")");
174 } else if (formation_id) {
175 auto f_it = formations.find(*formation_id);
176 const Formation &f = f_it->second;
177 return QString::fromUtf8(f.name);
178 } else if (e.type == EventType::FORMATION_OFFENSE || e.type == EventType::FORMATION_DEFENSE) {
179 return "(None/unknown)";
183 } else if (index.column() == 2) {
184 string type = event_type_to_string(e.type);
185 type[0] = toupper(type[0]);
186 for (char &ch : type) {
193 if (type == "Pull oob") {
195 } else if (type == "Formation defense") {
196 type = "Defensive formation";
197 } else if (type == "Formation offense") {
198 type = "Offensive formation";
199 } else if (type == "Set offense") {
201 } else if (type == "Set defense") {
203 } else if (type == "Catch") {
205 } else if (type == "Was d") {
209 return QString::fromUtf8(type);
214 void EventsModel::load_data()
219 // Read the players. (The ordering is used to build the order map.)
221 int ret = sqlite3_prepare_v2(db, "SELECT player, number, name FROM player ORDER BY gender, (number+0), number", -1, &stmt, 0);
222 if (ret != SQLITE_OK) {
223 fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
228 ret = sqlite3_step(stmt);
229 if (ret == SQLITE_ROW) {
231 p.player_id = sqlite3_column_int(stmt, 0);
232 p.number = (const char *)sqlite3_column_text(stmt, 1);
233 p.name = (const char *) sqlite3_column_text(stmt, 2);
234 players[p.player_id] = std::move(p);
235 player_ordering[p.player_id] = order++;
236 } else if (ret == SQLITE_DONE) {
239 fprintf(stderr, "SELECT step: %s\n", sqlite3_errmsg(db));
243 ret = sqlite3_finalize(stmt);
244 if (ret != SQLITE_OK) {
245 fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db));
249 // Read the formations.
250 ret = sqlite3_prepare_v2(db, "SELECT formation, name FROM formation", -1, &stmt, 0);
251 if (ret != SQLITE_OK) {
252 fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
256 ret = sqlite3_step(stmt);
257 if (ret == SQLITE_ROW) {
259 f.formation_id = sqlite3_column_int(stmt, 0);
260 f.name = (const char *) sqlite3_column_text(stmt, 1);
261 formations[f.formation_id] = std::move(f);
262 } else if (ret == SQLITE_DONE) {
265 fprintf(stderr, "SELECT step: %s\n", sqlite3_errmsg(db));
269 ret = sqlite3_finalize(stmt);
270 if (ret != SQLITE_OK) {
271 fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db));
276 ret = sqlite3_prepare_v2(db, "SELECT event, t, player, formation, type FROM event WHERE match=? ORDER BY t", -1, &stmt, 0);
277 if (ret != SQLITE_OK) {
278 fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
281 sqlite3_bind_int64(stmt, 1, match_id);
283 ret = sqlite3_step(stmt);
284 if (ret == SQLITE_ROW) {
286 e.event_id = sqlite3_column_int(stmt, 0);
287 e.t = sqlite3_column_int(stmt, 1);
288 if (sqlite3_column_type(stmt, 2) == SQLITE_INTEGER) { // Non-NULL.
289 e.player_id = sqlite3_column_int(stmt, 2);
291 if (sqlite3_column_type(stmt, 3) == SQLITE_INTEGER) { // Non-NULL.
292 e.formation_id = sqlite3_column_int(stmt, 3);
294 e.type = string_to_event_type((const char *)sqlite3_column_text(stmt, 4));
295 events.push_back(std::move(e));
296 } else if (ret == SQLITE_DONE) {
299 fprintf(stderr, "SELECT step: %s\n", sqlite3_errmsg(db));
303 ret = sqlite3_finalize(stmt);
304 if (ret != SQLITE_OK) {
305 fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db));
310 unsigned EventsModel::insert_event(uint64_t t, optional<int> player_id, optional<int> formation_id, const string &type)
312 auto it = lower_bound(events.begin(), events.end(), t,
313 [](const Event &e, uint64_t t) { return e.t < t; });
314 unsigned pos = distance(events.begin(), it);
315 beginInsertRows(QModelIndex(), pos, pos);
319 e.player_id = player_id;
320 e.formation_id = formation_id;
321 e.type = string_to_event_type(type);
322 events.insert(events.begin() + pos, e);
326 // Insert the new row into the database.
328 int ret = sqlite3_prepare_v2(db, "INSERT INTO event (match, t, player, formation, type) VALUES (?, ?, ?, ?, ?)", -1, &stmt, 0);
329 if (ret != SQLITE_OK) {
330 fprintf(stderr, "INSERT prepare: %s\n", sqlite3_errmsg(db));
334 sqlite3_bind_int64(stmt, 1, match_id);
335 sqlite3_bind_int64(stmt, 2, t);
337 sqlite3_bind_int64(stmt, 3, *player_id);
339 sqlite3_bind_null(stmt, 3);
342 sqlite3_bind_int64(stmt, 4, *formation_id);
344 sqlite3_bind_null(stmt, 4);
346 sqlite3_bind_text(stmt, 5, type.data(), type.size(), SQLITE_STATIC);
348 ret = sqlite3_step(stmt);
349 if (ret == SQLITE_ROW) {
350 fprintf(stderr, "INSERT step: %s\n", sqlite3_errmsg(db));
354 ret = sqlite3_finalize(stmt);
355 if (ret != SQLITE_OK) {
356 fprintf(stderr, "INSERT finalize: %s\n", sqlite3_errmsg(db));
360 events[pos].event_id = sqlite3_last_insert_rowid(db);
364 void EventsModel::delete_event(unsigned pos)
366 int event_id = events[pos].event_id;
368 beginRemoveRows(QModelIndex(), pos, pos);
369 events.erase(events.begin() + pos);
372 // Delete the row from the database.
374 int ret = sqlite3_prepare_v2(db, "DELETE FROM event WHERE event=?", -1, &stmt, 0);
375 if (ret != SQLITE_OK) {
376 fprintf(stderr, "DELETE prepare: %s\n", sqlite3_errmsg(db));
380 sqlite3_bind_int64(stmt, 1, event_id);
382 ret = sqlite3_step(stmt);
383 if (ret == SQLITE_ROW) {
384 fprintf(stderr, "DELETE step: %s\n", sqlite3_errmsg(db));
388 ret = sqlite3_finalize(stmt);
389 if (ret != SQLITE_OK) {
390 fprintf(stderr, "DELETE finalize: %s\n", sqlite3_errmsg(db));
395 void EventsModel::set_event_type(unsigned pos, const string &type)
397 events[pos].type = string_to_event_type(type);
398 emit dataChanged(createIndex(pos, 0), createIndex(pos, 2));
401 int ret = sqlite3_prepare_v2(db, "UPDATE event SET type=? WHERE event=?", -1, &stmt, 0);
402 if (ret != SQLITE_OK) {
403 fprintf(stderr, "INSERT prepare: %s\n", sqlite3_errmsg(db));
407 sqlite3_bind_text(stmt, 1, type.data(), type.size(), SQLITE_STATIC);
408 sqlite3_bind_int64(stmt, 2, events[pos].event_id);
410 ret = sqlite3_step(stmt);
411 if (ret == SQLITE_ROW) {
412 fprintf(stderr, "UPDATE step: %s\n", sqlite3_errmsg(db));
416 ret = sqlite3_finalize(stmt);
417 if (ret != SQLITE_OK) {
418 fprintf(stderr, "UPDATE finalize: %s\n", sqlite3_errmsg(db));
423 void EventsModel::set_event_formation(unsigned pos, int formation_id)
425 events[pos].formation_id = formation_id;
426 emit dataChanged(createIndex(pos, 0), createIndex(pos, 2));
429 int ret = sqlite3_prepare_v2(db, "UPDATE event SET formation=? WHERE event=?", -1, &stmt, 0);
430 if (ret != SQLITE_OK) {
431 fprintf(stderr, "INSERT prepare: %s\n", sqlite3_errmsg(db));
435 sqlite3_bind_int64(stmt, 1, formation_id);
436 sqlite3_bind_int64(stmt, 2, events[pos].event_id);
438 ret = sqlite3_step(stmt);
439 if (ret == SQLITE_ROW) {
440 fprintf(stderr, "UPDATE step: %s\n", sqlite3_errmsg(db));
444 ret = sqlite3_finalize(stmt);
445 if (ret != SQLITE_OK) {
446 fprintf(stderr, "UPDATE finalize: %s\n", sqlite3_errmsg(db));
451 unsigned EventsModel::get_last_event_pos(uint64_t t) const
453 // upper_bound() gives first where e.t > t,
454 // and the one before that is the one we want.
455 auto it = upper_bound(events.begin(), events.end(), t,
456 [](uint64_t t, const Event &e) { return t < e.t; });
457 if (it == events.begin()) {
460 return distance(events.begin(), it - 1);
464 EventsModel::Status EventsModel::get_status_at(uint64_t t)
469 s.attack_state = Status::NOT_STARTED;
470 s.offensive_formation = 0;
471 s.defensive_formation = 0;
473 s.pull_state = Status::SHOULD_PULL;
474 s.last_catching_player = -1;
475 uint64_t last_gained_possession = 0;
476 uint64_t last_stoppage = 0;
477 uint64_t time_spent_in_stoppage = 0;
478 unsigned num_touches = 0;
480 auto set_offense = [&s] { s.attack_state = Status::OFFENSE; s.last_catching_player = -1; };
481 auto set_defense = [&s] { s.attack_state = Status::DEFENSE; s.last_catching_player = -1; };
483 for (const Event &e : events) {
488 if (e.type == EventType::GOAL || e.type == EventType::THEIR_GOAL) {
489 s.pull_state = Status::SHOULD_PULL;
490 } else if (e.type == EventType::SWAP_IN || e.type == EventType::SWAP_OUT || e.type == EventType::STOPPAGE || e.type == EventType::RESTART || e.type == EventType::UNKNOWN || e.type == EventType::SET_DEFENSE || e.type == EventType::SET_OFFENSE) {
491 // No effect on pull status.
492 } else if (e.type == EventType::PULL) {
493 s.pull_state = Status::PULL_IN_AIR;
494 s.last_catching_player = -1; // Just to be sure.
496 s.pull_state = Status::NOT_PULLING; // Includes pull_landed and pull_oob.
499 if (e.type == EventType::SET_OFFENSE) {
501 } else if (e.type == EventType::SET_DEFENSE) {
505 if (e.type == EventType::GOAL) {
510 if (e.type == EventType::THEIR_GOAL) {
515 if (e.type == EventType::CATCH) {
516 if (num_touches == 0) { // Pick up.
517 last_gained_possession = e.t;
518 time_spent_in_stoppage = 0;
521 s.last_catching_player = *e.player_id;
523 if (e.type == EventType::INTERCEPTION) {
526 s.last_catching_player = *e.player_id;
527 last_gained_possession = e.t;
528 time_spent_in_stoppage = 0;
530 if (e.type == EventType::DEFENSE || e.type == EventType::THEIR_THROWAWAY) {
533 time_spent_in_stoppage = 0;
535 if (e.type == EventType::DROP || e.type == EventType::WAS_D || e.type == EventType::THROWAWAY || e.type == EventType::STALLOUT) {
539 if (e.type == EventType::STOPPAGE) {
543 if (e.type == EventType::RESTART) {
545 if (last_stoppage != 0) {
546 time_spent_in_stoppage += (e.t - last_stoppage);
550 if (e.type == EventType::FORMATION_OFFENSE) {
551 if (e.formation_id) {
552 s.offensive_formation = *e.formation_id;
554 s.offensive_formation = 0;
557 if (e.type == EventType::FORMATION_DEFENSE) {
558 if (e.formation_id) {
559 s.defensive_formation = *e.formation_id;
561 s.defensive_formation = 0;
565 if (s.stoppage && last_stoppage != 0) {
566 time_spent_in_stoppage += (t - last_stoppage);
569 s.num_passes = (num_touches == 0) ? 0 : num_touches - 1;
570 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;
571 s.stoppage_sec = (s.attack_state == Status::OFFENSE && last_gained_possession != 0 && num_touches != 0) ? time_spent_in_stoppage / 1000 : 0;
575 set<int> EventsModel::get_team_at(uint64_t t)
578 for (const Event &e : events) {
582 if (e.type == EventType::SWAP_IN) {
583 team.insert(*e.player_id);
585 if (e.type == EventType::SWAP_OUT) {
586 team.erase(*e.player_id);
592 void EventsModel::set_team_at(uint64_t t, const set<int> &new_team)
594 // Backdate to the last goal or stoppage, _or_ the last time someone
595 // going out is mentioned. (We don't really track injuries yet;
596 // do we want an explicit injury type? If we had one, it would probably
598 uint64_t backdate_point = 0;
599 for (const Event &e : events) {
603 if (e.type == EventType::GOAL || e.type == EventType::THEIR_GOAL || e.type == EventType::STOPPAGE || e.type == EventType::SET_OFFENSE || e.type == EventType::SET_DEFENSE) {
604 backdate_point = e.t + 1;
606 if (e.player_id.has_value() && !new_team.count(*e.player_id)) {
607 backdate_point = e.t + 1;
611 // Delete all in/outs already at the backdate point.
612 for (unsigned i = 0; i < events.size(); ) {
613 if (events[i].t > backdate_point) {
616 if (events[i].t == backdate_point && (events[i].type == EventType::SWAP_IN || events[i].type == EventType::SWAP_OUT)) {
623 // Finally make the subs we need.
624 set<int> old_team = get_team_at(backdate_point);
625 for (int player_id : old_team) {
626 if (!new_team.count(player_id)) {
627 insert_event(backdate_point, player_id, nullopt, "out");
630 for (int player_id : new_team) {
631 if (!old_team.count(player_id)) {
632 insert_event(backdate_point, player_id, nullopt, "in");
637 void EventsModel::set_formation_at(uint64_t t, bool offense, unsigned formation)
639 // If there's another goal/stoppage/turnover no more than 20 seconds ago,
640 // we assume that the formation started at that point (it just took
641 // the operator a bit of time to see it). If not, we assume we
642 // changed in the middle of a point.
643 uint64_t backdate_point = 0;
644 for (const Event &e : events) {
648 if (e.type == EventType::GOAL || e.type == EventType::THEIR_GOAL ||
649 e.type == EventType::SWAP_IN || e.type == EventType::SWAP_OUT ||
650 e.type == EventType::STOPPAGE ||
651 e.type == EventType::SET_DEFENSE || e.type == EventType::SET_OFFENSE ||
652 e.type == EventType::THROWAWAY || e.type == EventType::THEIR_THROWAWAY ||
653 e.type == EventType::DROP || e.type == EventType::WAS_D || e.type == EventType::DEFENSE || e.type == EventType::INTERCEPTION || e.type == EventType::STALLOUT ||
654 e.type == EventType::PULL || e.type == EventType::PULL_LANDED || e.type == EventType::PULL_OOB || e.type == EventType::THEIR_PULL ||
655 e.type == EventType::FORMATION_OFFENSE || e.type == EventType::FORMATION_DEFENSE) {
656 backdate_point = e.t + 1;
658 if (e.type == EventType::FORMATION_OFFENSE || e.type == EventType::FORMATION_DEFENSE) {
662 if (backdate_point != 0 && t - backdate_point < 20000) {
666 insert_event(t, nullopt, formation == 0 ? nullopt : optional{formation}, "formation_offense");
668 insert_event(t, nullopt, formation == 0 ? nullopt : optional{formation}, "formation_defense");
672 vector<int> EventsModel::sort_team(const set<int> &team) const
674 vector<int> ret(team.begin(), team.end());
675 std::sort(ret.begin(), ret.end(), [this](int a, int b) {
676 return player_ordering.find(a)->second < player_ordering.find(b)->second;