DebugConfig({
    //all:                true,
    generate_table:     0,
    SI:                 1,
    SI_set_options:     1,
    SI_set_precision:   0,
    timingUIChange:     1,
});


CTA861 = {};


// The intepreted current state of all input fields is stored here
Global_InputVars = {
    'HRES': '', // Int
    'VRES': '', // Int
    'FREQ': '', // Float
    'COLOR_DEPTH': '',
    'PIXEL_FORMAT': '',
    'COMP': '',
    'SCAN': '',
    'MARGINS': '',
    'TIMING_STD': '',
    'V_FP': '',
    'V_BP': '',
    'V_SW': '',
    'H_FP': '',
    'H_BP': '',
    'H_SW': '',
}



Encoding_Overhead = {
    '8b/10b': 1.25,
    '16b/18b': 1.125,
};

Interface = [
    {
        name:       "DisplayPort 1.3&ndash;1.4",
        encoding:   "8b/10b",
        datarate:   25.92 * (10 ** 9),
    },{
        name:       "DisplayPort 1.2",
        encoding:   "8b/10b",
        datarate:   17.28 * (10 ** 9),
    },{
        name:       "DisplayPort 1.0&ndash;1.1",
        encoding:   "8b/10b",
        datarate:   8.64 * (10 ** 9),
    },{
        name:       "HDMI 2.1",
        encoding:   "16b/18b",
        datarate:   48 * (10 ** 9) * (16/18),
    },{
        name:       "HDMI 2.0",
        encoding:   "8b/10b",
        datarate:   14.4 * (10 ** 9),
    },{
        name:       "HDMI 1.3&ndash;1.4",
        encoding:   "8b/10b",
        datarate:   8.16 * (10 ** 9),
    },{
        name:       "HDMI 1.0&ndash;1.2",
        encoding:   "8b/10b",
        datarate:   3.96 * (10 ** 9),
    },{
        name:       "Dual-Link DVI",
        encoding:   "8b/10b",
        datarate:   7.92 * (10 ** 9),
    },{
        name:       "Single-Link DVI",
        encoding:   "8b/10b",
        datarate:   3.96 * (10 ** 9),
    },{
        name:       "Thunderbolt 3 (Gen 2)",
        encoding:   "unknown",
        datarate:   40 * (10 ** 9),
    },{
        name:       "Thunderbolt 3 (Gen 1)",
        encoding:   "unknown",
        datarate:   34.56 * (10 ** 9),
    },{
        name:       "Thunderbolt 2",
        encoding:   "unknown",
        datarate:   17.28 * (10 ** 9),
    },{
        name:       "Thunderbolt",
        encoding:   "unknown",
        datarate:   8.64 * (10 ** 9),
    },
];



function submitVar(id, val) {
    DEBUG(id, val);
    var len = 0;
    var GlobalVars_Key = '';

    if (typeof(val) === 'number') { val = val.toString(); }

    if (id == 'INPUT_HRES' || id == 'INPUT_VRES') {
        if (id == 'INPUT_HRES') GlobalVars_Key = 'HRES';
        else if (id == 'INPUT_VRES') GlobalVars_Key = 'VRES';
        val = parseNum(val);
        if (isNum(val) == true) {
            val = Math.abs(parseInt(val));
            Global_InputVars[GlobalVars_Key] = val;
        }
        else {
            DEBUG(id + ' input is not a number:', val);
            Global_InputVars[GlobalVars_Key] = '';
        }
        DEBUG("Global_InputVars['" + GlobalVars_Key + "'] was set to", Global_InputVars[GlobalVars_Key]);
    }

    else if (id == 'INPUT_F') {
        GlobalVars_Key = 'FREQ';
        val = parseNum(val);
        if (isNum(val) == true) {
            Global_InputVars[GlobalVars_Key] = Math.abs(val);
        }
        else {
            DEBUG(id + ' input is not a number:', val);
            Global_InputVars[GlobalVars_Key] = '';
        }
        DEBUG("Global_InputVars['" + GlobalVars_Key + "'] was set to", Global_InputVars[GlobalVars_Key]);
    }

    else if (id == 'COLOR_DEPTH_FORM') {
        // Value is either the color depth number (in bits per pixel), or the string 'Custom'

        GlobalVars_Key = 'COLOR_DEPTH';
        colordepthUIChange();

        if (val != 'Custom') { Global_InputVars[GlobalVars_Key] = val; }
        else if (val == 'Custom') { // Custom color depth
            // If Custom, grab the number from the custom color depth input field
            val = parseInt(parseNum($('#CUSTOM_COLOR_DEPTH').val()));
            if (isNum(val) == true) {
                if ($('input[name=CD_UNIT_SLCT]:checked').val() == 'bpc') {
                    val = val * 3; // If user enters values in units of bpc, then multiply by 3 to convert to bit/px (bpp)
                }
                Global_InputVars[GlobalVars_Key] = Math.abs(val);
            }
            else {
                DEBUG(id + ' input is not a number:', val);
                Global_InputVars[GlobalVars_Key] = '';
            }

        }
        DEBUG("Global_InputVars['" + GlobalVars_Key + "'] was set to", Global_InputVars[GlobalVars_Key]);
    }

    else if (id == 'PIXEL_FORMAT_FORM') {
        GlobalVars_Key = 'PIXEL_FORMAT';
        if (val == 'RGB' || val == 'YCBCR 4:4:4') { val = 1.0; }
        else if (val == 'YCBCR 4:2:2') { val = 1.5; }
        else if (val == 'YCBCR 4:2:0') { val = 2.0; }
        Global_InputVars[GlobalVars_Key] = val;
        DEBUG("Global_InputVars['" + GlobalVars_Key + "'] was set to", Global_InputVars[GlobalVars_Key]);
    }

    else if (id == 'COMPRESSION_FORM') {
        GlobalVars_Key = 'COMP';
        if (val == 'Uncompressed') { val = 1.0; }
        else if (val == 'DSC 2.0x') { val = 2.0; }
        else if (val == 'DSC 2.5x') { val = 2.5; }
        else if (val == 'DSC 3.0x') { val = 3.0; }
        Global_InputVars[GlobalVars_Key] = val;
        DEBUG("Global_InputVars['" + GlobalVars_Key + "'] was set to", Global_InputVars[GlobalVars_Key]);
    }

    else if (id == 'SCAN_FORM') {
        GlobalVars_Key = 'SCAN';
        if (val == 'p') { val = 1; }
        else if (val == 'i') { val = 2; }
        Global_InputVars[GlobalVars_Key] = val;
        DEBUG("Global_InputVars['" + GlobalVars_Key + "'] was set to", Global_InputVars[GlobalVars_Key]);
        timingUIChange();
        DEBUG('Set interlaced timing row display to:', $('#Interlaced_Timing_Row').css('display'));
    }

    else if (id == 'MARGINS_FORM') {
        // val is either 'y' or 'n'
        GlobalVars_Key = 'MARGINS';
        marginsUIChange();

        if (val == 'n') {
            Global_InputVars[GlobalVars_Key] = 0;
        }
        else if (val == 'y') {
            val = parseFloat(parseNum($('#CUSTOM_MARGINS').val()));
            if (isNum(val) == true) {
                Global_InputVars[GlobalVars_Key] = Math.abs(val);
            }
            else {
                DEBUG(id + ' input is not a number:', val);
                Global_InputVars[GlobalVars_Key] = '';
            }
        }
        else { DEBUG('invalid input combination. id/val = ', id, val) } // If val is not 'y' or 'n', then something somewhere has gone terribly wrong
        DEBUG("Global_InputVars['" + GlobalVars_Key + "'] was set to", Global_InputVars[GlobalVars_Key]);
    }

    else if (id == 'TIMING_DROP') {
        GlobalVars_Key = 'TIMING_STD';

        Global_InputVars[GlobalVars_Key] = val;
        DEBUG("Global_InputVars['" + GlobalVars_Key + "'] was set to", Global_InputVars[GlobalVars_Key]);

        timingUIChange();
    }

    else if (id == 'V_FP' || id == 'V_BP' || id == 'V_SW' ||
        id == 'H_FP' || id == 'H_BP' || id == 'H_SW' ||
        id == 'V_FP_ODD' || id == 'V_SW_ODD' || id == 'V_BP_ODD') {
        GlobalVars_Key = id;
        val = parseFloat(parseNum(val));
        if (isNum(val)) {
            val = Math.abs(val);
            Global_InputVars[id] = val;
            $('#' + id).val(val);
            $('#V_BLANK').html(Global_InputVars['V_FP'] + Global_InputVars['V_BP'] + Global_InputVars['V_SW']);
            $('#H_BLANK').html(Global_InputVars['H_FP'] + Global_InputVars['H_BP'] + Global_InputVars['H_SW']);
            $('#V_BLANK_ODD').html(Global_InputVars['V_FP_ODD'] + Global_InputVars['V_BP_ODD'] + Global_InputVars['V_SW_ODD']);
        }
        else {
            DEBUG(id + ' input is not a number:', val);
            Global_InputVars[id] = '';
            $('#' + id).val('');
            $('#V_BLANK').html('');
            $('#H_BLANK').html('');
            $('#V_BLANK_ODD').html('');
        }
        DEBUG("Global_InputVars['" + GlobalVars_Key + "'] was set to", Global_InputVars[GlobalVars_Key]);

        // Update value displayed in the UI input field

        // Update total Vblank and Hblank numbers on the UI
    }
}


