]> git.sesse.net Git - pkanalytics/commitdiff
Count selected number of subs in the UI.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Thu, 27 Jul 2023 10:03:17 +0000 (12:03 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Thu, 27 Jul 2023 10:03:17 +0000 (12:03 +0200)
Includes support for gender counting. This was a long time coming,
and saves a fair amount of pain for the operator, at the cost of some
precious screen estate.

mainwindow.cpp
mainwindow.h
mainwindow.ui
players.cpp
players.h

index 254e62ddf6ccd66b844ccc186f024d953fd33453..eb82c0256fc3711d4cad86a4855164f7690f5105 100644 (file)
@@ -330,6 +330,29 @@ MainWindow::MainWindow(EventsModel *events, PlayersModel *players,
        connect(key_delete, &QShortcut::activated, [this] { ui->delete_->animateClick(); });
        connect(ui->delete_, &QPushButton::clicked, [this] { delete_current_event(); });
 
+       // Player list shortcuts.
+       connect(ui->get_current_players, &QPushButton::clicked, [this, players, events] {
+               uint64_t t = ui->video->get_position();
+               QItemSelection selection;
+               set<int> team = events->get_team_at(t);
+               for (int row = 0; row < players->rowCount(QModelIndex()); ++row) {
+                       if (team.count(players->get_player_id(row))) {
+                               selection.select(players->get_row_start_qt(row), players->get_row_end_qt(row));
+                       }
+               }
+               ui->player_view->selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect);
+       });
+       connect(ui->clear_player_list, &QPushButton::clicked, [this] {
+               ui->player_view->selectionModel()->clear();
+       });
+       connect(ui->player_view->selectionModel(), &QItemSelectionModel::selectionChanged, [this] {
+               update_gender_ratio(ui->video->get_position());
+       });
+       update_gender_ratio(0);
+
+       // The shortcuts take up so much space that we really need, so we sacrifice the header.
+       ui->player_view->horizontalHeader()->hide();
+
        // Menus.
        connect(ui->action_exit, &QAction::triggered, [this] { close(); });
        connect(ui->action_export_json, &QAction::triggered, [db] { export_to_json(db, "ultimate.json"); });
@@ -341,6 +364,7 @@ MainWindow::MainWindow(EventsModel *events, PlayersModel *players,
        });
        connect(ui->action_gender_pull_rule, &QAction::toggled, [this, db, match_id] {
                save_match_property(db, match_id, "gender_pull_rule", ui->action_gender_pull_rule->isChecked());
+               update_gender_ratio(ui->video->get_position());
        });
 }
 
@@ -464,6 +488,7 @@ void MainWindow::update_ui_from_time(uint64_t t)
        update_status(t);
        update_player_buttons(t);
        update_action_buttons(t);
+       update_gender_ratio(t);
 }
 
 void MainWindow::update_status(uint64_t t)
@@ -659,6 +684,167 @@ void MainWindow::update_action_buttons(uint64_t t)
        ui->their_pull->setEnabled(false);
 }
 
