MisatoTremor / AntiGameReborn SpyTableGrouper

// ==UserScript==
// @name         AntiGameReborn SpyTableGrouper
// @namespace    https://openuserjs.org/users/MisatoTremor
// @version      1.0.3
// @description  Groups the AntiGameReborn SpyTable by galaxy
// @author       MisatoTremor
// @license      MIT
// @copyright    2023, MisatoTremor
// @match        https://*.ogame.gameforge.com/game/index.php?page=messages*
// @updateURL    https://openuserjs.org/meta/MisatoTremor/AntiGameReborn_SpyTableGrouper.meta.js
// @downloadURL  https://openuserjs.org/install/MisatoTremor/AntiGameReborn_SpyTableGrouper.user.js
// @grant        GM_addStyle
// @run-at       document-start
// ==/UserScript==
'use strict';
// #region
class SpyTableGrouper
{
    constructor()
    {
        this.observerQueue = {};
        this.mutationList = {};
        this.observerTimers = [];

        this.observer = new MutationObserver(mutations =>
        {
            (() =>
            {
                let now = performance.now();

                for(let i=0, len=mutations.length; i<len; i++)
                {
                    let mutation = mutations[i];

                    if('agoSpyReportOverview' == mutation.target.id)
                    {
                        this.mutationList['tablereport'] = now;
                    }
                }
            })();

            if(Object.keys(this.mutationList).length > 0) tryUpdate();
        });
        let tryUpdate = () =>
        {
            Object.keys(this.mutationList).forEach(k =>
            {
                if(typeof this.observerQueue[k] === 'function') this.observerQueue[k]();
                delete this.mutationList[k];
            });
        }
        this.observer.observe(document.querySelector("#ui-id-2"), {childList:true, subtree:true});

        this.observeMutation(() => this.groupTable(), 'tablereport');
    }

    groupTable()
    {
        const table = document.querySelector("#spyTable");
        if (table && !table.classList.contains("grouped")) {
            // Get all the rows of the table
            const tableBody = document.querySelector("#spyTable tbody");
            const tableRows = Array.from(table.querySelectorAll("tbody tr"));
            // Sort the rows based on their x-coordinate values
            tableRows.sort((row1, row2) => {
                const x1 = parseFloat(row1.cells[0].textContent);
                const x2 = parseFloat(row2.cells[0].textContent);
                return x1 - x2;
            });
            tableBody.remove();
            // Create an empty tbody for the first group
            let currentGroup = document.createElement("tbody");
            // Iterate over the sorted rows and group them
            tableRows.forEach((row) => {
                const x = parseFloat(row.cells[0].textContent);
                if (x !== parseFloat(currentGroup.id)) {
                    // If the x-coordinate of the current row is different from the current group,
                    // create a new tbody and append the current group to the table
                    currentGroup = document.createElement("tbody");
                    currentGroup.id = x;
                    table.appendChild(currentGroup);
                }
                row.classList.remove("even")
                // Append the current row to the current group
                currentGroup.appendChild(row);
            });
            table.classList.add("grouped");
        }
    }

    observeMutation(callback, id)
    {
        this.observerQueue[id] = callback;
    }
}

class Util
{
    static createDom(element, params, content)
    {
        params = params || {};
        content = content ?? '';

        let dom = document.createElement(element);
        Object.entries(params).forEach(p => dom.setAttribute(p[0], p[1]));
        dom.innerHTML = content;

        return dom;
    }
}
// #endregion

if(document.readyState !== 'loading')
{
    unsafeWindow.agr_stg = new SpyTableGrouper();
}
else
{
    window.addEventListener("DOMContentLoaded", () =>
    {
        unsafeWindow.agr_stg = new SpyTableGrouper();
    });
}

// #region
const css =
`
/*css*/
#spyTable tbody:not(:last-of-type)::after {
    content: '';
    display: block;
    height: 1rem;
}
#spyTable tbody tr:nth-child(even) {
    background-color: #0f141a;
}
/*!css*/
`;

GM_addStyle(css);
// #endregion