infoComputing.js

/**
 * @file infoComputing.js
 * @description This file contains every fonction needed to compute a result to display on-screen as numbers or as signals, etc.
 * 
 * Functions included in this file : 
 *  - **getTimeScale**
 *  - **getTimePerDiv**
 *  - **mapVoltageToRaw**
 *  - **mapRawToVoltage**
 *  - **getTimeBetweenCursors**
 *  - **getMillivoltsBetweenCursors**
 *  - **getMilliVoltForACursor**
 *  - **getMilliVoltsRelativeToTriggerCursor**
 *  - **getTimeForACursor**
 *  - **setScreenInformation**
 *  - **generatePoints**
 *  - **calculateAutoMeasures**
 *  - **getMilliVoltsPerDiv**
 *  - **updateGeneratedMathSignalsData**
 *  - **toggleMeasurement**
 *  - **resetMeasurements**
 *  - **getPositionRelativeToTriggerCursor**
 *  - **updateTriggerSettings**
 *  - **calculateZoomFactors**
 * 
 * @version 1.0.0
 * @since 2024-05-31
 * @author Owen Pichot
 * 
 * @license Public Domain
 */

/**
 * @module Computing
 */

/**
 * This function takes a time in seconds and scales it to something that makes more sense depending on the value.
 * 
 * @function getTimeScale
 * @memberof module:Computing
 * @param {number} timeInSeconds Time period in seconds. 
 * @returns {object} An object with two attributes, 'value' & 'scale'.
 * @example
 * getTimeScale(0.05);
 * //output : { value: 50, scale: "ms" }
 * getTimeScale(0.0004);
 * //output : { value: 400, scale: "µs" }
 * getTimeScale(0.00000154);
 * //output : { value: 1.54, scale: "µs" }
 */
function getTimeScale(timeInSeconds){
    let scale;
    let value;

    //convert timeInSeconds to ms as a base unit to simplify the logic of the function
    const microseconds = timeInSeconds * 1e6;

    if (microseconds < 1) {// nanoseconds
        scale = 'ns'; 
        value = (microseconds * 1000).toFixed(2); // convert to nanoseconds
    } else if (microseconds < 1000) {// microseconds
        scale = 'µs'; 
        value = microseconds.toFixed(2);
    } else if (microseconds < 1e6) {// Milliseconds
        scale = 'ms'; 
        value = (microseconds / 1000).toFixed(2); // convert microseconds to milliseconds
    } else { // Seconds
        scale = 's';
        value = (microseconds / 1e6).toFixed(2); // convert microseconds to seconds
    }

    return { value: parseFloat(value), scale: scale };
};

/**
 * This function returns how much time represents 1 horizontal division on screen.
 * 
 * @function getTimePerDiv
 * @memberof module:Computing
 * @returns {object} An object with two attributes, 'value' & 'scale'.
 */
function getTimePerDiv() { 
    const totalSamplingTime = config.samplesPerFrame * 1e-8;
    const timePerDivision = (totalSamplingTime / config.horizontalDivisions) * (horizontalScale / 50);
    //we have to divide by 50 because the default value of the input is 50 which corresponds to 1 : no scaling
    
    const resultScaled = getTimeScale(timePerDivision);
    return resultScaled;
};

/**
 * This function converts a voltage to the equivalent absolute raw value of a 14-bit ADC
 * @function mapVoltageToRaw
 * @memberof module:Computing
 * @param {number} voltage - Voltage to convert.
 * @returns {number} The corresponding raw value to the voltage given.
 */
function mapVoltageToRaw(voltage) {
    return (voltage + (config.voltage / 2)) / config.voltage * config.maxSampleValue;
};

/**
 * This function converts a raw value from a 14-bit ADC to the equivalent voltage.
 * 
 * @function mapRawToVoltage
 * @memberof module:Computing
 * @param {number} rawValue - Raw value to convert.
 * @returns {number} The correponding voltage to the raw value given.
 */
function mapRawToVoltage(rawValue) {
    return (rawValue / config.maxSampleValue * config.voltage) - (config.voltage / 2);
}

/**
 * This function is used when the user uses the vertical measure cursors on the screen.
 * It calculates the time represented between the two vertical measure cursors.
 * @function getTimeBetweenCursors
 * @memberof module:Computing
 * @param {number} pixelsBetweenCursors Number of pixels between the two cursors.
 * @returns {number|string} Returns the time value and the corresponding scale.
 * @example
 * // /!\ Without any horizontal scaling /!\
 * getTimeBetweenCursors(542);
 * //output : { value: 4.63, scale: "µs" }
 * getTimeBetweenCursors(247):
 * //output : { value: 2.11, scale: "µs" }
 */