function generate_table(type, input_datarate) {
    if (type == "Interface Support") {
        table = $("#interface_support_table");
        contents = '';
        contents += ('<tr><th class="title" colspan="6">Interface Support</th></tr>');
        contents += ('<tr>');
        contents += ('<th rowspan="2">% Usage</th>');
        contents += ('<th rowspan="2">Interface</th>');
        contents += ('<th rowspan="2">Encoding</th>');
        contents += ('<th colspan="3" style="border-bottom:1px solid #FFFFFF;">Maximum</th></tr>');
        contents += ('<tr><th>Pixel Clock</th><th>Datarate</th><th>Bandwidth</th></tr>');

        var name;
        var encoding;
        var datarate;
        var pixel_clock;
        var bandwidth;
        var overhead;
        var saturation;

        for (var x = 0; x < Interface.length; x++) {
            name = Interface[x]['name'];
            encoding = Interface[x]['encoding'];
            if (encoding == 'unknown') {
                datarate = Interface[x]['datarate'];
                bandwidth = '?';
                encoding = '?';
            }
            else {
                overhead = Encoding_Overhead[encoding];
                datarate = Interface[x]['datarate'];
                bandwidth = datarate * overhead;
            }
            pixel_clock = datarate / 24;
            saturation = input_datarate / datarate;

            if (input_datarate == -1) {
                contents += ('<tr><td></td>');
            }
            else {
                contents += ('<tr><td>' + saturation.toFixed(1) + '%</td>');
            }

            contents += ('<td style="text-align:left; white-space:nowrap;">' + name + '</td><td>' + encoding + '</td><td>' + SI(pixel_clock, 'Hz', {p: ['M0', 'G3']}) + '</td><td>' + SI(datarate, 'bit/s', 2) + '</td><td>' + SI(bandwidth, 'bit/s', 2) + '</td></tr>');
        }

        table.html(contents);

        return;
    }

    else if (type == "Maximum Refresh Frequency") {
        return;
    }


    return;
}


function input_ok() {
    // This checks to make sure all the necessary fields are filled in, and their contents are valid, before allowing the Calculation function to proceed.
    var val;

    var checklist = ['HRES', 'VRES', 'FREQ'];
    for (var i = 0; i < checklist.length; i++) {
        val = Global_InputVars[checklist[i]];
        if (!(isPositive(val))) {
            DEBUG('Calculation aborted. ' + checklist[i] + ' contains an empty or non-numeric string.', val);
            return false;
        }
    }

    if ($('input[name=COLOR_DEPTH_SLCT]:checked').val() == 'Custom') {
        val = Global_InputVars['COLOR_DEPTH'];
        if (!(isPositive(val))) {
            DEBUG('Calculation aborted. COLOR_DEPTH_SLCT is set to Custom, but CUSTOM_COLOR_DEPTH contains an empty or non-numeric string, or 0.', val);
            return false;
        }
    }

    if (Global_InputVars['TIMING_STD'] == 'Custom') {
        checklist = ['V_FP', 'V_BP', 'V_SW', 'H_FP', 'H_BP', 'H_SW'];
        var abort = false;
        for (var i = 0; i < checklist.length; i++) {
            val = Global_InputVars[checklist[i]];
            if (!(isPositiveZ(val))) {
                abort = true;
            }
        }
        if (abort == true) {
            DEBUG('Calculation aborted. TIMING_DROP is set to Custom, but one or more timing parameter fields contains an empty or non-numeric string.', Global_InputVars);
            return false;
        }
    }

    if ($('input[name=MARGINS_SLCT]:checked').val() == 'y') {
        val = Global_InputVars['MARGINS'];
        if (!(isPositiveZ(val))) {
            DEBUG('Calculation aborted. MARGINS_SLCT is set to Yes, but CUSTOM_MARGINS contains an empty or non-numeric string.', val);
            return false;
        }
    }
    
    return true;
}