+vector<pair<string, int>> sort_gender(const map<string, int> &gender_count)
+{
+       vector<pair<string, int>> sorted_gender;
+       for (const auto &[gender, count] : gender_count) {
+               sorted_gender.emplace_back(gender, count);
+       }
+       sort(sorted_gender.begin(), sorted_gender.end(), [](const pair<string, int> &a, const pair<string, int> &b) {
+               if (a.second != b.second) {
+                       return b.second < a.second;
+               }
+               return a.first < b.first;
+       });
+       return sorted_gender;
+}
+
+string format_gender_counts(const map<string, int> &gender_count)
+{
+       vector<pair<string, int>> sorted_gender = sort_gender(gender_count);
+
+       string str;
+       for (const auto &[gender, count] : sorted_gender) {
+               if (!str.empty()) {
+                       str += ", ";
+               }
+               char buf[256];
+               snprintf(buf, sizeof(buf), "%d ", count);
+               str += buf;
+               str += gender;
+       }
+       return str;
+}
+
+string format_gender_counts(const map<string, int> &gender_count, const map<string, int> &reference_gender_count)
+{
+       vector<pair<string, int>> sorted_gender = sort_gender(reference_gender_count);  // Less swapping around this way.
+
+       string str;
+       for (const auto &[gender, count] : sorted_gender) {
+               if (!str.empty()) {
+                       str += ", ";
+               }
+               char buf[256];
+               snprintf(buf, sizeof(buf), "%d/%d ", gender_count.find(gender)->second, count);
+               str += buf;
+               str += gender;
+       }
+       return str;
+}
+
+void MainWindow::update_gender_ratio(uint64_t t)
+{
+       string str;
+       bool ok = true;
+
+       // Count the gender ratio in the current selection.
+       map<string, int> gender_count;
+       for (int i = 0; i < players->rowCount(QModelIndex()); ++i) {
+               string gender = players->get_player_gender(i);
+               gender_count[gender] = 0;
+       }
+       int num_players = 0;
+       QItemSelectionModel *select = ui->player_view->selectionModel();
+       for (QModelIndex row : select->selectedRows()) {
+               string gender = players->get_player_gender(row.row());
+               ++gender_count[gender];
+               ++num_players;
+       }
+
+       const bool gender_rule_a = ui->action_gender_rule_a->isChecked();
+       if (gender_rule_a) {
+               // This is tricky. We don't want to hard-code assumptions about gender,
+               // since there are so many possible variations (e.g. 5 players for indoors,
+               // or loose mixed). We can't get everything right, but our general
+               // strategy will be:
+               //
+               //  - We assume ABBA pattern is followed throughout, ie., we switch
+               //    on odd-numbered points. We use goals as reference for points.
+               //  - We always use the two or three latest points as reference;
+               //    this means an issue with the first point won't persist forever.
+               //  - When we don't switch, we expect the identical number as last time.
+               //  - When we _do_ switch, we copy the variation from two points ago
+               //    if we have it; if not, we simply expect it to be different.
+               //  - We should always have the same number of people as last point.
+               //
+               // The viewer's warnings also checks that things are correct across
+               // stoppages, which we don't yet.
+               map<string, int> current_gender_count;
+               for (int i = 0; i < players->rowCount(QModelIndex()); ++i) {
+                       string gender = players->get_player_gender(i);
+                       current_gender_count[gender] = 0;
+               }
+               vector<map<string, int>> historical_gender_counts;
+               for (int row = 0; row < events->rowCount(QModelIndex()); ++row) {
+                       if (events->get_time(row) > t) {
+                               break;
+                       }
+                       EventType type = events->get_event_type(row);
+                       if (type == EventType::GOAL || type == EventType::THEIR_GOAL) {
+                               historical_gender_counts.push_back(current_gender_count);
+                       } else if (type == EventType::IN) {
+                               string gender = players->get_player_gender_by_id(*events->get_player_id(row));
+                               ++current_gender_count[gender];
+                       } else if (type == EventType::OUT) {
+                               string gender = players->get_player_gender_by_id(*events->get_player_id(row));
+                               --current_gender_count[gender];
+                       }
+               }
+
+               ok = true;
+               if (historical_gender_counts.empty()) {
+                       // We don't have any points yet. Just output the ratio.
+                       str = format_gender_counts(gender_count);
+               } else if (historical_gender_counts.size() == 1) {
+                       // We have one, so this one must be different, but we don't know what it must be.
+                       // It must have the same number of players, though.
+                       str = format_gender_counts(gender_count);
+                       ok = (gender_count != historical_gender_counts.back());
+                       int old_sum = 0, new_sum = 0;
+                       for (const auto &[gender, count] : historical_gender_counts.back()) {
+                               old_sum += count;
+                       }
+                       for (const auto &[gender, count] : gender_count) {
+                               new_sum += count;
+                       }
+                       if (old_sum != new_sum) {
+                               ok = false;
+                       }
+               } else if (historical_gender_counts.size() % 2 == 0) {
+                       // Must be same as previous.
+                       str = format_gender_counts(gender_count, historical_gender_counts.back());
+                       ok = (gender_count == historical_gender_counts.back());
+               } else {
+                       // Must be same as two points ago.
+                       const auto &ref = historical_gender_counts[historical_gender_counts.size() - 3];
+                       str = format_gender_counts(gender_count, ref);
+                       ok = (gender_count == ref);
+               }
+       } else if (gender_count.size() == 1) {
+               // Everybody is either of the same gender or nobody has gender noted,
+               // so just count the number of players. We don't make red here.
+               char buf[256];
+               snprintf(buf, sizeof(buf), "%d selected", num_players);
+               str = buf;
+       } else {
+               // We don't have gender rule A, but we have gender counts,
+               // so show that. We don't make red here.
+               string str = format_gender_counts(gender_count);
+       }
+
+       // Seemingly this setting this every frame is very costly, so we diff.
+       if (QString::fromUtf8(str) != ui->selected_gender_ratio->text()) {
+               ui->selected_gender_ratio->setText(QString::fromUtf8(str));
+       }
+       bool current_ok = ui->selected_gender_ratio->styleSheet().isEmpty();
+       if (ok && !current_ok) {
+               ui->selected_gender_ratio->setStyleSheet("");
+       } else if (!ok && current_ok) {
+               ui->selected_gender_ratio->setStyleSheet("QLabel { color: red }");
+       }
+}
+
 void MainWindow::formation_double_clicked(bool offense, unsigned row)
 {
        FormationsModel *formations = offense ? offensive_formations : defensive_formations;
index ea4ba37048a4e893280713276b6558a5eba82ae9..c47e9bf8a8213128606ec6ecaacd148eef1401fd 100644 (file)
@@ -39,6 +39,7 @@ private:
        void update_status(uint64_t t);
        void update_player_buttons(uint64_t t);
        void update_action_buttons(uint64_t t);
+       void update_gender_ratio(uint64_t t);
 
        Ui::MainWindow *ui;
        EventsModel *events;
index 3806b608d438c57fac0839556312ddc51e0ea829..14608d3c9da48c8a032fb8abec00214cf1e07aa4 100644 (file)
@@ -6,7 +6,7 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>1251</width>
+    <width>1278</width>
     <height>754</height>
    </rect>
   </property>
@@ -18,7 +18,7 @@
     <item row="0" column="0">
      <layout class="QGridLayout" name="main_grid" rowstretch="1,0,0,0" columnstretch="1,0">
       <item row="0" column="1" rowspan="4">
-       <layout class="QVBoxLayout" name="buttons" stretch="0,0,0,0,0,0,0,1">
+       <layout class="QVBoxLayout" name="buttons" stretch="0,0,0,0,0,0,0,1,0">
         <item>
          <layout class="QGridLayout" name="player_grid">
           <item row="3" column="0" colspan="2">
           </property>
          </widget>
         </item>
+        <item>
+         <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,0,0">
+          <item>
+           <widget class="QPushButton" name="get_current_players">
+            <property name="text">
+             <string>Current</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QPushButton" name="clear_player_list">
+            <property name="text">
+             <string>Clear</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QLabel" name="selected_gender_ratio">
+            <property name="text">
+             <string>0/0 F, 0/0 M</string>
+            </property>
+            <property name="alignment">
+             <set>Qt::AlignCenter</set>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </item>
        </layout>
       </item>
       <item row="0" column="0">
     <rect>
      <x>0</x>
      <y>0</y>
-     <width>1251</width>
+     <width>1278</width>
      <height>23</height>
     </rect>
    </property>
index 0ed7d32edae845b1aaaaf8729ff4efacacbc026e..585956fde318023f57e00d62669b10f4c10c6b35 100644 (file)
@@ -63,6 +63,12 @@ string PlayersModel::get_player_name_by_id(unsigned player_id)
        return it->name;
 }
 
+string PlayersModel::get_player_gender_by_id(unsigned player_id)
+{
+       auto it = find_if(players.begin(), players.end(), [player_id](const Player &p) { return p.player_id == int(player_id); });
+       return it->gender;
+}
+
 void PlayersModel::load_data()
 {
        players.clear();
index a8940ff28683ff2e665b49bf67e7a2e5dc7563a3..b5ba81afb5590eec890767494986dd48e6515e93 100644 (file)
--- a/players.h
+++ b/players.h
@@ -25,7 +25,17 @@ public:
        int get_player_id(unsigned row) const {
                return players[row].player_id;
        }
+       std::string get_player_gender(unsigned row) const {
+               return players[row].gender;
+       }
        std::string get_player_name_by_id(unsigned player_id);
+       std::string get_player_gender_by_id(unsigned player_id);
+       QModelIndex get_row_start_qt(unsigned row) const {
+               return createIndex(row, 0);
+       }
+       QModelIndex get_row_end_qt(unsigned row) const {
+               return createIndex(row, 2);
+       }
 
 private:
        struct Player {