function getTimeBetweenCursors(pixelsBetweenCursors){
    const totalSamplingTime = config.samplesPerFrame * 1e-8;
    const sizeOfOneDivisionInPixels = CANVAS.width / config.horizontalDivisions;
    const DivisionsBetweenCursors = pixelsBetweenCursors / sizeOfOneDivisionInPixels;
    
    const TimeBetweenCursors = totalSamplingTime * (DivisionsBetweenCursors / config.horizontalDivisions) * (horizontalScale / 50);

    const resultScaled = getTimeScale(TimeBetweenCursors);
    return resultScaled;
};

/**
 * This function, similarly to 'getTimeBetweenCursors' gets the voltage represented between the two horizontal measure cursors.
 * It takes into account which channel we are currently focused on and its current vertical scaling.
 * 
 * @function getMillivoltsBetweenCursors
 * @memberof module:Computing
 * @param {number} pixelsBetweenCursors Number of pixels between the two cursors.
 * @returns {number|string} Returns the voltage and its associated scale (mv, v, etc).
 * @example
 * getMillivoltsBetweenCursors(254);
 * //output : { value: "698.5", scale: "mV" }
 * getMillivoltsBetweenCursors(57);
 * //output : { value: "156.8", scale: "mV" }
 */
function getMillivoltsBetweenCursors(pixelsBetweenCursors){
    //If a channel is focused then we get the mv/div of this one.
    let result = {value: "No channel", scale: "selected"};

    Object.keys(channelData).forEach(key => {
        if(channelData[key].focused){
            const sizeOfOneDivisionInPixels = CANVAS.height / config.verticalDivisions;
            const milliVoltsPerDivision = getMilliVoltsPerDiv(channelData[key].verticalScale);
            const milliVoltsBetweenCursors = (pixelsBetweenCursors / sizeOfOneDivisionInPixels) * milliVoltsPerDivision;
            result =  {value: milliVoltsBetweenCursors.toFixed(1), scale: "mV"};
        }
    });
    //If not, we just return "NA" until the user selects a certain channel. 
    return result;
};

/**
 * This function returns the equivalent in mV relative to the position of a horizontal measure cursor.
 * It also takes into account the vertical scaling of the current channel focused.
 * If no channel is focused then the function will just return "No channel Selected".
 * 
 * @function getMilliVoltForACursor
 * @memberof module:Computing
 * @param {number} cursorPosition Position, in pixels from the top of the screen to the cursor.
 * @returns {number|string} Returns the voltage (in mv) equivalent to the position of the cursor.
 * @example
 * getMilliVoltForACursor(150);
 * //output : { value: "687.5", scale: "mV" }
 * getMilliVoltForACursor(540);
 * //output : { value: -385, scale: "mV" }
 * getMilliVoltForACursor(300);//While no channel is focused.
 * //output : { value: "No channel", scale: "selected" }
 */
function getMilliVoltForACursor(cursorPosition){
    let result = {value: "No channel", scale: "selected"};

    Object.keys(channelData).forEach(key => {
        if(channelData[key].focused){
            const sizeOfOneDivisionInPixels = CANVAS.height / config.verticalDivisions;
            const milliVoltsPerDivision = getMilliVoltsPerDiv(channelData[key].verticalScale);
            if (cursorPosition == (CANVAS.height / 2)){
                result = {value: "0", scale: "mV"};
            }else if (cursorPosition > (CANVAS.height / 2)){//negative values
                const cursorValue = ((cursorPosition - (CANVAS.height / 2)) / sizeOfOneDivisionInPixels) * milliVoltsPerDivision;
                result = {value: -cursorValue.toFixed(1), scale: "mV"}
            }else if (cursorPosition < (CANVAS.height / 2)){//positive values
                const cursorValue = (((CANVAS.height / 2) - cursorPosition) / sizeOfOneDivisionInPixels) * milliVoltsPerDivision;
                result = {value: cursorValue.toFixed(1), scale: "mV"}
            }
        }
    });
    return result;
};

/**
 * This fonction returns how much mV represents the current position of the trigger cursor (right of the screen, T icon).
 * This function is mostly used when the user moves the trigger cursor to change the current threshold.
 * 
 * @function getMilliVoltsRelativeToTriggerCursor
 * @memberof module:Computing
 * @param {number} Position Position of the trigger cursor, in pixels, on its scrollbar.
 * @returns {number|string} Returns the voltage (in mv) relative to the cursor's position.
 * @example
 * getMilliVoltsRelativeToTriggerCursor(150);
 * //output : { value: 687.5, scale: "mV" }
 * getMilliVoltsRelativeToTriggerCursor(430);
 * //output : { value: -82.5, scale: "mV" }
 */