function calcMain() {
    if (!input_ok()) { clearResults(); return; }

    var hres = Global_InputVars['HRES'];
    var vres = Global_InputVars['VRES'];
    var freq = Global_InputVars['FREQ'];

    var color_depth = Global_InputVars['COLOR_DEPTH'];
    var px_format = Global_InputVars['PIXEL_FORMAT'];
    var comp = Global_InputVars['COMP'];
    var scan = Global_InputVars['SCAN'];
    var timing_standard = Global_InputVars['TIMING_STD'];

    // Get timing parameters
    Timing = getTiming(timing_standard);
    DEBUG('Timing:', Timing);
    
    var results = {
        hres:       hres,
        vres:       vres,
        freq:       freq,
        px_bits:    color_depth,
        px_format:  px_format,
        comp:       comp,
        scan:       scan,

        Timing:     Timing,

        // Calculate results
        // Resolution with blanking intervals
        h_eff: Timing['H_EFF'],
        v_eff: Timing['V_EFF'],

        // Aspect ratio
        ratio_num: hres / vres,
        ratio_str: (hres / GCD(hres, vres)) + ':' + (vres / GCD(hres, vres)),

        // Total pixel count of image
        px_per_frame: hres * vres,
        px_per_frame_eff: (Timing['H_EFF']) * (Timing['V_EFF']),

        // Size (bits) of one frame
        bits_per_frame: hres * vres * color_depth,
        bits_per_frame_eff: (Timing['H_EFF']) * (Timing['V_EFF']) * color_depth,

        // Pixel clock
        px_per_sec: hres * vres * Timing['F_ACTUAL'],
        px_per_sec_eff: (Timing['H_EFF']) * (Timing['V_EFF']) * Timing['F_ACTUAL'],

        // Raw bit rate
        bits_per_sec_eff: (Timing['H_EFF']) * (Timing['V_EFF']) * color_depth * Timing['F_ACTUAL'],
    };

    DEBUG('Results:', SI(results['bits_per_sec_eff'], 'bit/s', 2), results);
    updateDisplay(results);
}


function getTiming(timing_standard) {
    // Just a traffic control function
    // Actual parameters are calculated in dedicated functions for each standard

    if (!(isPositiveZ(Global_InputVars['HRES']) && isPositiveZ(Global_InputVars['VRES']) && isPositiveZ(Global_InputVars['FREQ']))) {
        DEBUG('Timing calculation aborted, HRES, VRES, or FREQ contains invalid input.', Global_InputVars['HRES'], Global_InputVars['VRES'], Global_InputVars['FREQ']);
        return {
            'V_FP': '',
            'V_BP': '',
            'V_SW': '',
            'H_FP': '',
            'H_BP': '',
            'H_SW': '',

            'V_BL': '',
            'H_BL': '',

            'F_ACTUAL': Global_InputVars['FREQ'],
        };
    }

    if (timing_standard != 'Custom') {
        if (timing_standard == 'CVT-R2') {
            Timing = CVT_R(2);
        }
        else if (timing_standard == 'CVT-RB') {
            Timing = CVT_R(1);
        }
        else if (timing_standard == 'CVT') {
            Timing = CVT();
        }
        else if (timing_standard == 'GTF') {
            Timing = GTF();
        }
        else if (timing_standard == 'DMT') {
            Timing = DMT();
        }
        else if (timing_standard == 'CTA-861') {
            Timing = CTA();
        }
        else if (timing_standard == 'None') {
            Timing = {
                'V_FP': 0,
                'V_BP': 0,
                'V_SW': 0,
                'H_FP': 0,
                'H_BP': 0,
                'H_SW': 0,

                'V_FP_ODD': 0,
                'V_BP_ODD': 0,
                'V_SW_ODD': 0,

                'V_BL': 0,
                'H_BL': 0,

                'F_ACTUAL': Global_InputVars['FREQ'],
            }
        }
        // Update UI timing parameter fields with the newly generated timings
        submitVar('V_FP', Timing['V_FP']);
        submitVar('V_BP', Timing['V_BP']);
        submitVar('V_SW', Timing['V_SW']);
        submitVar('H_FP', Timing['H_FP']);
        submitVar('H_BP', Timing['H_BP']);
        submitVar('H_SW', Timing['H_SW']);
        submitVar('V_FP_ODD', Timing['V_FP_ODD']);
        submitVar('V_SW_ODD', Timing['V_SW_ODD']);
        submitVar('V_BP_ODD', Timing['V_BP_ODD']);
    }

    else if (timing_standard == 'Custom') {
        // Read the timing from the UI
        submitVar('V_FP', $('#V_FP').val());
        submitVar('V_BP', $('#V_BP').val());
        submitVar('V_SW', $('#V_SW').val());
        submitVar('H_FP', $('#H_FP').val());
        submitVar('H_BP', $('#H_BP').val());
        submitVar('H_SW', $('#H_SW').val());
        submitVar('V_FP_ODD', Global_InputVars['V_FP'] + 0.5);
        submitVar('V_SW_ODD', Global_InputVars['V_SW']);
        submitVar('V_BP_ODD', Global_InputVars['V_BP'] + 0.5);

        Timing = {
            'V_FP': Global_InputVars['V_FP'],
            'V_BP': Global_InputVars['V_BP'],
            'V_SW': Global_InputVars['V_SW'],
            'H_FP': Global_InputVars['H_FP'],
            'H_BP': Global_InputVars['H_BP'],
            'H_SW': Global_InputVars['H_SW'],

            'V_FP_ODD': Global_InputVars['V_FP_ODD'],
            'V_BP_ODD': Global_InputVars['V_BP_ODD'],
            'V_SW_ODD': Global_InputVars['V_SW_ODD'],

            'V_BL': Global_InputVars['V_FP'] + Global_InputVars['V_BP'] + Global_InputVars['V_SW'],
            'H_BL': Global_InputVars['H_FP'] + Global_InputVars['H_BP'] + Global_InputVars['H_SW'],

            'F_ACTUAL': Global_InputVars['FREQ'],
        }
    }

    $('#V_BLANK').html(Timing['V_BL']);
    $('#H_BLANK').html(Timing['H_BL']);

    return Timing;
}


