routerconsole/jsp/js/graphSingle.js

/**
 * @module graphSingle
 * @description Handles AJAX graph refresh and button interactions for single graph
 * pages. Manages hide/show legend state via localStorage and URL synchronization,
 * and provides periodic graph image updates.
 * @author dr|z3d
 * @license AGPL3 or later
 */

/** @type {HTMLImageElement|null} */
let graphImage;
/** @type {string} */
const LS_KEY = "singleGraphHideLegend";

/**
 * Reads the hide legend preference from localStorage.
 * @function getLocalStorageHideLegend
 * @returns {boolean} True if legend should be hidden, false otherwise
 */
function getLocalStorageHideLegend() {
  try {
    const stored = localStorage.getItem(LS_KEY);
    return stored === "true";
  } catch (e) {
    return false;
  }
}

/**
 * Saves the hide legend preference to localStorage.
 * @function setLocalStorageHideLegend
 * @param {boolean} value - Whether the legend should be hidden
 * @returns {void}
 */
function setLocalStorageHideLegend(value) {
  try {
    localStorage.setItem(LS_KEY, value ? "true" : "false");
  } catch (e) {}
}

/**
 * Reads the hideLegend query parameter from the current URL.
 * @function getHideLegendFromURL
 * @returns {boolean|null} True/false from URL, or null if not present
 */
function getHideLegendFromURL() {
  const urlParams = new URLSearchParams(window.location.search);
  const value = urlParams.get("hideLegend");
  if (value === "true") { return true; }
  if (value === "false") { return false; }
  return null;
}

/**
 * Updates or appends the hideLegend parameter in the given URL string.
 * @function updateHideLegendInURL
 * @param {string} url - The URL to update
 * @param {boolean} hideLegend - The new value for the hideLegend parameter
 * @returns {string} The updated URL string
 */
function updateHideLegendInURL(url, hideLegend) {
  const param = "hideLegend=" + (hideLegend ? "true" : "false");
  if (url.includes("hideLegend=")) {
    return url.replace(/hideLegend=(true|false)/, param);
  }
  return url + (url.includes("?") ? "&" : "?") + param;
}

/**
 * Synchronizes localStorage hide legend state with the URL parameter if present.
 * @function syncLocalStorageWithURL
 * @returns {void}
 */
function syncLocalStorageWithURL() {
  const urlHideLegend = getHideLegendFromURL();
  if (urlHideLegend !== null) {
    setLocalStorageHideLegend(urlHideLegend);
  }
}

/**
 * Sets up click delegation for graph option links, handling legend toggling
 * and graph updates via fetch.
 * @function initButtons
 * @returns {void}
 */
function initButtons() {
  document.addEventListener("click", function(event) {
    if (event.target.closest("#graphopts a")) {
      event.preventDefault();
      const link = event.target.closest("#graphopts a");
      const href = link.getAttribute("href");

      // Ensure the URL matches localStorage for all links
      const storedHideLegend = getLocalStorageHideLegend();
      const adjustedHref = updateHideLegendInURL(href, storedHideLegend);

      // Check if this is a Hide/Show Legend link
      if (link.textContent.includes("Legend")) {
        // Toggle localStorage state
        const newHideLegend = !storedHideLegend;
        setLocalStorageHideLegend(newHideLegend);

        // Update URL with new state
        const newHref = updateHideLegendInURL(href, newHideLegend);

        fetch(newHref)
          .then(response => response.text())
          .then(data => {
            const temp = document.createElement("div");
            temp.innerHTML = data;

            // Update the graph image
            const updatedImgSrc = temp.querySelector("#graphSingle").src;
            graphImage.src = updatedImgSrc;

            // Update the graph options
            const graphopts = document.getElementById("graphopts");
            const updatedOpts = temp.querySelector("#graphopts").innerHTML;
            graphopts.innerHTML = updatedOpts;

            // Update the address bar
            history.pushState(null, "", newHref);
            syncLocalStorageWithURL();
            initCss();
          })
          .catch(error => console.error("Error updating graph: ", error));
      } else {
        // Handle other buttons with localStorage-synced URL
        fetch(adjustedHref)
          .then(response => response.text())
          .then(data => {
            const temp = document.createElement("div");
            temp.innerHTML = data;

            // Update the graph image
            const updatedImgSrc = temp.querySelector("#graphSingle").src;
            graphImage.src = updatedImgSrc;

            // Update the graph options
            const graphopts = document.getElementById("graphopts");
            const updatedOpts = temp.querySelector("#graphopts").innerHTML;
            graphopts.innerHTML = updatedOpts;

            // Update the address bar
            history.pushState(null, "", adjustedHref);
            syncLocalStorageWithURL();
            initCss();
          })
          .catch(error => console.error("Error updating graph: ", error));
      }
    }
  });
}