function getMilliVoltsRelativeToTriggerCursor(Position){
    const triggerChannel = triggerOptions.triggerChannel;
    const sizeOfOneDivisionInPixels = CANVAS.height / config.verticalDivisions;
    const milliVoltsPerDivision = getMilliVoltsPerDiv(channelData[triggerChannel].verticalScale);
    let result;
    if (Position == (CANVAS.height / 2)){
        result = {value: "0", scale: "mV"};
    }else if (Position > (CANVAS.height / 2)){//negative values
        const cursorValue = ((Position - (CANVAS.height / 2)) / sizeOfOneDivisionInPixels) * milliVoltsPerDivision;
        result = {value: -cursorValue.toFixed(1), scale: "mV"}
    }else if (Position < (CANVAS.height / 2)){//positive values
        const cursorValue = (((CANVAS.height / 2) - Position) / sizeOfOneDivisionInPixels) * milliVoltsPerDivision;
        result = {value: cursorValue.toFixed(1), scale: "mV"}
    }
    return result;
};

/**
 * Similarly to 'getMilliVoltForACursor', this function returns the equivalent time relative to the position of a vertical measure cursor.
 * It takes into account the global horizontal scaling of the oscilloscope.
 * 
 * @function getTimeForACursor
 * @memberof module:Computing
 * @param {number} cursorPosition Position, in pixels, from the left of the screen to the cursor.
 * @returns {number|string} Returns the time and time-scale relative to the cursor's position.
 * @example
 * getTimeForACursor(150);
 * //output : { value: 1.28, scale: "µs" }
 * getTimeForACursor(655);
 * //output : { value: 5.59, scale: "µs" }
 */
function getTimeForACursor(cursorPosition){
    const totalSamplingTime = config.samplesPerFrame * 1e-8; // Total time for all divisions.
    const sizeOfOneDivisionInPixels = CANVAS.width / config.horizontalDivisions; // Pixel width of one division.

    // Calculate how many divisions the cursor is from the left of the canvas.
    const divisionsFromLeft = cursorPosition / sizeOfOneDivisionInPixels;

    // Calculate the time at the cursor position using the proportion of the total time.
    const timeAtCursor = totalSamplingTime * (divisionsFromLeft / config.horizontalDivisions) * (horizontalScale / 50);

    const resultScaled = getTimeScale(timeAtCursor);
    return resultScaled;
};

/**
 * This function displays multiple informations below the screen like Mv/div, S/div, etc etc.
 * It also displays all the 'auto-measures' selected by the user for a specific channel like vpp, rms, etc.
 * This function's job is more about displaying the information on screen and not so much about doing the actual calculations for each value.
 * 
 * @function setScreenInformation
 * @memberof module:Computing
 * @returns {void}
 */