function CVT_R(R) { // Variable R is an integer representing the reduced blanking revision to use, version 1 or version 2.
    var H = Global_InputVars['HRES']; // Horizontal active pixels
    var V = Global_InputVars['VRES']; // Vertical active pixels
    var F = Global_InputVars['FREQ']; // Nominal vertical refresh frequency
    var S = Global_InputVars['SCAN']; // 1 for progressive scan, 2 for interlaced
    var M = Global_InputVars['MARGINS']; // Margins (%)

    // Declaring variables for all results
    var V_FP; // Vertical front porch
    var V_SW; // Vertical sync width
    var V_BP; // Vertical back porch

    var H_FP; // Horizontal front porch
    var H_SW; // Horizontal sync width
    var H_BP; // Horizontal back porch

    var V_BLANK; // Total vertical blanking (V_FP + V_SW + V_BP)
    var H_BLANK; // Total horizontal blanking (H_FP + H_SW + H_BP)
    var V_EFF;   // V + V_Blank + V_Margins
    var H_EFF;   // H + H_Blank + H_Margins

    var F_ACTUAL; // Actual vertical refresh frequency (after pixel clock rounding)
    var F_HOR; // Horizontal refresh frequency

    // Common constants
    var V_PER_MIN = 0.00046; /* Minimum vertical blanking period for reduced blank timing (in seconds), defined by VESA CVT 1.2 standard */
    var V_LINES = Math.floor(V / S) // If progressive scan, S = 1 and V_LINES = V. If interlaced, V_LINES = floor(V / 2).

    if (R == 1) {
        // CVT-RB constants
        H_BLANK = 160;
        H_FP = 48;
        H_BP = 80;
        H_SW = 32;
        V_FP = 3;
        var V_BP_MIN = 6;

        // All H timings are defined, as well as V_FP. Only V_SW and V_BP remain (and V_BLANK, the sum of all 3 V parameters)

        // Determine vertical sync width (V_SW) from table of magic numbers defined in VESA CVT standard
        var V_SYNC_TABLE = [
            [4/3,  4],
            [16/9, 5],
            [8/5,  6],
            [5/3,  7],
            [5/4,  7],
        ]
        V_SW = 10; // default value defined in standard
        for (var i = 0; i < V_SYNC_TABLE.length; i++) {
            if ((H / V) - V_SYNC_TABLE[i][0] < 0.05) { // Check if aspect ratio of image (H/V) matches an aspect ratio defined in table, within 0.05
                V_SW = V_SYNC_TABLE[i][1];
            }
        }

        // V_BP is determined in reverse, by calculating V_BLANK first (the sum of V_FP, V_SW, and V_BP) and subtracting out V_FP and V_SW.

        var CellGran = 8; // Cell granularity constant defined by CVT standard
        var H_RND = Math.floor(H / CellGran) * CellGran; // Round down horizontal resolution to be a multiple 8
        var V_MARGIN = Math.floor(M / 100) * V_LINES; // If margins percent (M) is 0, this result is 0
        var H_MARGIN = Math.floor(H_RND * M / 100 / CellGran) * CellGran; // If margins percent (M) is 0, this result is 0

        var H_PER_EST = ((1 / F) - V_PER_MIN) / (V_LINES + (2 * V_MARGIN)); // Horizontal blanking period estimate
        V_BLANK = Math.floor((V_PER_MIN / H_PER_EST) + 1);

        var V_BLANK_MIN = V_FP + V_SW + V_BP_MIN;
        if (V_BLANK < V_BLANK_MIN) { V_BLANK = V_BLANK_MIN; } // Enforce minimum value for V_blank

        V_BP = V_BLANK - (V_FP + V_SW);

        V_EFF = V_LINES + V_BLANK + V_MARGIN + ((S - 1)/2); // (S-1)/2 = 0 for progressive, 0.5 for interlaced
        H_EFF = H_RND + H_BLANK + H_MARGIN;

        // Calculate pixel clock, to enforce pixel clock rounding to the nearest 250 kHz, as required by the CVT standard
        var CLK = F * (V_EFF) * (H_EFF); // Pixel clock (Hz)
        CLK = Math.floor(CLK / 250000) * 250000; // Pixel clock (Hz) rounded down to the next multiple of 0.25 MHz (250 kHz, 250000 Hz)

        F_HOR = CLK / (H_EFF); // Horizontal refresh frequency (Hz)
        F_ACTUAL = F_HOR / (V_EFF); // Pixel clock rounding is enforced via lowering the vertical refresh frequency to adjust pixel clock
    }
    else if (R == 2) {
        // CVT-R2 constants defined by CVT standard
        H_BLANK = 80; 
        H_FP =  8;
        H_BP = 40;
        H_SW = 32;
        V_BP = 6;
        V_SW = 8;
        var V_FP_MIN = 1;

        // All parameters are defined as constant except V_FP
        // V_FP is determined in reverse, by calculating V_BLANK first (the sum of V_FP, V_SW, and V_BP) and subtracting V_SW and V_BP out.

        var V_MARGIN = Math.floor(M / 100) * V_LINES; // If margins percent (M) is 0, this result is 0
        var H_MARGIN = Math.floor(H * M / 100); // If margins percent (M) is 0, this result is 0

        var H_PER_EST = ((1 / F) - V_PER_MIN) / (V_LINES + (2 * V_MARGIN)); // Horizontal blanking period estimate
        V_BLANK = Math.floor((V_PER_MIN / H_PER_EST) + 1);

        var V_BLANK_MIN = V_FP_MIN + V_SW + V_BP;
        if (V_BLANK < V_BLANK_MIN) { V_BLANK = V_BLANK_MIN; } // Enforce minimum value for V_blank

        V_FP = V_BLANK - (V_BP + V_SW);

        V_EFF = V_LINES + V_BLANK + V_MARGIN + ((S - 1)/2); // (S-1)/2 = 0 for progressive, 0.5 for interlaced
        H_EFF = H + H_BLANK + H_MARGIN;
    
        // Calculate pixel clock, to enforce pixel clock rounding to the nearest 1 kHz, as required by the CVT standard
        var CLK = F * (V_EFF) * (H_EFF); // Pixel clock (Hz)
        CLK = Math.floor(CLK / 1000) * 1000; // Pixel clock (Hz) rounded down to the next multiple of 0.001 MHz (1 kHz, 1000 Hz)

        F_HOR = CLK / (H_EFF); // Horizontal refresh frequency (Hz)
        F_ACTUAL = F_HOR / (V_EFF);
    }
    //DEBUG('CLK', CLK);
    //DEBUG('F_HOR', F_HOR);
    //DEBUG('F_ACTUAL', F_ACTUAL);

    //DEBUG(V_BLANK);
    return {
        V_FP: V_FP, // For interlaced, these vertical timing are used for the even fields
        V_BP: V_BP,
        V_SW: V_SW,
        H_FP: H_FP,
        H_BP: H_BP,
        H_SW: H_SW,

        V_FP_ODD: V_FP + 0.5, // For interlaced, V_FP and V_BP are 0.5 higher for odd fields (V_SW is same)
        V_SW_ODD: V_SW,
        V_BP_ODD: V_BP + 0.5,

        V_BL: V_BLANK,
        H_BL: H_BLANK,

        V_EFF: V_EFF,
        H_EFF: H_EFF,

        F_ACTUAL: F_ACTUAL,
    };
}


function CVT() {
    return;
}


function CTA() {
    return;
}


function updateDisplay() {
    return;
}


function clearResults() {
    return;
}

