From 2a99f8f683f6b096078624cbb9561d1a444051f6 Mon Sep 17 00:00:00 2001
From: Csanad Tabajdi <tabajdi.csanad@proton.me>
Date: Sun, 9 Mar 2025 12:13:40 +0100
Subject: [PATCH] Refactor grouping logic and improve area export functionality
 in science plugins

---
 src/js/science_plugins.js | 221 ++++++++++++++++------------------
 src/js/sidebar.js         | 245 +++++++++++++++++++++-----------------
 2 files changed, 239 insertions(+), 227 deletions(-)

diff --git a/src/js/science_plugins.js b/src/js/science_plugins.js
index 8e195dc..53697fd 100644
--- a/src/js/science_plugins.js
+++ b/src/js/science_plugins.js
@@ -189,7 +189,7 @@ Science_plugins.prototype.updateSliceRegion = async function (img_id = _via_imag
   }
   _via_img_metadata[_via_image_id].updateHash(hash);
   this.GroupingWorker.postMessage({
-    image_id : img_id,
+    image_id: img_id,
     slice_regions_id: JSON.stringify(groupBy),
     regions: JSON.stringify(_via_img_metadata[img_id].regions),
     grouped_regions: JSON.stringify(_via_img_metadata[img_id].groupedRegions.groups, Project.replacer),
@@ -219,20 +219,20 @@ Science_plugins.prototype.groupingWorkerOnMessage = async function (e) {
 Science_plugins.prototype.getGroupingProgress = function (imageId, timeout = 5000) {
   return new Promise((resolve) => {
     const startTime = Date.now();
-    
+
     const checkProgress = () => {
-       // If no longer processing, resolve success
-       if (!this.workerSignal.has(imageId)) {
+      // If no longer processing, resolve success
+      if (!this.workerSignal.has(imageId)) {
         resolve(true);
         return;
       }
-      
+
       // If timeout reached, resolve false
       if (Date.now() - startTime > timeout) {
         resolve(false);
         return;
       }
-      
+
       // Check again after small delay
       setTimeout(checkProgress, 100);
     };
@@ -335,7 +335,7 @@ Science_plugins.prototype.calcPolygonArea = function (xCoordinates, yCoordinates
 
 // Export the region sizes to a csv file
 Science_plugins.prototype.ExportArea = async function () {
-  let rows = [
+  const rows = [
     ["Filename", "File_ID", "Treatment_ID", "Slice_ID", "Slice_area", "Infract_area", "Risk_area", "Slice_score", "Infract_score", "Risk_score"]
   ];
   let done = 0;
@@ -344,130 +344,119 @@ Science_plugins.prototype.ExportArea = async function () {
     if (_via_img_metadata[img_id].regions.length === 0) {
       continue;
     }
-    if (!_via_img_metadata[img_id].groupedRegions.groups.size > 0) {
-      if (!this.updateSliceRegion(img_id, false)) {
-        console.log("Slice regions updated but not grouped yet. Skipping image: " + img_id);
-        continue;
-      }
+    this.updateSliceRegion(_via_image_id, false);
+    await this.getGroupingProgress(_via_image_id);
+
+    let slices = [];
+    if (_via_img_metadata[img_id].groupedRegions.groups === 0) {
+      message.showError("No groups found! Ensure that there are slice regions in the image: " + _via_image_filename_list[img_index]);
+      return;
     }
-    await new Promise((resolve) => {
-      let interval = setInterval(() => {
-        if (_via_img_metadata[img_id].groupedRegions.groups.size > 0) {
-          clearInterval(interval);
-          resolve();
-        }
-      }, 100);
-    }).then(() => {
-      let slices = [];
-      if (_via_img_metadata[img_id].groupedRegions.groups === 0) {
-        return;
+    for (let i = 0; i < _via_img_metadata[img_id].groupedRegions.groupBy.length; i++) {
+      let element = _via_img_metadata[img_id].groupedRegions.groupBy[i];
+      let slice = {
+        ID: i + 1,
+        Area: 0,
+        InfarctArea: 0,
+        RiskArea: 0,
+        SliceScore: "n/a",
+        InfarctScore: "n/a",
+        RiskScore: "n/a",
+      };
+      let scores = {
+        score_exists: false,
+        Slice: 0,
+        Slice_no: 0,
+        Infarct: 0,
+        Infarct_no: 0,
+        Risk: 0,
+        Risk_no: 0,
+      };
+      if (_via_img_metadata[img_id].groupedRegions.groupIDs.has(element)) {
+        slice.ID = _via_img_metadata[img_id].groupedRegions.groupIDs.get(element);
       }
-      for (let i = 0; i < _via_img_metadata[img_id].groupedRegions.groupBy.length; i++) {
-        let element = _via_img_metadata[img_id].groupedRegions.groupBy[i];
-        let slice = {
-          ID: i + 1,
-          Area: 0,
-          InfarctArea: 0,
-          RiskArea: 0,
-          SliceScore: "n/a",
-          InfarctScore: "n/a",
-          RiskScore: "n/a",
-        };
-        let scores = {
-          score_exists: false,
-          Slice: 0,
-          Slice_no: 0,
-          Infarct: 0,
-          Infarct_no: 0,
-          Risk: 0,
-          Risk_no: 0,
-        };
-        if (_via_img_metadata[img_id].groupedRegions.groupIDs.has(element)) {
-          slice.ID = _via_img_metadata[img_id].groupedRegions.groupIDs.get(element);
-        }
-        slice.Area = this.calcPolygonArea(_via_img_metadata[img_id].regions[element].shape_attributes.all_points_x, _via_img_metadata[img_id].regions[element].shape_attributes.all_points_y);
-        if (_via_img_metadata[img_id].regions[element].hasOwnProperty("score")) {
+      slice.Area = this.calcPolygonArea(_via_img_metadata[img_id].regions[element].shape_attributes.all_points_x, _via_img_metadata[img_id].regions[element].shape_attributes.all_points_y);
+      if (_via_img_metadata[img_id].regions[element].hasOwnProperty("score")) {
+        scores.score_exists = true;
+        scores.Slice += _via_img_metadata[img_id].regions[element].score;
+        scores.Slice_no++;
+      }
+      for (let i = 0; i < _via_img_metadata[img_id].getGroupedRegion(element).length; i++) {
+        let region = _via_img_metadata[img_id].regions[_via_img_metadata[img_id].getGroupedRegion(element)[i]];
+        let Area = this.calcPolygonArea(region.shape_attributes.all_points_x, region.shape_attributes.all_points_y);
+        if (region.hasOwnProperty("score")) {
           scores.score_exists = true;
-          scores.Slice += _via_img_metadata[img_id].regions[element].score;
-          scores.Slice_no++;
-        }
-        for (let i = 0; i < _via_img_metadata[img_id].getGroupedRegion(element).length; i++) {
-          let region = _via_img_metadata[img_id].regions[_via_img_metadata[img_id].getGroupedRegion(element)[i]];
-          let Area = this.calcPolygonArea(region.shape_attributes.all_points_x, region.shape_attributes.all_points_y);
-          if (region.hasOwnProperty("score")) {
-            scores.score_exists = true;
-            switch (region.region_attributes.Type) {
-              case "Slice":
-                if (region.score === "n/a") {
-                  break;
-                }
-                scores.Slice += region.score;
-                scores.Slice_no++;
-                break;
-              case "Risk":
-                if (region.score === "n/a") {
-                  break;
-                }
-                scores.Risk += region.score;
-                scores.Risk_no++;
-                break;
-              case "Infarct":
-                if (region.score === "n/a") {
-                  break;
-                }
-                scores.Infarct += region.score;
-                scores.Infarct_no++;
-                break;
-            }
-          }
           switch (region.region_attributes.Type) {
             case "Slice":
-              slice.Area -= Area;
+              if (region.score === "n/a") {
+                break;
+              }
+              scores.Slice += region.score;
+              scores.Slice_no++;
               break;
             case "Risk":
-              slice.RiskArea += Area;
+              if (region.score === "n/a") {
+                break;
+              }
+              scores.Risk += region.score;
+              scores.Risk_no++;
               break;
             case "Infarct":
-              slice.InfarctArea += Area;
+              if (region.score === "n/a") {
+                break;
+              }
+              scores.Infarct += region.score;
+              scores.Infarct_no++;
               break;
           }
         }
-        if (scores.score_exists) {
-          if (scores.Slice_no > 0 && scores.Slice > 0) {
-            slice.SliceScore = scores.Slice / scores.Slice_no;
-          } else {
-            slice.SliceScore = "n/a";
-          }
-          if (scores.Infarct_no > 0 && scores.Infarct > 0) {
-            slice.InfarctScore = scores.Infarct / scores.Infarct_no;
-          } else {
-            slice.InfarctScore = "n/a";
-          }
-          if (scores.Risk_no > 0 && scores.Risk > 0) {
-            slice.RiskScore = scores.Risk / scores.Risk_no;
-          } else {
-            slice.RiskScore = "n/a";
-          }
+        switch (region.region_attributes.Type) {
+          case "Slice":
+            slice.Area -= Area;
+            break;
+          case "Risk":
+            slice.RiskArea += Area;
+            break;
+          case "Infarct":
+            slice.InfarctArea += Area;
+            break;
+        }
+      }
+      if (scores.score_exists) {
+        if (scores.Slice_no > 0 && scores.Slice > 0) {
+          slice.SliceScore = scores.Slice / scores.Slice_no;
+        } else {
+          slice.SliceScore = "n/a";
+        }
+        if (scores.Infarct_no > 0 && scores.Infarct > 0) {
+          slice.InfarctScore = scores.Infarct / scores.Infarct_no;
+        } else {
+          slice.InfarctScore = "n/a";
+        }
+        if (scores.Risk_no > 0 && scores.Risk > 0) {
+          slice.RiskScore = scores.Risk / scores.Risk_no;
+        } else {
+          slice.RiskScore = "n/a";
         }
-        slices.push(slice);
       }
-      let img_file = _via_image_filename_list[img_index];
-      let file_id = _via_img_metadata[img_id].file_attributes.ID || "";
-      slices.forEach((slice, i) => {
-        rows.push([
-          img_file,
-          file_id,
-          slice.ID,
-          slice.Area,
-          slice.InfarctArea,
-          slice.RiskArea,
-          slice.SliceScore,
-          slice.InfarctScore,
-          slice.RiskScore
-        ]);
-      });
-      done++;
+      slices.push(slice);
+    }
+    let img_file = _via_image_filename_list[img_index];
+    let file_id = _via_img_metadata[img_id].file_attributes.ID || "";
+    slices.forEach((slice, i) => {
+      rows.push([
+        img_file,
+        file_id,
+        slice.ID,
+        slice.Area,
+        slice.InfarctArea,
+        slice.RiskArea,
+        slice.SliceScore,
+        slice.InfarctScore,
+        slice.RiskScore
+      ]);
     });
+    done++;
 
   }
   let csvContent = "data:text/csv;charset=utf-8," + rows.map(e => e.join(",")).join("\n");
diff --git a/src/js/sidebar.js b/src/js/sidebar.js
index baf0a18..7ee6d0d 100644
--- a/src/js/sidebar.js
+++ b/src/js/sidebar.js
@@ -696,7 +696,7 @@ class Sidebar {
         this.ae.empty();
         this.annotation_editor_update_header_html();
         this.annotation_editor_update_metadata_html();
-        this.ae.removeClass("d-none"); // show the annotation editor
+        this.ae.removeClass("d-none");
         drawing.updateCheckedLockHtml();
       }
     } catch (err) {
@@ -882,117 +882,145 @@ class Sidebar {
     }
   }
 
-  async annotation_editor_update_metadata_html() {
-    if (!_via_img_count) {
-      return;
+  ensureTableBodyExists() {
+    if (!this.ae.find("tbody").length) {
+      const tbody = $('<tbody id="annotation_editor_content"></tbody>');
+      this.ae.append(tbody);
+      this.aec = tbody;
     }
+  }
 
-    if (!this.ae.has("tbody").length) {
-      this.ae.append('<tbody id="annotation_editor_content"></tbody>');
-      this.aec = $("#annotation_editor_content");
-    }
+  createGroupRow(groupIndex, groupId) {
+    const rowId = `ae_row_${groupId}`;
+    const row = $("<tr>", {
+      id: rowId,
+      onclick: "plugin.selectAllRegionsInGroup(this)",
+      style: "height: 40px;",
+      class: "align-bottom",
+    });
+    
+    const header = $("<th>", {
+      scope: "row",
+      id: `ae_${groupId}_rid`,
+      html: "O",
+    });
+    
+    const placeholder = _via_img_metadata[_via_image_id].groupedRegions.groupIDs.has(groupId) 
+      ? _via_img_metadata[_via_image_id].groupedRegions.groupIDs.get(groupId)
+      : `Group: ${groupIndex + 1}`;
+    
+    const cell = $("<td>", {
+      id: `ae_row_${groupId}_group`,
+      colspan: 4,
+    });
+    
+    const input = $("<input>", {
+      class: 'form-control form-control-sm',
+      onchange: 'plugin.changeGroupIdentifier(this)',
+      type: 'text',
+      placeholder: placeholder,
+      'aria-label': 'group identifier',
+      id: `group_${groupId}`
+    });
+    
+    cell.append(input);
+    row.append(header, cell);
+    
+    return row;
+  }
+  
 
-    switch (_via_metadata_being_updated) {
-      case "region":
-        let rindex;
-        if (_via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID) {
-          this.aec.append(this.annotation_editor_get_metadata_row_html(0));
+  async updateRegionMetadata(){
+    let rindex;
+    if (_via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID) {
+      this.aec.append(this.annotation_editor_get_metadata_row_html(0));
+    } else {
+      if (_via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE) {
+        if (_via_annotation_editor_mode === VIA_ANNOTATION_EDITOR_MODE.SINGLE_REGION) {
+          this.aec.append(
+            this.annotation_editor_get_metadata_row_html(
+              drawing.userSelRegionId()
+            )
+          );
         } else {
-          if (_via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE) {
-            if (_via_annotation_editor_mode === VIA_ANNOTATION_EDITOR_MODE.SINGLE_REGION) {
-              this.aec.append(
-                this.annotation_editor_get_metadata_row_html(
-                  drawing.userSelRegionId()
-                )
+          if (_via_img_metadata[_via_image_id].isGroupable()) {
+            plugin.updateSliceRegion(_via_image_id, false);
+            await plugin.getGroupingProgress(_via_image_id);
+            let tmp = [];
+            let colspan = 4;
+            let done_ids = [];
+            const fragment = document.createDocumentFragment();
+            for (rindex = 0; rindex < _via_img_metadata[_via_image_id].groupedRegions.groupBy.length; ++rindex) {
+              if (!_via_img_metadata[_via_image_id].isGroupedKey(_via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex])) {
+                continue;
+              }
+              this.createGroupRow(rindex, _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex]).appendTo(fragment);
+              $(fragment).append(
+                this.annotation_editor_get_metadata_row_html(_via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex])
               );
-            } else {
-              if (_via_img_metadata[_via_image_id].isGroupable()) {
-                plugin.updateSliceRegion(_via_image_id);
-                await plugin.getGroupingProgress(_via_image_id);
-                let tmp = [];
-                let colspan = 4;
-                let done_ids = [];
-                for (rindex = 0; rindex < _via_img_metadata[_via_image_id].groupedRegions.groupBy.length; ++rindex) {
-                  if (!_via_img_metadata[_via_image_id].isGroupedKey(_via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex])) {
-                    continue;
-                  }
-                  $("<tr>", {
-                    id: "ae_row_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex],
-                    onclick: "plugin.selectAllRegionsInGroup(this)",
-                    style: "height: 40px;",
-                    class: "align-bottom",
-                  }).appendTo(this.aec);
-                  $("<th>", {
-                    scope: "row",
-                    id: "ae_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex] + "_rid",
-                    html: "O",
-                  }).appendTo("#ae_row_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex]);
-                  if (_via_img_metadata[_via_image_id].groupedRegions.groupIDs.has(_via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex])) {
-                    $("<td> ", {
-                      id: "ae_row_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex] + "_group",
-                      colspan: colspan,
-                      html: "<input class='form-control form-control-sm' onchange='plugin.changeGroupIdentifier(this)' type='text' placeholder='" + _via_img_metadata[_via_image_id].groupedRegions.groupIDs.get(_via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex]) + "' aria-label='group identifier' id='group_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex] + "'>"
-                    }).appendTo("#ae_row_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex]);
-                  } else {
-                    $("<td> ", {
-                      id: "ae_row_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex] + "_group",
-                      colspan: colspan,
-                      html: "<input class='form-control form-control-sm' onchange='plugin.changeGroupIdentifier(this)' type='text' placeholder='" + "Group: " + (rindex + 1) + "' aria-label='group identifier' id='group_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex] + "'>"
-                    }).appendTo("#ae_row_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex]);
-                  }
-
-                  this.aec.append(
-                    this.annotation_editor_get_metadata_row_html(_via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex])
-                  );
-                  done_ids.push(_via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex]);
-                  tmp = _via_img_metadata[_via_image_id].getGroupedRegion(_via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex]);
-                  for (const element of tmp) {
-                    this.aec.append(
-                      this.annotation_editor_get_metadata_row_html(element)
-                    );
-                    done_ids.push(element);
-                  }
-                }
-                $("<tr>", {
-                  id: "ae_no_group",
-                  style: "height: 40px;",
-                  class: "align-bottom",
-                }).appendTo(this.aec);
-                $("<th>", {
-                  scope: "row",
-                  id: "ae_no_group_rid",
-                  html: "X",
-                }).appendTo("#ae_no_group");
-                $("<td> ", {
-                  id: "ae_row_no_group",
-                  colspan: colspan,
-                  html: "Other",
-                }).appendTo("#ae_no_group");
-                for (
-                  rindex = 0;
-                  rindex < _via_img_metadata[_via_image_id].regions.length;
-                  ++rindex
-                ) {
-                  if (!done_ids.includes(rindex)) {
-                    this.aec.append(
-                      this.annotation_editor_get_metadata_row_html(rindex)
-                    );
-                  }
-                }
-              } else {
-                for (
-                  rindex = 0;
-                  rindex < _via_img_metadata[_via_image_id].regions.length;
-                  ++rindex
-                ) {
-                  this.aec.append(
-                    this.annotation_editor_get_metadata_row_html(rindex)
-                  );
-                }
+              done_ids.push(_via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex]);
+              let tmp = _via_img_metadata[_via_image_id].getGroupedRegion(_via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex]);
+              for (const element of tmp) {
+                $(fragment).append(
+                  this.annotation_editor_get_metadata_row_html(element)
+                );
+                done_ids.push(element);
               }
             }
+            this.aec.append(fragment);
+            const row = $("<tr>", {
+              id: "ae_no_group",
+              style: "height: 40px;",
+              class: "align-bottom",
+            });
+            const header = $("<th>", {
+              scope: "row",
+              id: "ae_no_group_rid",
+              html: "X",
+            })
+            const cell = $("<td> ", {
+              id: "ae_row_no_group",
+              colspan: colspan,
+              html: "Other",
+            });
+            row.append(header, cell);
+            $(fragment).append(row);
+            for (rindex = 0; rindex < _via_img_metadata[_via_image_id].regions.length; ++rindex) {
+              if (!done_ids.includes(rindex)) {
+                $(fragment).append(
+                  this.annotation_editor_get_metadata_row_html(rindex)
+                );
+              }
+            }
+            this.aec.append(fragment);
+          } else {
+            const fragment = document.createDocumentFragment();
+            for (
+              rindex = 0;
+              rindex < _via_img_metadata[_via_image_id].regions.length;
+              ++rindex
+            ) {
+              $(fragment).append(
+                this.annotation_editor_get_metadata_row_html(rindex)
+              );
+            }
+            this.aec.append(fragment);
           }
         }
+      }
+    }
+  }
+
+  async annotation_editor_update_metadata_html() {
+    if (!_via_img_count) {
+      return;
+    }
+
+    this.ensureTableBodyExists(); // experiment
+
+    switch (_via_metadata_being_updated) {
+      case "region":
+        await this.updateRegionMetadata();
         break;
 
       case "file":
@@ -1002,10 +1030,8 @@ class Sidebar {
   }
 
   annotation_editor_update_row(row_id) {
-    if (!this.ae.has("tbody").length) {
-      this.ae.append('<tbody id="annotation_editor_content"></tbody>');
-      this.aec = $("#annotation_editor_content");
-    }
+
+    this.ensureTableBodyExists(); // experiment
     let new_row = this.annotation_editor_get_metadata_row_html(row_id);
     let id = new_row.attr("id");
 
@@ -1016,10 +1042,7 @@ class Sidebar {
 
   annotation_editor_add_row(row_id) {
     if (this.is_annotation_editor_visible()) {
-      if (!this.ae.has("tbody").length) {
-        this.ae.append('<tbody id="annotation_editor_content"></tbody>');
-        this.aec = $("#annotation_editor_content");
-      }
+      this.ensureTableBodyExists(); // experiment
       let new_row = this.annotation_editor_get_metadata_row_html(row_id);
       let penultimate_row_id = parseInt(row_id) - 1;
       if (penultimate_row_id >= 0) {
-- 
GitLab