function setScreenInformation(){
    //insert time scale to the screen
    const timePerDiv = getTimePerDiv();
    document.getElementById('tpdiv-value').innerHTML = timePerDiv.value + ' ' + timePerDiv.scale + '/div';

    Object.keys(channelData).forEach(key => {
        const voltsPerDiv = getMilliVoltsPerDiv(channelData[key].verticalScale);
        let channelNumber = parseInt(key.substring(2), 10);
        if (voltsPerDiv > 1000){
            document.getElementById('mes-CH' + channelNumber).innerHTML = (voltsPerDiv / 1000).toFixed(2) + ' V/dv';
        }else{
            document.getElementById('mes-CH' + channelNumber).innerHTML = voltsPerDiv + ' mv/dv';
        }
        document.getElementById('mes-CH' + channelNumber).style.color = channelData[key].colorDark;
    });

    if (triggerOptions.isTriggerOn == "on"){
        if (triggered){
            TRIGGER.style.color = "lightgreen"
        }else{
            TRIGGER.style.color = "chocolate"
        }
    }else{
        TRIGGER.style.color = "#e2e2e2"//white
    }

    //This section is about the auto-measures display.
    //These are put on screen below the voltages display.
    valuesToDisplay = []; // format inside : {text: "textToDisplay", color: "colorOfTheChannel", value: "valueToDisplay"}

    let min, max//we declare them here to reuse in case vpp and either min or max are set, it prevent another call to the function.

    if (autoMeasureOptions.vpp.set){// get min, max, vpp
        min = autoMeasures.getMinValue(channelData[autoMeasureOptions.associatedChannel].points);
        max = autoMeasures.getMaxValue(channelData[autoMeasureOptions.associatedChannel].points);
        let vpp = autoMeasures.getVppValue(channelData[autoMeasureOptions.associatedChannel].points);

        valuesToDisplay.push({
            text: "Vpp", 
            color: channelData[autoMeasureOptions.associatedChannel].colorDark, 
            value: vpp.toFixed(1) + "mV",
            id: "vpp-measure",
        });
    };

    if (autoMeasureOptions.min.set){// get only min
        if (min == undefined){
            min = autoMeasures.getMinValue(channelData[autoMeasureOptions.associatedChannel].points);
        }
        valuesToDisplay.push({
            text: "Min", 
            color: channelData[autoMeasureOptions.associatedChannel].colorDark, 
            value: min.toFixed(1) + "mV",
            id: "min-measure",
        });
    };

    if (autoMeasureOptions.max.set){// get only max
        if (max == undefined){
            max = autoMeasures.getMaxValue(channelData[autoMeasureOptions.associatedChannel].points);
        }
        valuesToDisplay.push({
            text: "Max", 
            color: channelData[autoMeasureOptions.associatedChannel].colorDark, 
            value: max.toFixed(1) + "mV",
            id: "max-measure",
        });
    };

    if (autoMeasureOptions.mean.set){// get avg value
        let mean = autoMeasures.getMeanValue(channelData[autoMeasureOptions.associatedChannel].points);
        valuesToDisplay.push({
            text: "Avg", 
            color: channelData[autoMeasureOptions.associatedChannel].colorDark, 
            value: mean.toFixed(1) + "mV",
            id: "avg-measure",
        });
    };

    if (autoMeasureOptions.mid.set){// get middle value
        let middle = autoMeasures.getMiddleValue(channelData[autoMeasureOptions.associatedChannel].points);
        valuesToDisplay.push({
            text: "Mid", 
            color: channelData[autoMeasureOptions.associatedChannel].colorDark, 
            value: middle.toFixed(1) + "mV",
            id: "mid-measure",
        });
    };

    if (autoMeasureOptions.rms.set){// get rms value
        let rms = autoMeasures.getRMS(channelData[autoMeasureOptions.associatedChannel].points);
        valuesToDisplay.push({
            text: "RMS", 
            color: channelData[autoMeasureOptions.associatedChannel].colorDark, 
            value: rms.toFixed(1) + "mV",
            id: "rms-measure",
        });
    };

    function formatFrequency(frequency) {
        if (frequency >= 1e9) {  // gte to 1 Gigahertz
            return (frequency / 1e9).toFixed(1) + " GHz";
        } else if (frequency >= 1e6) {  // gte to 1 Megahertz
            return (frequency / 1e6).toFixed(1) + " MHz";
        } else if (frequency >= 1e3) {  // gte to 1 Kilohertz
            return (frequency / 1e3).toFixed(1) + " kHz";
        } else {
            return frequency.toFixed(1) + " Hz";  // lt 1 Kilohertz, display in Hertz (Very very very unlikely but still, we never know)
        }
    }

    if (autoMeasureOptions.freq.set){// get avg frequency
        let freq = autoMeasures.getAverageFrequency(channelData[autoMeasureOptions.associatedChannel].points, config.frequency);
        valuesToDisplay.push({
            text: "Freq", 
            color: channelData[autoMeasureOptions.associatedChannel].colorDark, 
            value: formatFrequency(freq),
            id: "avg-freq-measure",
        });
    };

    if (autoMeasureOptions.highFreq.set || autoMeasureOptions.lowFreq.set){// get min and max frequencies
        let freqs = autoMeasures.getFrequenciesMaxMin(channelData[autoMeasureOptions.associatedChannel].points, config.frequency);
        if (autoMeasureOptions.highFreq.set){// high freq
            valuesToDisplay.push({
                text: "Max Freq", 
                color: channelData[autoMeasureOptions.associatedChannel].colorDark, 
                value: formatFrequency(freqs.highestFrequency),
                id: "max-freq-measure",
            });
        };

        if (autoMeasureOptions.lowFreq.set){//low freq
            valuesToDisplay.push({
                text: "Min Freq", 
                color: channelData[autoMeasureOptions.associatedChannel].colorDark, 
                value: formatFrequency(freqs.lowestFrequency),
                id: "min-freq-measure",
            });
        };
    };

    //now we inject the calculated values to the html page.
    const valuesContainer = document.getElementById('auto-measures-display');
    valuesContainer.querySelectorAll('p').forEach(p => p.style.display = 'none');
    for (let i = 0; i < valuesToDisplay.length; i++) {
        let paragraph = document.getElementById(valuesToDisplay[i].id);
        paragraph.style.display = 'block';
        paragraph.style.color = valuesToDisplay[i].color;
        paragraph.style.borderColor = valuesToDisplay[i].color;
        paragraph.textContent = valuesToDisplay[i].text + " : " + valuesToDisplay[i].value;
    };
};

/**
 * This function is responsible for generating the points array for the signals not received directly but generated by the user.
 * This goes for all maths functions like +, -, fft, integral, etc.
 * Each time this function is called for a certain channel, we check the type of operation to execute, generate the points and return immediately.
 * The drawSignal function will then draw the signal like any other.
 * 
 * @function generatePoints
 * @memberof module:Computing
 * @param {string} channelKey Key to access this signal within 'channelData' (CH1, CH2, etc.).
 * @returns {void}
 */