function timingUIChange() {
    // Controls the enabled/disabled state of the custom timing format input fields
    var timing_params = [
        $('#V_FP'),
        $('#V_BP'),
        $('#V_SW'),
        $('#H_FP'),
        $('#H_BP'),
        $('#H_SW'),
    ];
    value = $('#TIMING_DROP').val();
    if (value == 'Custom') {
        for (var i = 0; i < timing_params.length; i++) {
            param = timing_params[i];
            param.prop('disabled', false);
            //if (field.prop('oldvalue') != '') {
            //    field.val(field.prop('oldvalue'));
            //}
        }
    }

    else {
        for (var i = 0; i < timing_params.length; i++) {
            param = timing_params[i];
            param.prop('disabled', true);
            //field.prop('oldvalue', field.val());
            //field.val('');
        }
    }
    value = $('input[name=SCAN_SLCT]:checked').val();
    if (value == 'i') {
        $('#V_BLANK_ODD_LABEL').css('display', 'table-cell');
        $('#V_FP_ODD_CONTAINER').css('display', 'table-cell');
        $('#V_BP_ODD_CONTAINER').css('display', 'table-cell');
        $('#V_SW_ODD_CONTAINER').css('display', 'table-cell');
        $('#V_BLANK_ODD_CONTAINER').css('display', 'table-cell');
        $('#V_BLANK_EVEN_LABEL').html('(Even)&nbsp;V<sub>blank</sub>');
    }
    else if (value == 'p') {
        $('#V_BLANK_ODD_LABEL').css('display', 'none');
        $('#V_FP_ODD_CONTAINER').css('display', 'none');
        $('#V_BP_ODD_CONTAINER').css('display', 'none');
        $('#V_SW_ODD_CONTAINER').css('display', 'none');
        $('#V_BLANK_ODD_CONTAINER').css('display', 'none');
        $('#V_BLANK_EVEN_LABEL').html('V<sub>blank</sub>');
    }
    else {
        DEBUG('Something somewhere has gone terribly wrong. Attemped to grab SCAN_SLCT value, and it was neither "p" nor "i"!');
    }
}


function colordepthUIChange() {
    // Controls the enabled/disabled state of the custom timing format input fields
    var field = $('#CUSTOM_COLOR_DEPTH');
    value = $('input[name=COLOR_DEPTH_SLCT]:checked').val();
    if (value == 'Custom') {
        field.prop('disabled', false);
        $('input[name=CD_UNIT_SLCT]').prop('disabled', false);
    }

    else {
        field.prop('disabled', true);
        $('input[name=CD_UNIT_SLCT]').prop('disabled', true);
    }
}


function marginsUIChange() {
    // Controls the enabled/disabled state of the custom timing format input fields
    var field = $('#CUSTOM_MARGINS');
    value = $('input[name=MARGINS_SLCT]:checked').val();
    if (value == 'y') {
        field.prop('disabled', false);
        if (field.val() == '') {
            field.val('1.8');
        }
    }

    else {
        field.prop('disabled', true);
    }
}


function SI(value, unit, options_input) {
    DEBUG('Input:', value, unit, options_input);
    if (isNaN(parseInt(value))) { return value; }
    if (typeof(options_input) == 'number' || typeof(options_input) == 'string') { options_input = {p: options_input.toString()}; }

    /* Valid options:
    SI_options = {
        p:     2            Specifies default precision (number of decimal places). Use '-1' or 'a' or 'adaptive' for adaptive mode (autodetect decimal places)
                            Also accepts individual decimal place settings for each prefix, in array format; for example:
                                {p: [1, G2, M0]} sets a default of 1 for all prefixes, then specifies 2 decimal places for Giga and 0 for Mega.
        p_max: 4            Maximum number of decimal places (for adaptive precision mode)
        p_min: 2            Minimum number of decimal places (for adaptive precision mode)

        separator: ','      Character to use as thousands separator (default comma)
        decimal: '.'        Character to use as decimal point (default period)
        unit_sep: '&nbsp;'  Character to place between the value and unit symbol (default non-breaking space)
        cap_kilo: 'true'    Use capital K for kilo instead of small k. (default false)
        no_mu: 'true'       Use "u" instead of "µ" on output, if necessary for compatibility reasons (default false)

        exclude: ['c', 'd'] SI prefixes to exclude, for situational use (i.e. for displaying time, one may not wish for "centiseconds").
                            Symbols can also be used as shortcuts, as in the following examples:
                                '>=G' excludes Giga and larger
                                '>G' excludes larger than Giga, but not Giga itself
                                '<G' and "<=G" same as above, but for prefixes smaller than Giga
                                '*' excludes all prefixes (unless protected)
                                '!G' protects a prefix from '*', i.e. {exclude: ['*', '!k', '!M']} excludes all prefixes except Kilo and Mega)
                            Multiple arguments accepted in array format
                            "u" is accepted as an argument for excluding "µ", and "0" is accepted for excluding the prefixless base unit
                            By default, the following are excluded already, and must be un-excluded (using !c, !d, etc.) to be used:
                                'c' (centi, 10^-2)
                                'd' (deci, 10^-1)
                                'D' (deca, 10^1)
                                'H' (hecto, 10^2)
    }
    */


    //console.log(SI_options['precision']);

    var out_value;
    var out_prefix;
    var precision;

    var prefixDef = {
        '-24': {sym:'y', name:'yocto', p: 0, excl: false},
        '-21': {sym:'z', name:'zepto', p: 0, excl: false},
        '-18': {sym:'a', name:'atto',  p: 0, excl: false},
        '-15': {sym:'f', name:'femto', p: 0, excl: false},
        '-12': {sym:'p', name:'pico',  p: 0, excl: false},
        '-9':  {sym:'n', name:'nano',  p: 0, excl: false},
        '-6':  {sym:'µ', name:'micro', p: 0, excl: false},
        '-3':  {sym:'m', name:'milli', p: 0, excl: false},
        '-2':  {sym:'c', name:'centi', p: 0, excl: false},
        '-1':  {sym:'d', name:'deci',  p: 0, excl: false},
        '0':   {sym:'',  name:'',      p: 0, excl: false},
        '1':   {sym:'D', name:'deca',  p: 0, excl: false},
        '2':   {sym:'H', name:'hecto', p: 0, excl: false},
        '3':   {sym:'k', name:'kilo',  p: 0, excl: false},
        '6':   {sym:'M', name:'mega',  p: 0, excl: false},
        '9':   {sym:'G', name:'giga',  p: 0, excl: false},
        '12':  {sym:'T', name:'tera',  p: 0, excl: false},
        '15':  {sym:'P', name:'peta',  p: 0, excl: false},
        '18':  {sym:'E', name:'exa',   p: 0, excl: false},
        '21':  {sym:'Z', name:'zetta', p: 0, excl: false},
        '24':  {sym:'Y', name:'yotta', p: 0, excl: false},
    };

    var pre2num = {
        'y': -24,
        'z': -21,
        'a': -18,
        'f': -15,
        'p': -12,
        'n': -9,
        'µ': -6,
        'u': -6,
        'm': -3,
        'c': -2,
        'd': -1,
        '0': 0,
        'D': 1,
        'H': 2,
        'k': 3,
        'K': 3,
        'M': 6,
        'G': 9,
        'T': 12,
        'P': 15,
        'E': 18,
        'Z': 21,
        'Y': 24,
    }

    SI_defaults = {
        p:          2,
        p_max:      8,
        p_min:      0,
        separator:  ',',
        decimal:    '.',
        unit_sep:   '&nbsp;',
        cap_kilo:   false,
        no_mu:      false,
        exclude:    ['c', 'd', 'D', 'H'],
    }
    // Apply the default precision setting above
    for (var i = -24; i <= 24; i++) {
        if (i.toString() in prefixDef) {
            prefixDef[i.toString()]['p'] = SI_defaults['p'];
        }
    }
    //SI_options = Unpack_SI_options(SI_options, options_input);
    var SI_options = SI_set_options(SI_defaults, options_input, prefixDef, pre2num);
    //console.log(SI_options);
    //console.log(options_input);
    SI_options['p'] = options_input['p'];

    //console.log('SI_options: ', SI_options);

    prefixDef = SI_set_precision(SI_options, prefixDef, pre2num);

    var magnitude = Math.floor(Math.log(value)/Math.log(10));

    if (magnitude >= -24 && magnitude <= 24) {
        // Change any inbetween magnitudes to one that has a prefix assigned to it
        /*
        if (!(magnitude in prefixDef)) {
            magnitude = 3 * Math.floor(magnitude / 3);   
        }*/
        while (!(magnitude in prefixDef)) {
            magnitude--;
        }
        // Get the precision specified for that magnitude
        precision = prefixDef[magnitude]['p'];

        // Divide the number by the appropriate power of 10, and return the new number and associated SI prefix
        out_value = Commas(Number(value / Math.pow(10, magnitude)).toFixed(precision));
        out_prefix = prefixDef[magnitude]['sym'];
    }
    else {
        out_value = Commas(value);
        out_prefix = '';
    }

    return (out_value + '&nbsp;' + out_prefix + unit);
}