/**
 * Injects the graph container width style in one shot. Safe to call before
 * the image has loaded - the width falls back to the image attribute until
 * the next onload event re-injects with the natural width.
 * @function initCss
 * @returns {void}
 */
function initCss() {
  if (window._graphCssTimer) {clearInterval(window._graphCssTimer);}
  if (!graphImage) {return;}
  injectCss();
}

/**
 * Injects a style element with the graph container width based on the image dimensions.
 * @function injectCss
 * @returns {void}
 */
function injectCss() {
  if (!graphImage) {return;}
  var graphWidth = graphImage.naturalWidth;
  var graphHeight = graphImage.naturalHeight;
  var graphcss = document.getElementById("graphcss");
  if (graphcss) {graphcss.remove();}
  var s = document.createElement("style");
  s.setAttribute("id", "graphcss");
  var w = graphWidth !== 0 ? graphWidth + 4 : graphImage.width + 4;
  s.innerHTML = ".graphContainer{width:" + w + "px}";
  document.head.appendChild(s);
}

/**
 * Sets up periodic graph image refresh at the configured interval.
 * @function refreshGraph
 * @returns {void}
 */
function refreshGraph() {
  if (graphRefreshInterval > 0) {
    var graph = document.getElementById("single");
    graphImage = document.getElementById("graphSingle");

    function updateGraphURL() {
      var currentImgSrc = graphImage.src;
      var graphURL = currentImgSrc.replace(/time=\d+/, "time=" + Date.now());
      // Preserve hideLegend in refresh
      const storedHideLegend = getLocalStorageHideLegend();
      graphURL = updateHideLegendInURL(graphURL, storedHideLegend);
      graphImage.src = graphURL;
      graphImage.onload = function() {initCss();};
    }
    updateGraphURL();

    if (graph && document.visibilityState == "visible") {
      setInterval(function() {
        progressx.show(window.theme);
        updateGraphURL();
        progressx.hide();
      }, graphRefreshInterval);
    }
  }
}

/**
 * Checks localStorage for a hide legend preference and redirects the page
 * if the URL doesn't reflect the stored preference.
 * @function checkLocalStorageAndRedirect
 * @returns {boolean} True if a redirect was initiated, false otherwise
 */
function checkLocalStorageAndRedirect() {
  const urlHideLegend = getHideLegendFromURL();

  if (urlHideLegend === null) {
    const storedHideLegend = getLocalStorageHideLegend();
    if (storedHideLegend) {
      const newHref = updateHideLegendInURL(window.location.href, true);
      window.location.href = newHref;
      return true;
    }
  } else {
    // Sync localStorage with URL
    setLocalStorageHideLegend(urlHideLegend);
  }
  return false;
}

document.addEventListener("DOMContentLoaded", () => {
  // Check localStorage and redirect if needed
  if (checkLocalStorageAndRedirect()) {
    return;
  }

  graphImage = document.getElementById("graphSingle");
  initButtons();
  progressx.hide();
  graphImage.onload = function() {initCss();}
  refreshGraph();
});