function generatePoints(channelKey){
    const originChannel1 = channelData[channelKey].originChannel1;
    const originChannel2 = channelData[channelKey].originChannel2;
    const operation = channelData[channelKey].operation;

    if (operation == "squared"){
        const pointsSquared = channelData[originChannel1].points.map(point => {
            const voltage = autoMeasures.voltage_from_raw(point);
            const squaredVoltage = voltage * voltage;
            return parseInt(mapVoltageToRaw(squaredVoltage).toFixed(0));
        });

        channelData[channelKey].points = pointsSquared;
        return
    };

    if (operation == "deriv"){
        function getDerivative(points) {
            let derivative = [];
            for (let i = 0; i < points.length - 1; i++) {
                difference = points[i + 1] - points[i];
                //Since the derivative show the difference between two points on the graph we need to add the equivalent of 1/2 the height of the graph 
                //so that it doesn't start being drawn at the bottom of the screen.
                derivative[i] = parseInt((difference + (config.maxSampleValue / 2)).toFixed(0));
            }
            return derivative;
        };
        
        channelData[channelKey].points = getDerivative(channelData[originChannel1].points);
        return
    }

    if (operation == "integral"){
    
        function integrateSignal(points, deltaT) {
            const integratedSignal = [];
            let integralSum = 0;
            for (let i = 0; i < points.length; i++) {
                const pointV = mapRawToVoltage(points[i]);
                integralSum += pointV * deltaT;
                integratedSignal.push(mapVoltageToRaw(integralSum));
            }
            return integratedSignal;
        }
        channelData[channelKey].points = integrateSignal(channelData[originChannel1].points, 1);
        return
    };

    if (operation == "fft") {
        function fillArrayToNextPowerOfTwo(array) {
            // check if the length of the array is already a power of two
            function isPowerOfTwo(n) {
                return n && (n & (n - 1)) === 0;
            }
        
            let targetLength = array.length;
            if (!isPowerOfTwo(targetLength)) {
                // if not a power of two, find the next power of two
                targetLength = Math.pow(2, Math.ceil(Math.log2(targetLength)));
                // fill the array with zeros until it reaches the target length
                while (array.length < targetLength) {
                    array.push(0);
                }
            }
        
            return array;
        }

        function calculateFFT(points, sampleRate) {
            //===============================================================
            // FFT calculation - credit : https://github.com/indutny/fft.js/
            //===============================================================
            const fft = new FFT(points.length);
        
            const complexInput = fft.toComplexArray(points);
            const complexOutput = fft.createComplexArray();
        
            fft.realTransform(complexOutput, complexInput);
            fft.completeSpectrum(complexOutput);
        
            const magnitudes = complexOutput.map((value, index) => {
                if (index % 2 === 0) {
                    // even index, real part
                    const re = value;
                    // odd index, imaginary part (since the array is [re, im, re, im, ...]) => see fft.js doc
                    const im = complexOutput[index + 1];
                    return Math.sqrt(re * re + im * im);
                }
                return null;
            })

            const displayData = magnitudes.map((magnitude, index) => {
                const frequency = index * sampleRate / points.length;
                return { frequency: frequency, magnitude: magnitude };
            });
        
            const halfDisplayData = displayData.slice(0, displayData.length / 2);
            return halfDisplayData;
        }
        //we have to fill the gaps in the array until the next power of 2 (required by the library)
        let tempOriginChannel = channelData[originChannel1];
        channelData[channelKey].points = fillArrayToNextPowerOfTwo(tempOriginChannel.points);
        channelData[channelKey].points = calculateFFT(channelData[channelKey].points, config.frequency);

        channelData[channelKey].verticalScale = 2.5; // We set the vertical scale to 2.5 to make the fft thinner than other signals. This can still be modified by the user oc.
        return
    };

    if (operation == "add"){
        const points1 = channelData[originChannel1].points;
        const points2 = channelData[originChannel2].points;
        const pointsAdded = points1.map((point, index) => {
            point = autoMeasures.voltage_from_raw(point);
            let point2 = autoMeasures.voltage_from_raw(points2[index]);
            return parseInt(mapVoltageToRaw(point + point2).toFixed(0));
        });
        channelData[channelKey].points = pointsAdded;
    };

    if (operation == "mult"){
        const points1 = channelData[originChannel1].points;
        const points2 = channelData[originChannel2].points;
        const pointsMultiplied = points1.map((point, index) => {
            point = autoMeasures.voltage_from_raw(point);
            let point2 = autoMeasures.voltage_from_raw(points2[index]);
            return parseInt(mapVoltageToRaw(point * point2).toFixed(0));
        });
        channelData[channelKey].points = pointsMultiplied;
    };

    if (operation == "sub"){
        const points1 = channelData[originChannel1].points;
        const points2 = channelData[originChannel2].points;
        const pointsSubtracted = points1.map((point, index) => {
            point = autoMeasures.voltage_from_raw(point);
            let point2 = autoMeasures.voltage_from_raw(points2[index]);
            return parseInt(mapVoltageToRaw(point - point2).toFixed(0));
        });
        channelData[channelKey].points = pointsSubtracted;
    };

    if (operation == "div"){
        const points1 = channelData[originChannel1].points;
        const points2 = channelData[originChannel2].points;
        const pointsDivided = points1.map((point, index) => {
            point = autoMeasures.voltage_from_raw(point);
            let point2 = autoMeasures.voltage_from_raw(points2[index]);
            if (point2 === 0){
                return 8192;// = 0V 
            }else{
                return parseInt(mapVoltageToRaw(point / point2).toFixed(0));
            }
        });
        channelData[channelKey].points = pointsDivided;
    };
};