function SI_set_options(SI_options, options_input, prefixDef, pre2num) {
    var opt = ['p', 'p_max', 'p_min', 'separator', 'decimal', 'unit_sep', 'cap_k', 'no_mu'];
    // Loop through all options, if specified by options_input, replace the default
    for (var i = 0; i < opt.length; i++) {
        if (opt[i] in options_input) {
            SI_options[opt[i]] = options_input[opt[i]];
        }
    }
    var explicit_incl = [];
    var arg;
    var offset;

    if (!('exclude' in options_input)) {
        DEBUG('options_input contained no exclusions');
    }
    if (typeof(options_input['exclude']) == 'string') {
        DEBUG('(SI_options == string) returns true');
    }

    else if (typeof(options_input['exclude']) == 'object') {
        DEBUG('(SI_options == object) returns true');

        // First pass, loops through [exclude] list provided in the input options, to check for any prefixes declared as Explicitly Included, with '!'
        for (var i = 0; i < options_input['exclude'].length; i++) {
            arg = options_input['exclude'][i];
            if (isNaN(parseInt(arg[0]))) {
                // If arg[0] is !, add arg[1] to the list of explicitly included prefixes
                if (arg[0] == '!' && pre2num[arg[1]] in prefixDef) {
                    explicit_incl.push(arg[1]);
                }
            }
        }
        DEBUG('Explicit inclusions:', explicit_incl);
        // Second pass, loops through [exclude] list to look for '*', which excludes all prefixes except the ones on the explicit_incl list.
        for (var i = 0; i < options_input['exclude'].length; i++) {
            arg = options_input['exclude'][i];
            if (isNaN(parseInt(arg[0]))) {
                if (arg[0] == '*') {
                    // If arg[0] is *, then exclude all prefixes except those explicitly included
                    for (var j = -24; j <= 24; j++ ) {
                        if (j in prefixDef) {
                            if (explicit_incl.indexOf(prefixDef[j]['sym']) == -1 && !(prefixDef[j]['sym'] in SI_options['exclude'])) {
                                SI_options['exclude'].push(prefixDef[j]['sym']);
                                DEBUG('Prefix "' + prefixDef[j]['sym'] + '" excluded');
                                DEBUG(prefixDef[j]['sym'] + ' not in explicit_incl: ' + !(prefixDef[j]['sym'] in explicit_incl));
                            }
                        }
                    }
                    break;
                }
            }
        }
        // Third pass, loops through [exclude] list again, to expand all shortcuts like ">=G" into "[G, T, P, E ... ]"
        for (var i = 0; i < options_input['exclude'].length; i++) {
            arg = options_input['exclude'][i];
            if (isNaN(parseInt(arg[0]))) {
                if (arg[0] == '>') { 
                    if (arg[1] == '=') { offset = 0; DEBUG('Offset set to 0') } // If command is ">=", offset 0 to include starting position.
                    else               { offset = 1; } // Otherwise, command is ">", offset 1 to not include the specified prefix
                    for (var j = pre2num[arg[1 + (1-offset)]] + offset; j <= 24; j++) {
                        DEBUG('J:', j);
                        if (j in prefixDef) {
                            // Check to make sure it hasn't been explicitly included (present in explicit_incl list) or duplicate (present in SI[exclude])
                            if (explicit_incl.indexOf(prefixDef[j]['sym']) == -1 && !(prefixDef[j]['sym'] in SI_options['exclude'])) {
                                // Exclude prefixes at/above the prefix specified by arg[1]
                                SI_options['exclude'].push(prefixDef[j]['sym']);
                            }
                        }
                    }
                }
                else if (arg[0] == '<') {
                    if (arg[1] == '=') { offset = 0; } // If command is "<=", offset 0
                    else               { offset = 1; } // Otherwise, command is "<", offset 1 to include the specified prefix
                    for (var j = pre2num[arg[1 + (1-offset)]] - offset; j >= -24; j--) {
                        if (j in prefixDef) {
                            // Check to make sure it hasn't been explicitly included (present in explicit_incl list) or duplicate (present in SI[exclude])
                            if (explicit_incl.indexOf(prefixDef[j]['sym']) == -1 && !(prefixDef[j]['sym'] in SI_options['exclude'])) {
                                // Exclude prefixes at/below the prefix specified by arg[1]
                                SI_options['exclude'].push(prefixDef[j]['sym']);
                            }
                        }
                    }
                }
                else {
                    j = pre2num[arg[0]];
                    if (j in prefixDef) {
                        if (explicit_incl.indexOf(prefixDef[j]['sym']) == -1 && !(prefixDef[j]['sym'] in SI_options['exclude'])) {
                            SI_options['exclude'].push(prefixDef[j]['sym']);
                        }
                    }
                }
            }
        }

    }
    return SI_options;
}


function SI_set_precision(SI_options, prefixDef, pre2num) {
    for (var i = 0; i < SI_options['exclude'].length; i++) {
        //prefixDef[pre2num[SI_options['exclude'][i]]]['excl'] = true;
        // Delete prefixes that have been excluded
        delete prefixDef[pre2num[SI_options['exclude'][i]]];
    }
    var precision = SI_options['p'];

    // If the precision argument is a string, then there is only 1 precision specified
    if (typeof(precision) == 'string') {

        // If it's a 1-character string, then it's a pure number, which means it is set as the default for all prefixes
        if (precision.length == 1) {
            for (var i = -24; i <= 24; i++) {
                if (i.toString() in prefixDef) {
                    prefixDef[i.toString()]['p'] = precision;
                }
            }
        }
        // If it's a 2-character string, it could be a 2 digit number or a prefix-specific code
        else if (precision.length == 2) {
            if (isNaN(parseInt(precision[0]))) {
                prefixDef[pre2num[precision[0]]]['p'] = parseInt(precision[1]);
            }
        }
    }
    else if (typeof(precision) == 'object') {
        for (var j = 0; j < precision.length; j++) {
            if (precision[j].length == 1) {
                for (var i = -24; i <= 24; i++) {
                    if (i.toString() in prefixDef) {
                        prefixDef[i.toString()]['p'] = parseInt(precision[j]);
                    }
                }
            }
                // If it's a 2-character string, it could be a 2 digit number or a prefix-specific code
            else if (precision[j].length == 2) {
                if (isNaN(parseInt(precision[j][0])) && (pre2num[precision[j][0]] in prefixDef)) {
                    prefixDef[pre2num[precision[j][0]]]['p'] = parseInt(precision[j][1]);
                }
            }
        }
    }
    return prefixDef
}


function LoadCTA861(){
    // read text from URL location
    var request = new XMLHttpRequest();
    request.open('GET', 'CTA861.txt', true);
    request.send(null);
    request.onreadystatechange = function () {
        if (request.readyState === 4 && request.status === 200) {
            var type = request.getResponseHeader('Content-Type');
            if (type.indexOf("text") !== 1) {
                return request.responseText;
            }
        }
    }
}


async function LoadCTA861_Async() {
    DEBUG('CTA Test 7');
    // Loads the timing definitions for the CTA-861 standard from a txt file
    response = await fetch('CTA861.txt');
    CTA861 = $.csv.toObjects(await response.text());
}