/**
 * This function regroups all sub-functions needed to calculate the values we display below the oscilloscope's screen.
 * These sub-functions won't be entirely documented, most of them are pretty self-explanatory and comments are there already to clarify their use.
 * 
 * @function calculateAutoMeasures
 * @memberof module:Computing
 * @returns {functions} Returns the following functions to use : 
 *  - **voltage_from_raw**
 *  - **getMinValue**
 *  - **getMaxValue**
 *  - **getVppValue**
 *  - **getMeanValue**
 *  - **getMiddleValue**
 *  - **getRMS**
 *  - **getAverageFrequency**
 *  - **calculateThreshold**
 *  - **getFrequenciesMaxMin**
 * 
 * @example
 * const measures = calculateAutoMeasures();
 * let minimumValue = measures.getMinValue(channelData["CH2"].points);
 */
function calculateAutoMeasures(){
    //converts an absolute value (from 0 to 16383) to the corresponding voltage value (-1.1 to +1.1v or else if the config changes).
    function voltage_from_raw(raw_value, max_raw=config.maxSampleValue){
        let voltage_range = [-(config.voltage/2), config.voltage/2]
        let span = voltage_range[1] - voltage_range[0]
        normalized = raw_value / max_raw
        return voltage_range[0] + (normalized * span)
    };

    function getMinValue(points){
        let minAbsValue = Math.min(...points); // get smallest value from array
        minAbsValue = voltage_from_raw(minAbsValue); //convert abs value to V
        return minAbsValue * 1000;//convert V to mV
    }

    function getMaxValue(points){
        let maxAbsValue = Math.max(...points); // get biggest value from array
        maxAbsValue = voltage_from_raw(maxAbsValue); //convert abs value to V
        return maxAbsValue * 1000;//convert V to mV
    }

    function getVppValue(points){
        const maxVoltage = getMinValue(points);//mV
        const minVoltage = getMaxValue(points);//mV
        const vpp = maxVoltage - minVoltage;//mV
        return vpp;
    }

    function getMeanValue(points){
        const average = getMedian(points);//Get the average abs value of the array
        const averageInVolts = voltage_from_raw(average); //convert abs value to V
        return averageInVolts * 1000;//convert V to mV
    }

    function getMiddleValue(points){
        let middleAbs = Math.round(points.length / 2); //get the middle of the point array
        middleInVolts = voltage_from_raw(points[middleAbs]);//convert the value in volts
        return middleInVolts * 1000; //convert V to mV
    }

    function getRMS(points){
        let sum = 0;
        points.forEach(point => {//add each point squared to the total sum
            sum += point * point;
        });
        const rmsAbs = Math.sqrt(sum / points.length);//take the square root of the average of the points
        const rmsInVolts = voltage_from_raw(rmsAbs);//convert the value in volts
        return rmsInVolts * 1000;//convert final value to mV
    }

    function getAverageFrequency(points, sampleRate) {
        const threshold = calculateThreshold(points); // Define a suitable threshold
        let cycleCount = 0;
        let isInCycle = false;
    
        for (let i = 0; i < points.length; i++) {
            if (points[i] > threshold && !isInCycle) {
                cycleCount++;  // start of a new cycle
                isInCycle = true;
            } else if (points[i] < threshold && isInCycle) {
                isInCycle = false;  // end of a cycle
            }
        }
        const totalDuration = points.length / sampleRate;  // total duration in seconds (sample rate will be at 1e8 Hz since agata has a sampling period of 10ns)
        return cycleCount / totalDuration;  // average frequency in Hz
    }
    
    function calculateThreshold(points) {
        let mean = points.reduce((acc, val) => acc + val, 0) / points.length;
        let sumOfSquares = points.reduce((acc, val) => acc + (val - mean) ** 2, 0);
        let standardDeviation = Math.sqrt(sumOfSquares / points.length);
        return mean + standardDeviation;  // Threshold at mean + 1 SD
    }

    function getFrequenciesMaxMin(points, sampleRate) {
        const threshold = calculateThreshold(points);
        let cycleStart = null;
        let frequencies = [];
    
        for (let i = 0; i < points.length; i++) {
            if (points[i] > threshold && cycleStart === null) {
                cycleStart = i;  //new cycle
            } else if ((points[i] < threshold || i === points.length - 1) && cycleStart !== null) {
                const cycleEnd = i;
                const cycleDuration = (cycleEnd - cycleStart) / sampleRate;  // one cycle in seconds
                if (cycleDuration > 0) {  // avoid division by zero
                    const frequency = 1 / cycleDuration;
                    frequencies.push(frequency);
                }
                cycleStart = null;  // look for new cycle in the next loop
            }
        }
    
        if (frequencies.length === 0) {
            return {lowestFrequency: 0, highestFrequency: 0};  // no cycles found
        }
    
        const lowestFrequency = Math.min(...frequencies);
        const highestFrequency = Math.max(...frequencies);
    
        return {
            lowestFrequency: lowestFrequency,
            highestFrequency: highestFrequency
        };
    }

    return {getMinValue, getMaxValue, getVppValue, getMeanValue, getMiddleValue, getRMS, getAverageFrequency, getFrequenciesMaxMin, voltage_from_raw};
};

/**
 * This function returns how many mV are within a single vertical division.
 * 
 * @function getMilliVoltsPerDiv
 * @memberof module:Computing
 * @param {number} channelVerticalScale Vertical scale of a specific channel we want to know the mv/div for.
 * @returns {number} Returns the mv/div for the given channel.
 */
function getMilliVoltsPerDiv(channelVerticalScale) {
    const totalVoltageRange = config.voltage;
    const verticalDivisions = config.verticalDivisions;

    const voltsPerDivision = totalVoltageRange / (verticalDivisions * channelVerticalScale); 

    return (voltsPerDivision * 1000).toFixed(1);
};

/**
 * This function is called whenever a user selects a new signal to generate via a math function.
 * We start by creating a new slot for a channel within channelData.
 * We then update the config and the UI components related to this channel (button, scroller).
 * We finish by setting up the interaction for the user with the offset cursor.
 * The new signal will then be generated and drawned within the main loop.
 * 
 * @function updateGeneratedMathSignalsData
 * @memberof module:Computing
 * @param {string} slotChannel Empty slot to use for the new signal.
 * @param {string} channel1 Base signal for the generated new one.
 * @param {string} channel2 Second base signal in case the operation needs two operands.
 * @param {string} operation Which operation to use for the new signal.
 * @returns {void}
 * @example
 * updateGeneratedMathSignalsData("CH6", "CH2", null, "squared");
 * updateGeneratedMathSignalsData("CH8", "CH4", "CH1", "add");
 */
function updateGeneratedMathSignalsData(slotChannel, channel1, channel2, operation){
    //console.log(`Updating math signal in slot ${slotChannel} for the operation : ${operation} between ${channel1} and ${channel2}`);

    channelData[slotChannel] = {
        points: [],
        display: true,
        type: "generatedData",
        focused: false,
        colorDark: channelsMetaData[slotChannel].colorDark,
        colorLight: channelsMetaData[slotChannel].colorLight,
        verticalOffset: 0,
        verticalScale: 1,
        verticalOffsetRelativeCursorPosition: 395,
        //these attributes are specific to generated signals from a function
        originChannel1: channel1,
        originChannel2: channel2,//none if operation does not require 2 channels
        operation: operation,
    };

    //Here we set the button styles to make it show up as available
    channelButton = document.getElementById(slotChannel);

    channelButton.classList.remove("channel-not-displayed");
    channelButton.classList.add("channel-displayed");
    channelButton.classList.add(channelData[slotChannel].colorDark);

    //we update the global config to reflect the new channel
    config.numChannels += 1;

    //we add a cursor for this newly created channel
    const scroller = document.getElementById("scroller-" + slotChannel);
    scroller.style.display = "block";
    scroller.style.backgroundColor = channelData[slotChannel].colorDark;
    const channelID = slotChannel.split("CH")[1];
    setScrollersEvents(channelID);
};

/**
 * This function changes the status for the auto-measures, which ones to display or not.
 * The values being changed are all stored within the global object 'autoMeasureOptions'.
 * 
 * @function toggleMeasurement
 * @memberof module:Computing
 * @param {string} measureKey Key within the 'autoMeasureOptions' object representing the measure.
 * @param {string} buttonId Id of the button associated to this measure.
 * @returns {void}
 */
function toggleMeasurement(measureKey, buttonId) {
    let measure = autoMeasureOptions[measureKey];
    if (measure) {
        measure.set = !measure.set; // Toggle the 'set' value
        //change button class depending on wether the measure is set or not
        document.getElementById(buttonId).className = measure.set ? 'modal-measure-button active-measure-button' : 'modal-measure-button inactive-measure-button';
    }
};

/**
 * This function resets all values within the object 'autoMeasureOptions'.
 * It also resets the visual aspect of each button for the user to select the auto-measures.
 * 
 * @function resetMeasurements
 * @memberof module:Computing
 * @returns {void}
 */
function resetMeasurements(){
    try{
        autoMeasureOptions.associatedChannel = "CH1";
        Object.keys(autoMeasureOptions).forEach(key => {
            if (key !== "associatedChannel") {
                autoMeasureOptions[key].set = false;
                autoMeasureOptions[key].value = null;
                document.querySelectorAll('.modal-measure-button').forEach(button => {
                    button.className = 'modal-measure-button inactive-measure-button';
                });
            }
        });
        document.getElementById("selectChannel").value = "CH1";
    
        showToast("Auto Measurements values reset !", "toast-info");
    } catch (error) {
        console.error('Failed to reset measurements', error);
        showToast("Error while resetting the measurements..", "toast-error");
    }
};