//Small functions
{
function Commas(num) {
    var parts = num.toString().split(".");
    parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
    return parts.join(".");
}


function getPrecision(x, watchdog) {
    // https://stackoverflow.com/questions/27082377/get-number-of-decimal-places-with-javascript
    x = Math.abs(x);
    watchdog = watchdog || 32;
    var i = 0;
    while (x % 1 > 0 && i < watchdog) {
        i++;
        x = x * 10;
    }
    return i;
}


function GCD(a, b) {
    a = Math.abs(a);
    b = Math.abs(b);
    if (b > a) { var temp = a; a = b; b = temp; }
    while (true) {
        if (b == 0) return a;
        a %= b;
        if (a == 0) return b;
        b %= a;
    }
}


function SameRatio(H, V, A) {
    /* Checks if the ratio H/V is equal to the given ratio A (within a defined margin of error E) */
    /* Negative signs on H, V, and A are ignored */

    var E = 0.001; /* E is a percent error written as a decimal (i.e. E = 0.001 would give an acceptable error margin of 0.1%) */

    if (Math.abs((Math.abs(H / V) / Math.abs(A)) - 1) <= E) { return true; }
    else { return false; }
}


    /*
function parseNum(val) {
    // Converts string to floating point if it has a decimal point, or integer if there is no decimal point. Also strips commas and spaces, and optionally applies absolute value.

    if (typeof val === "string") {
        // val = val.replace(/[^0-9\. ]/g, ''); // Apply absolute value and ignore non-numeric characters
        // val = val.replace(/[^0-9\. -]/g, ''); // Allow negative numbers and ignore non-numeric characters
        
        //val = val.replace(/-/g, ''); // Remove minus signs

        // Return NaN if...
        if (val == '' // input is blank
            || val.indexOf('.') != val.lastIndexOf('.') // input contains multiple decimal places
            || val.indexOf('-') != val.lastIndexOf('-') // input contains multiple minus signs
            || (val.indexOf('-') != -1 && val.indexOf('-') != 0) // input contains a minus sign in a position other than the first character
            || val.match(/[^0-9.-]/g)) // input contains any character other than a number, decimal, minus, or space
        {
            return NaN;
        }

        else {
            // Once we have checked that the input contains no characters other than numerals, periods, and minus signs
            // And that there are at most one period and one minus sign, and both are in valid positions, we can evaluate the number as either float or string.
            // If for some reason either of them fail, it will still return NaN anyway
            if (val.indexOf('.') == -1)
                return parseInt(val);
            else {
                return parseFloat(val);
            }
        }
    }
    else if (Number.isNaN(val)) {
        return NaN; // Check if val is literally 'NaN'
    }
    else if (typeof val === "number") {
        return val; // NaN is recognized as a number, so this line must be after NaN is handled
    }
    else {
        return NaN;
    }
}*/


function parseNum(val) {
    // This function is designed to interpret strings with formatted numbers (which may contain currencies, digit grouping commas, units, etc.)
    // It will return NaN if it cannot be interpreted as a valid number (i.e. no numeric characters, multiple periods or minus signs, etc.)

    if (typeof(val) === 'number') {
        // If the input argument is already a number, then nothing needs to be done, simply return the input value
        if (Number.isNaN(val) == true) {
            // However, we do need to check that it isn't NaN, because that is identified as a number.
            return NaN;
        }
        else {
            return val;
        }
    }

    else if (typeof val === 'string') {
        // First, remove all non-numeric characters on the outsides of the string
        for (var i = 0; i < val.length; i++) {
            if (!(i < val.length)) { break; }
            //DEBUG('i:', i, 'val:', val, 'val[i]:', val[i]);
            // Loop through each character starting from the front
            if ((/[^0-9.-]/g).test(val[i]) == true) {
                // If character is not a number, period, or minus sign, remove it
                if (i == 0 && val.length > 1) { val = val.slice(1); }
                else if (i == val.length - 1) { return NaN; } // If this is the last character in the string
                else if (i > 0) { val = (val.slice(0, i)) + (val.slice(i+1));}
                i = i - 1; // Since a character has been removed, the next character is now at the same index
                continue;
            }
            else if ((/[-]/g).test(val[i]) == true) {
                // If character is a negative sign, ignore it. This is because there may still be non-number characters between the negative sign and the first digit, such as "-$1.00". The negative sign should stay but the dollar needs to be removed.
                continue;
            }
            else if ((/[.]/g).test(val[i]) == true) {
                // If character is a period, then following character MUST be a digit, unless it's the end of the number.
                if (i + 1 < val.length) {
                    if ((/[0-9]/g).test(val[i+1]) == true) {
                        // If the character after the period is a digit, then the first digit has been reached
                        break;
                    }
                    else {
                        // If the string contained a period followed by a non-numeric character, it is not a number
                        return NaN;
                    }
                }
                else {
                    // If i+1 was not < val.length, then the period is the last character in the string, which implicitly means the string contains no numeric characters.
                    return NaN;
                }
            }
            else if ((/[0-9]/g).test(val[i]) == true) {
                // If character was a numeric character, we have successfully stripped any leading characters and reached the start of the number.
                break;
            }
        }

        // Now do a similar starting from the backside, to strip any trailing characters (such as units of measurement)
        for (var i = (val.length - 1); i >= 0; i--) {
            if ((/[^0-9]/g).test(val[i]) == true) {
                // No need to modify iterator for this direction
                // Since we are dealing with the end of the number, minus signs are no longer part of the number. Periods at the end are valid, but superfluous
                // Since we are only checking the characters after the digits have ended, minus signs are not invalid, as they may be part of a unit of measurement. Therefore, they are simply removed, rather than returning NaN.
                if (val.length == 1) { return NaN; }
                else { val = val.slice(0, i); }
                continue;
            }
            else if ((/[0-9]/g).test(val[i]) == true) {
                // If character is a numeric character, we have reached the back end of the number and successfully stripped any trailing characters.
                break;
            }
        }

        // We now strip any commas and whitespace throughout the string
        val = val.replace(/[, ]/g, '');
        // If, after removing leading and trailing units, whitespace, and commas, there are any non-numeric characters (i.e. in the middle of the string after the numbers start), it is not a valid number
        if ((/[^0-9.-]/g).test(val) == true) {
            return NaN;
        }
        // Now that the string only contains numeric characters, minus signs, and periods, we must check if there are any invalid usages of the periods and signs, such as multiple periods/signs, or a minus sign anywhere other than the first character.

        if (val == '' // string is empty
            || val.indexOf('.') != val.lastIndexOf('.') // string contains multiple decimal point
            || val.indexOf('-') != val.lastIndexOf('-') // string contains multiple minus signs
            || (val.indexOf('-') != -1 && val.indexOf('-') != 0)) // input contains a minus sign and it isn't the first character
        {
            return NaN;
        }

        // Finished; string now only contains numbers, up to one period, and up to one minus sign as the first character of the string.

        // If for some reason the parseFloat fails, the function will return NaN, so the output is still consistent with any other failed condition.
        return parseFloat(val);
    }
}


function isNum(num) {
    // Returns false if input is not a number, including a blank string or NaN
    // Can accept arrays, and returns true only if all elements would return true individually
    if (Array.isArray(num) == true) {
        for (a = 0; a < num.length; a++) {
            if (Number.isNaN(parseFloat(num[a])) == true) {
                return false;
            }
            return true;
        }
    }
    else {
        return !Number.isNaN(parseFloat(num));
    }
}


function isPositive(num) {

    if (Array.isArray(num) == true) {
        for (a = 0; a < num.length; a++) {
            if (Number.isNaN(parseNum(num[a])) == true)
                return false;
            else if (num[a] <= 0)
                return false;
        }
        return true;
    }
    else {
        if (Number.isNaN(parseNum(num)) == true)
            return false;
        else if (num > 0)
            return true;
        else {
            return false;
        }
    }
}


function isPositiveZ(num) {
    // Returns true if input is positive or zero. Can recognize formatted numbers (i.e. "1,234,567 Gbit/s" or "-$1,234 USD") as allowed by parseNum
    // Combine with isNum() to allow only true (non-formatted) numbers and strings (i.e. if (isNum(x) && isPositiveZ(x))

    /*
    
    True:

    isPositiveZ(1)
    isPositiveZ('1')
    isPositiveZ(1.1)
    isPositiveZ('1.1')
    isPositiveZ('1.')             Number string with stray decimal
    isPositiveZ(0) or ('0')
    isPositiveZ(Infinity)
    isPositiveZ([1, '2', 3.3])    true iff all elements are non-negative

    False:

    isPositiveZ(-1)
    isPositiveZ('-1')
    isPositiveZ(-1.1)
    isPositiveZ('-1.1')
    isPositiveZ('1 ')     Number with whitespace
    isPositiveZ('')       Empty String
    isPositiveZ('abc')
    isPositiveZ(NaN)
    isPositiveZ([-1, '2', 3.3])   If any element is negative
    
    */

    if (Array.isArray(num) == true) {
        for (a = 0; a < num.length; a++) {
            if (Number.isNaN(parseNum(num[a])) == true)
                return false;
            else if (num[a] < 0)
                return false;
        }
        return true;
    }
    else {
        if (Number.isNaN(parseNum(num)) == true)
            return false;
        else if (num >= 0)
            return true;
        else {
            return false;
        }
    }
}


function isInt(num) {
    /*
    True:

    isInt(1)
    isInt('1')
    isInt(-1)
    isInt('-1')
    isInt(0)
    isInt([1, -2, '3'])
    isInt(1, 'a')  -> true (be careful to enter multiple arguments as an array, otherwise it will only evaluate the first argument, as seen here)

    False:

    isInt(1.1)
    isInt('1 ')
    isInt('1 2')
    isInt('abc')
    isInt('abc123')
    isInt('')
    isInt(NaN)
    isInt(Infinity)
    isInt([1, 2, 3.3])  Returns false if any of the array elements are not an integer

    */

    if (Array.isArray(num) == true) {
        for (a = 0; a < num.length; a++) {
            if (Number.isInteger(parseNum(num[a])) == false) {
                return false;
            }
        }
        return true;
    }
    else {
        num = parseNum(num);
        return Number.isInteger(num);
    }
}


function isFloat(num) {
    /*

    True:

    isFloat(1.1)
    isFloat('1.1')
    isFloat(-1.1)
    isFloat('-1.1')
    isFloat('.1')
    isFloat('-.1')
    isFloat([1.1, -1.2, '1.3'])
    isFloat(Infinity)
    isFloat([1.1, 'a'])  -> true (be careful to enter multiple arguments as an array, otherwise it will only evaluate the first argument, as seen here)

    False:

    isFloat(1.0)
    isFloat('1.0')
    isFloat(0)
    isFloat(1)
    isFloat('1.1.1')
    isFloat('')
    isFloat('a')
    isFloat('1.1a')
    isFloat([1.1, 1.2, 3])
    isFloat(NaN)

    */
    if (Array.isArray(num) == true) {
        for (a = 0; a < num.length; a++) {
            num[a] = parseNum(num[a]);
            if (Number.isInteger(num[a]) == true || Number.isNaN(num[a]) == true) {
                return false;
            }
        }
        return true;
    }
    else {
        num = parseNum(num);
        return !(Number.isInteger(num) || Number.isNaN(num));
    }
}


}


window.onpageshow = function() {
    generate_table('Interface Support', -1);
    CTA861 = LoadCTA861();
    $('#INPUT_HRES')[0].onchange();
    $('#INPUT_VRES')[0].onchange();
    $('#INPUT_F')[0].onchange();
    $('#COLOR_DEPTH_FORM')[0].onchange();
    $('#PIXEL_FORMAT_FORM')[0].onchange();
    $('#COMPRESSION_FORM')[0].onchange();
    $('#SCAN_FORM')[0].onchange();
    $('#MARGINS_FORM')[0].onchange();
    $('#TIMING_DROP')[0].onchange();
    calcMain();
}