/**
 * This function is used to get the position to set the trigger cursor at relative to the threshold set by the user.
 * 
 * @function getPositionRelativeToTriggerCursor
 * @memberof module:Computing
 * @param {number} milliVolts Current treshold set for the trigger in mv.
 * @param {string} channelKey Object key corresponding to a certain signal saved within 'channelData'.
 * @returns {number} Position, in pixels, of the trigger cursor along the scrollbar.
 */
function getPositionRelativeToTriggerCursor(milliVolts, channelKey) {
    const sizeOfOneDivisionInPixels = CANVAS.height / config.verticalDivisions;
    const milliVoltsPerDivision = getMilliVoltsPerDiv(channelData[channelKey].verticalScale);
    let position;
    if (milliVolts === 0) {
        position = CANVAS.height / 2;
    } else if (milliVolts < 0) {
        const cursorValue = Math.abs(milliVolts) / milliVoltsPerDivision;
        position = (CANVAS.height / 2) + (cursorValue * sizeOfOneDivisionInPixels);
    } else if (milliVolts > 0) {
        const cursorValue = milliVolts / milliVoltsPerDivision;
        position = (CANVAS.height / 2) - (cursorValue * sizeOfOneDivisionInPixels);
    }

    return position;
};

/**
 * This function is used to update the current trigger settings set by the user.
 * We had to add a few verifications in case the values entered by the user are outside the acceptable range.
 * 
 * @function updateTriggerSettings
 * @memberof module:Computing
 * @param {DOMElement} modalElement Modal containing the inputs relative to the trigger options.
 * @returns {void}
 */
function updateTriggerSettings(modalElement){
    triggerOptions.isTriggerOn = modalElement.querySelector("#selectOnOffStatus").value;
    triggerOptions.triggerMode = modalElement.querySelector("#selectTriggerMode").value;
    triggerOptions.triggerChannel = modalElement.querySelector("#selectTriggerChannel").value;
    if (modalElement.querySelector("#TriggerLevelInput").value > 1100){
        triggerOptions.triggerLevel = 1100
    }else if(modalElement.querySelector("#TriggerLevelInput").value < -1100){
        triggerOptions.triggerLevel = -1100
    }else{
        triggerOptions.triggerLevel = modalElement.querySelector("#TriggerLevelInput").value;
    }
    if (modalElement.querySelector("#WindowLevelMinInput").value > 1100){
        triggerOptions.windowLevelMin = 1100
    }else if(modalElement.querySelector("#WindowLevelMinInput").value < -1100){
        triggerOptions.windowLevelMin = -1100
    }else{
        triggerOptions.windowLevelMin = modalElement.querySelector("#WindowLevelMinInput").value;
    }
    if (modalElement.querySelector("#WindowLevelMaxInput").value > 1100){
        triggerOptions.windowLevelMax = 1100
    }else if(modalElement.querySelector("#WindowLevelMaxInput").value < -1100){
        triggerOptions.windowLevelMax = -1100
    }else{
        triggerOptions.windowLevelMax = modalElement.querySelector("#WindowLevelMaxInput").value;
    }
    triggerOptions.triggerSlope = modalElement.querySelector("#selectTriggerSlope").value;
    if (modalElement.querySelector("#holdOffInput").value > 3600){
        triggerOptions.holdOff = 3600
    }else if(modalElement.querySelector("#holdOffInput").value < 0){
        triggerOptions.holdOff = 0
    }else{
        triggerOptions.holdOff = modalElement.querySelector("#holdOffInput").value;
    }

    showToast("Trigger settings updated !", "toast-info");

    if (triggerOptions.isTriggerOn == "on"){
        document.getElementById("trigger-cursor").style.display = "block";

        const newCursorPosition = getPositionRelativeToTriggerCursor(parseFloat(triggerOptions.triggerLevel), triggerOptions.triggerChannel);
        document.getElementById("trigger-cursor").style.top = newCursorPosition + 'px';
    }else{
        document.getElementById("trigger-cursor").style.display = "none";
    }
};

/**
 * This function calculates the zoom factors when a user selects an area to zoom on.
 * It also update the global object 'zoomConfig' with the new values for the current zoom if any.
 * 
 * @function calculateZoomFactors
 * @memberof module:Computing
 * @returns {void}
 */
function calculateZoomFactors() {
    const selectedWidth = zoomConfig.finalX - zoomConfig.initX;
    const selectedHeight = zoomConfig.finalY - zoomConfig.initY;

    // Calculate scale factors to fit the selected area to the canvas size
    zoomConfig.zoomX = CANVAS.width / selectedWidth;
    zoomConfig.zoomY = CANVAS.height / selectedHeight;
};