function createElementWithString(htmlString){
    const tempDiv = document.createElement('div');
    tempDiv.innerHTML = htmlString;
    return tempDiv.firstElementChild;
}

function div$2(options) {
    return create$1("div", options);
}

function create$1(tag, options) {
    const elem = document.createElement(tag);
    if (options) {
        if (options.class) {
            elem.classList.add(options.class);
        }
        if (options.id) {
            elem.id = options.id;
        }
        if(options.style) {
            applyStyle$1(elem, options.style);
        }
    }
    return elem;
}

function hide$1(elem) {
    const cssStyle = getComputedStyle(elem);
    if(cssStyle.display !== "none") {
        elem._initialDisplay = cssStyle.display;
    }
    elem.style.display = "none";
}

function show$1(elem) {
    //const currentDisplay = getComputedStyle(elem).display;
    //if (currentDisplay === "none") {
        const d = elem._initialDisplay || "block";
        elem.style.display = d;
   // }
}

function pageCoordinates(e) {

    if (e.type.startsWith("touch")) {
        const touch = e.touches[0];
        return {x: touch.pageX, y: touch.pageY};
    } else {
        return {x: e.pageX, y: e.pageY}
    }
}

function applyStyle$1(elem, style) {
    for (let key of Object.keys(style)) {
        elem.style[key] = style[key];
    }
}

function guid$2  () {
    return ("0000" + (Math.random() * Math.pow(36, 4) << 0).toString(36)).slice(-4);
}

let getMouseXY = (domElement, { clientX, clientY }) => {

    // DOMRect object with eight properties: left, top, right, bottom, x, y, width, height
    const { left, top, width, height } = domElement.getBoundingClientRect();

    const x = clientX - left;
    const y = clientY - top;
    return { x, y, xNormalized: x/width, yNormalized: y/height, width, height };

};

/**
 * Translate the mouse coordinates for the event to the coordinates for the given target element
 * @param event
 * @param domElement
 * @returns {{x: number, y: number}}
 */
function translateMouseCoordinates(event, domElement) {

    const { clientX, clientY } = event;
    return getMouseXY(domElement, { clientX, clientY });

}

/**
 * Generic container for UI components
 */
class Panel {

    constructor() {
        this.elem = create$1('div', { class: 'igv-ui-panel-column' });
    }

    add(component) {

        if(component instanceof Node) {
            this.elem.appendChild(component);
        }
        else if(typeof component === 'object') {
            this.elem.appendChild(component.elem);
        }
        else {
            // Assuming a string, possibly html
            const wrapper = div$2();
            wrapper.innerHTML = component;
            this.elem.appendChild(wrapper);
            this.html = wrapper;
        }
    }


}

function createCheckbox$1(name, initialState) {
    const container = div$2({class: 'igv-ui-trackgear-popover-check-container'});
    const svg = iconMarkup$1('check', (true === initialState ? '#444' : 'transparent'));
    svg.style.borderColor = 'gray';
    svg.style.borderWidth = '1px';
    svg.style.borderStyle = 'solid';

    container.appendChild(svg);
    let label = div$2(); //{ class: 'igv-some-label-class' });
    label.textContent = name;
    container.appendChild(label);

    return container;
}

function createIcon$1(name, color) {
    return iconMarkup$1(name, color);
}

function iconMarkup$1(name, color) {
    color = color || "currentColor";
    let icon = icons$1[name];
    if (!icon) {
        console.error(`No icon named: ${name}`);
        icon = icons$1["question"];
    }

    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttributeNS(null,'viewBox', '0 0 ' + icon[0] + ' ' + icon[1]);
    const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
    path.setAttributeNS(null,'fill',  color );
    path.setAttributeNS(null,'d', icon[4]);
    svg.appendChild(path);
    return svg;
}

const icons$1 = {
    "check": [512, 512, [], "f00c", "M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"],
    "cog": [512, 512, [], "f013", "M444.788 291.1l42.616 24.599c4.867 2.809 7.126 8.618 5.459 13.985-11.07 35.642-29.97 67.842-54.689 94.586a12.016 12.016 0 0 1-14.832 2.254l-42.584-24.595a191.577 191.577 0 0 1-60.759 35.13v49.182a12.01 12.01 0 0 1-9.377 11.718c-34.956 7.85-72.499 8.256-109.219.007-5.49-1.233-9.403-6.096-9.403-11.723v-49.184a191.555 191.555 0 0 1-60.759-35.13l-42.584 24.595a12.016 12.016 0 0 1-14.832-2.254c-24.718-26.744-43.619-58.944-54.689-94.586-1.667-5.366.592-11.175 5.459-13.985L67.212 291.1a193.48 193.48 0 0 1 0-70.199l-42.616-24.599c-4.867-2.809-7.126-8.618-5.459-13.985 11.07-35.642 29.97-67.842 54.689-94.586a12.016 12.016 0 0 1 14.832-2.254l42.584 24.595a191.577 191.577 0 0 1 60.759-35.13V25.759a12.01 12.01 0 0 1 9.377-11.718c34.956-7.85 72.499-8.256 109.219-.007 5.49 1.233 9.403 6.096 9.403 11.723v49.184a191.555 191.555 0 0 1 60.759 35.13l42.584-24.595a12.016 12.016 0 0 1 14.832 2.254c24.718 26.744 43.619 58.944 54.689 94.586 1.667 5.366-.592 11.175-5.459 13.985L444.788 220.9a193.485 193.485 0 0 1 0 70.2zM336 256c0-44.112-35.888-80-80-80s-80 35.888-80 80 35.888 80 80 80 80-35.888 80-80z"],
    "exclamation": [192, 512, [], "f12a", "M176 432c0 44.112-35.888 80-80 80s-80-35.888-80-80 35.888-80 80-80 80 35.888 80 80zM25.26 25.199l13.6 272C39.499 309.972 50.041 320 62.83 320h66.34c12.789 0 23.331-10.028 23.97-22.801l13.6-272C167.425 11.49 156.496 0 142.77 0H49.23C35.504 0 24.575 11.49 25.26 25.199z"],
    "exclamation-circle": [512, 512, [], "f06a", "M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"],
    "exclamation-triangle": [576, 512, [], "f071", "M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"],
    "minus": [448, 512, [], "f068", "M424 318.2c13.3 0 24-10.7 24-24v-76.4c0-13.3-10.7-24-24-24H24c-13.3 0-24 10.7-24 24v76.4c0 13.3 10.7 24 24 24h400z"],
    "minus-circle": [512, 512, [], "f056", "M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zM124 296c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h264c6.6 0 12 5.4 12 12v56c0 6.6-5.4 12-12 12H124z"],
    "minus-square": [448, 512, [], "f146", "M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zM92 296c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h264c6.6 0 12 5.4 12 12v56c0 6.6-5.4 12-12 12H92z"],
    "plus": [448, 512, [], "f067", "M448 294.2v-76.4c0-13.3-10.7-24-24-24H286.2V56c0-13.3-10.7-24-24-24h-76.4c-13.3 0-24 10.7-24 24v137.8H24c-13.3 0-24 10.7-24 24v76.4c0 13.3 10.7 24 24 24h137.8V456c0 13.3 10.7 24 24 24h76.4c13.3 0 24-10.7 24-24V318.2H424c13.3 0 24-10.7 24-24z"],
    "plus-circle": [512, 512, [], "f055", "M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm144 276c0 6.6-5.4 12-12 12h-92v92c0 6.6-5.4 12-12 12h-56c-6.6 0-12-5.4-12-12v-92h-92c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h92v-92c0-6.6 5.4-12 12-12h56c6.6 0 12 5.4 12 12v92h92c6.6 0 12 5.4 12 12v56z"],
    "plus-square": [448, 512, [], "f0fe", "M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-32 252c0 6.6-5.4 12-12 12h-92v92c0 6.6-5.4 12-12 12h-56c-6.6 0-12-5.4-12-12v-92H92c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h92v-92c0-6.6 5.4-12 12-12h56c6.6 0 12 5.4 12 12v92h92c6.6 0 12 5.4 12 12v56z"],
    "question": [384, 512, [], "f128", "M202.021 0C122.202 0 70.503 32.703 29.914 91.026c-7.363 10.58-5.093 25.086 5.178 32.874l43.138 32.709c10.373 7.865 25.132 6.026 33.253-4.148 25.049-31.381 43.63-49.449 82.757-49.449 30.764 0 68.816 19.799 68.816 49.631 0 22.552-18.617 34.134-48.993 51.164-35.423 19.86-82.299 44.576-82.299 106.405V320c0 13.255 10.745 24 24 24h72.471c13.255 0 24-10.745 24-24v-5.773c0-42.86 125.268-44.645 125.268-160.627C377.504 66.256 286.902 0 202.021 0zM192 373.459c-38.196 0-69.271 31.075-69.271 69.271 0 38.195 31.075 69.27 69.271 69.27s69.271-31.075 69.271-69.271-31.075-69.27-69.271-69.27z"],
    "save": [448, 512, [], "f0c7", "M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z"],
    "search": [512, 512, [], "f002", "M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"],
    "share": [512, 512, [], "f064", "M503.691 189.836L327.687 37.851C312.281 24.546 288 35.347 288 56.015v80.053C127.371 137.907 0 170.1 0 322.326c0 61.441 39.581 122.309 83.333 154.132 13.653 9.931 33.111-2.533 28.077-18.631C66.066 312.814 132.917 274.316 288 272.085V360c0 20.7 24.3 31.453 39.687 18.164l176.004-152c11.071-9.562 11.086-26.753 0-36.328z"],
    "spinner": [512, 512, [], "f110", "M304 48c0 26.51-21.49 48-48 48s-48-21.49-48-48 21.49-48 48-48 48 21.49 48 48zm-48 368c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zm208-208c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zM96 256c0-26.51-21.49-48-48-48S0 229.49 0 256s21.49 48 48 48 48-21.49 48-48zm12.922 99.078c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.491-48-48-48zm294.156 0c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.49-48-48-48zM108.922 60.922c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.491-48-48-48z"],
    "square": [448, 512, [], "f0c8", "M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z"],
    "square-full": [512, 512, [], "f45c", "M512 512H0V0h512v512z"],
    "times": [384, 512, [], "f00d", "M323.1 441l53.9-53.9c9.4-9.4 9.4-24.5 0-33.9L279.8 256l97.2-97.2c9.4-9.4 9.4-24.5 0-33.9L323.1 71c-9.4-9.4-24.5-9.4-33.9 0L192 168.2 94.8 71c-9.4-9.4-24.5-9.4-33.9 0L7 124.9c-9.4 9.4-9.4 24.5 0 33.9l97.2 97.2L7 353.2c-9.4 9.4-9.4 24.5 0 33.9L60.9 441c9.4 9.4 24.5 9.4 33.9 0l97.2-97.2 97.2 97.2c9.3 9.3 24.5 9.3 33.9 0z"],
    "times-circle": [512, 512, [], "f057", "M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm121.6 313.1c4.7 4.7 4.7 12.3 0 17L338 377.6c-4.7 4.7-12.3 4.7-17 0L256 312l-65.1 65.6c-4.7 4.7-12.3 4.7-17 0L134.4 338c-4.7-4.7-4.7-12.3 0-17l65.6-65-65.6-65.1c-4.7-4.7-4.7-12.3 0-17l39.6-39.6c4.7-4.7 12.3-4.7 17 0l65 65.7 65.1-65.6c4.7-4.7 12.3-4.7 17 0l39.6 39.6c4.7 4.7 4.7 12.3 0 17L312 256l65.6 65.1z"],
    "wrench": [512, 512, [], "f0ad", "M481.156 200c9.3 0 15.12 10.155 10.325 18.124C466.295 259.992 420.419 288 368 288c-79.222 0-143.501-63.974-143.997-143.079C223.505 65.469 288.548-.001 368.002 0c52.362.001 98.196 27.949 123.4 69.743C496.24 77.766 490.523 88 481.154 88H376l-40 56 40 56h105.156zm-171.649 93.003L109.255 493.255c-24.994 24.993-65.515 24.994-90.51 0-24.993-24.994-24.993-65.516 0-90.51L218.991 202.5c16.16 41.197 49.303 74.335 90.516 90.503zM104 432c0-13.255-10.745-24-24-24s-24 10.745-24 24 10.745 24 24 24 24-10.745 24-24z"],
};

function attachDialogCloseHandlerWithParent$1(parent, closeHandler) {

    var container = document.createElement("div");
    parent.appendChild(container);
    container.appendChild(createIcon$1("times"));
    container.addEventListener('click', function (e) {
        e.preventDefault();
        e.stopPropagation();
        closeHandler();
    });
}

/**
 * Make the target element movable by clicking and dragging on the handle.  This is not a general purprose function,
 * it makes several options specific to igv dialogs, the primary one being that the
 * target is absolutely positioned in pixel coordinates

 */

let dragData$1;  // Its assumed we are only dragging one element at a time.

function makeDraggable$1(target, handle, constraint) {

    handle.addEventListener('mousedown', dragStart.bind(target));

    function dragStart(event) {

        event.stopPropagation();
        event.preventDefault();

        const dragFunction = drag$1.bind(this);
        const dragEndFunction = dragEnd$1.bind(this);
        const computedStyle = getComputedStyle(this);


        const boundingClientRect = this.getBoundingClientRect();
        dragData$1 =
            {
                constraint,
                dragFunction,
                dragEndFunction,
                screenX: event.screenX,
                screenY: event.screenY,
                minDy: -boundingClientRect.top,   // Don't slide upwards more than this
                minDx: -boundingClientRect.left,
                top: parseInt(computedStyle.top.replace("px", "")),
                left: parseInt(computedStyle.left.replace("px", ""))
            };

        document.addEventListener('mousemove', dragFunction);
        document.addEventListener('mouseup', dragEndFunction);
        document.addEventListener('mouseleave', dragEndFunction);
        document.addEventListener('mouseexit', dragEndFunction);
    }
}

function drag$1(event) {

    if (!dragData$1) {
        console.error("No drag data!");
        return
    }
    event.stopPropagation();
    event.preventDefault();
    const dx = Math.max(dragData$1.minDx, event.screenX - dragData$1.screenX);
    const dy = Math.max(dragData$1.minDy, event.screenY - dragData$1.screenY);
    const left = dragData$1.left + dx;
    const top = dragData$1.top + dy;

    this.style.left = `${left}px`;
    this.style.top = `${top}px`;
}

function dragEnd$1(event) {

    if (!dragData$1) {
        console.error("No drag data!");
        return
    }
    event.stopPropagation();
    event.preventDefault();

    const dragFunction = dragData$1.dragFunction;
    const dragEndFunction = dragData$1.dragEndFunction;
    document.removeEventListener('mousemove', dragFunction);
    document.removeEventListener('mouseup', dragEndFunction);
    document.removeEventListener('mouseleave', dragEndFunction);
    document.removeEventListener('mouseexit', dragEndFunction);
    dragData$1 = undefined;
}

class Dialog {

    constructor({parent, label, content, okHandler, cancelHandler}) {

        this.parent = parent;

        const cancel = () => {
            this.elem.style.display = 'none';
            if (typeof cancelHandler === 'function') {
                cancelHandler(this);
            }
        };

        // dialog container
        this.elem = div$2();
        this.elem.classList.add('igv-ui-generic-dialog-container', 'igv-ui-center-fixed');

        // dialog header
        const header = div$2({class: 'igv-ui-generic-dialog-header'});
        this.elem.appendChild(header);

        attachDialogCloseHandlerWithParent$1(header, cancel);

        // dialog label
        if(label) {
            const labelDiv = div$2({class: 'igv-ui-dialog-one-liner'});
            this.elem.appendChild(labelDiv);
            labelDiv.innerHTML = label;
        }

        // input container
        content.elem.style.margin = '16px';
        this.elem.appendChild(content.elem);

        this.content = content;

        // ok | cancel
        const buttons = div$2({class: 'igv-ui-generic-dialog-ok-cancel'});
        this.elem.appendChild(buttons);

        // ok
        this.ok = div$2();
        buttons.appendChild(this.ok);
        this.ok.textContent = 'OK';

        // cancel
        this.cancel = div$2();
        buttons.appendChild(this.cancel);
        this.cancel.textContent = 'Cancel';

        this.callback = undefined;

        this.ok.addEventListener('click',  e => {
            this.elem.style.display = 'none';
            if (typeof okHandler === 'function') {
                okHandler(this);
            } else if (this.callback && typeof this.callback === 'function') {
                this.callback(this);
            }
        });

        this.cancel.addEventListener('click', cancel);

        makeDraggable$1(this.elem, header);

        // Consume all clicks in component
        this.elem.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
        });

    }

    present(options, e) {

        if (options.label && this.label) {
            this.label.textContent = options.label;
        }

        if (options.html) {
            const div = this.content.html;
            div.innerHTML = options.html;
        }

        if (options.text) {
            const div = this.content.html;
            div.innerText = options.text;
        }

        if (options.value && this.input) {
            this.input.value = options.value;
        }

        if (options.callback) {
            this.callback = options.callback;
        }

        const { top} = e.currentTarget.parentElement.getBoundingClientRect();
        this.elem.style.top = `${ top }px`;

        this.elem.style.display = 'flex';
    }
}

/**
 * Covers string literals and String objects
 * @param x
 * @returns {boolean}
 */
function isString$3(x) {
    return typeof x === "string" || x instanceof String
}


// StackOverflow: http://stackoverflow.com/a/10810674/116169
function numberFormatter$1(rawNumber) {

    var dec = String(rawNumber).split(/[.,]/),
        sep = ',',
        decsep = '.';

    return dec[0].split('').reverse().reduce(function (prev, now, i) {
        return i % 3 === 0 ? prev + sep + now : prev + now;
    }).split('').reverse().join('') + (dec[1] ? decsep + dec[1] : '');
}

const splitLines$3 = function (string) {
    return string.split(/\n|\r\n|\r/g);
};


function splitStringRespectingQuotes(string, delim) {

    var tokens = [],
        len = string.length,
        i,
        n = 0,
        quote = false,
        c;

    if (len > 0) {

        tokens[n] = string.charAt(0);
        for (i = 1; i < len; i++) {
            c = string.charAt(i);
            if (c === '"') {
                quote = !quote;
            } else if (!quote && c === delim) {
                n++;
                tokens[n] = "";
            } else {
                tokens[n] += c;
            }
        }
    }
    return tokens;
}

function stripQuotes$2(str) {
    if(str === undefined) {
        return str;
    }
    if(str.startsWith("'") || str.startsWith('"')) {
        str = str.substring(1);
    }
    if (str.endsWith("'") || str.endsWith('"')) {
        str = str.substring(0, str.length - 1);
    }
    return str;
}

function capitalize(str) {
    return str.length > 0 ? str.charAt(0).toUpperCase() + str.slice(1) : str;
}


/**
 * Parse a locus string and return a range object.  Locus string is of the form chr:start-end.  End is optional
 *
 */
function parseLocusString$1(string) {

    const t1 = string.split(":");
    const t2 = t1[1].split("-");

    const range = {
        chr: t1[0],
        start: Number.parseInt(t2[0].replace(/,/g, '')) - 1
    };

    if (t2.length > 1) {
        range.end = Number.parseInt(t2[1].replace(/,/g, ''));
    } else {
        range.end = range.start + 1;
    }

    return range;
}

/**
 * Return the filename from the path.   Example
 *   https://foo.com/bar.bed?param=2   => bar.bed
 * @param urlOrFile
 */

function getFilename$2(urlOrFile) {

    if (urlOrFile.name !== undefined) {
        return urlOrFile.name
    } else if (isString$3(urlOrFile)) {

        let index = urlOrFile.lastIndexOf("/");
        let filename = index < 0 ? urlOrFile : urlOrFile.substr(index + 1);

        //Strip parameters -- handle local files later
        index = filename.indexOf("?");
        if (index > 0) {
            filename = filename.substr(0, index);
        }
        return filename
    } else {
        throw Error(`Expected File or string, got ${typeof urlOrFile}`)
    }
}


/**
 * Test if object is a File or File-like object.
 *
 * @param object
 */
function isFile$1(object) {
    if(!object) {
        return false;
    }
    return typeof object !== 'function' &&
        (object instanceof File ||
            (object.hasOwnProperty("name") && typeof object.slice === 'function' && typeof object.arrayBuffer === 'function'))
}

function download$1(filename, data) {

    const element = document.createElement('a');
    element.setAttribute('href', data);
    element.setAttribute('download', filename);
    element.style.display = 'none';
    document.body.appendChild(element);
    element.click();
    document.body.removeChild(element);
}

if (typeof process === 'object' && typeof window === 'undefined') {
    global.atob = function (str) {
        return Buffer.from(str, 'base64').toString('binary');
    };
}


function parseUri$1(str) {

    var o = options$1,
        m = o.parser["loose"].exec(str),
        uri = {},
        i = 14;

    while (i--) uri[o.key[i]] = m[i] || "";

    uri[o.q.name] = {};
    uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
        if ($1) uri[o.q.name][$1] = $2;
    });

    return uri;
}

const options$1 = {
    strictMode: false,
    key: ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "anchor"],
    q: {
        name: "queryKey",
        parser: /(?:^|&)([^&=]*)=?([^&]*)/g
    },
    parser: {
        strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
        loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
    }
};

/**
 * Resolve a url, which might be a string, function (that returns a string or Promse), or Promise (that resolves to a string)
 *
 * @param url
 * @returns {Promise<*>}
 */
async function resolveURL(url) {
    return (typeof url === 'function')  ?  url() :  url;
}

/*! pako 2.1.0 https://github.com/nodeca/pako @license (MIT AND Zlib) */
// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

/* eslint-disable space-unary-ops */

/* Public constants ==========================================================*/
/* ===========================================================================*/


//const Z_FILTERED          = 1;
//const Z_HUFFMAN_ONLY      = 2;
//const Z_RLE               = 3;
const Z_FIXED$1$1               = 4;
//const Z_DEFAULT_STRATEGY  = 0;

/* Possible values of the data_type field (though see inflate()) */
const Z_BINARY$1              = 0;
const Z_TEXT$1                = 1;
//const Z_ASCII             = 1; // = Z_TEXT
const Z_UNKNOWN$1$1             = 2;

/*============================================================================*/


function zero$1$1(buf) { let len = buf.length; while (--len >= 0) { buf[len] = 0; } }

// From zutil.h

const STORED_BLOCK$1 = 0;
const STATIC_TREES$1 = 1;
const DYN_TREES$1    = 2;
/* The three kinds of block type */

const MIN_MATCH$1$1    = 3;
const MAX_MATCH$1$1    = 258;
/* The minimum and maximum match lengths */

// From deflate.h
/* ===========================================================================
 * Internal compression state.
 */

const LENGTH_CODES$1$1  = 29;
/* number of length codes, not counting the special END_BLOCK code */

const LITERALS$1$1      = 256;
/* number of literal bytes 0..255 */

const L_CODES$1$1       = LITERALS$1$1 + 1 + LENGTH_CODES$1$1;
/* number of Literal or Length codes, including the END_BLOCK code */

const D_CODES$1$1       = 30;
/* number of distance codes */

const BL_CODES$1$1      = 19;
/* number of codes used to transfer the bit lengths */

const HEAP_SIZE$1$1     = 2 * L_CODES$1$1 + 1;
/* maximum heap size */

const MAX_BITS$1$1      = 15;
/* All codes must not exceed MAX_BITS bits */

const Buf_size$1      = 16;
/* size of bit buffer in bi_buf */


/* ===========================================================================
 * Constants
 */

const MAX_BL_BITS$1 = 7;
/* Bit length codes must not exceed MAX_BL_BITS bits */

const END_BLOCK$1   = 256;
/* end of block literal code */

const REP_3_6$1     = 16;
/* repeat previous bit length 3-6 times (2 bits of repeat count) */

const REPZ_3_10$1   = 17;
/* repeat a zero length 3-10 times  (3 bits of repeat count) */

const REPZ_11_138$1 = 18;
/* repeat a zero length 11-138 times  (7 bits of repeat count) */

/* eslint-disable comma-spacing,array-bracket-spacing */
const extra_lbits$1 =   /* extra bits for each length code */
  new Uint8Array([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0]);

const extra_dbits$1 =   /* extra bits for each distance code */
  new Uint8Array([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13]);

const extra_blbits$1 =  /* extra bits for each bit length code */
  new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7]);

const bl_order$1 =
  new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]);
/* eslint-enable comma-spacing,array-bracket-spacing */

/* The lengths of the bit length codes are sent in order of decreasing
 * probability, to avoid transmitting the lengths for unused bit length codes.
 */

/* ===========================================================================
 * Local data. These are initialized only once.
 */

// We pre-fill arrays with 0 to avoid uninitialized gaps

const DIST_CODE_LEN$1 = 512; /* see definition of array dist_code below */

// !!!! Use flat array instead of structure, Freq = i*2, Len = i*2+1
const static_ltree$1  = new Array((L_CODES$1$1 + 2) * 2);
zero$1$1(static_ltree$1);
/* The static literal tree. Since the bit lengths are imposed, there is no
 * need for the L_CODES extra codes used during heap construction. However
 * The codes 286 and 287 are needed to build a canonical tree (see _tr_init
 * below).
 */

const static_dtree$1  = new Array(D_CODES$1$1 * 2);
zero$1$1(static_dtree$1);
/* The static distance tree. (Actually a trivial tree since all codes use
 * 5 bits.)
 */

const _dist_code$1    = new Array(DIST_CODE_LEN$1);
zero$1$1(_dist_code$1);
/* Distance codes. The first 256 values correspond to the distances
 * 3 .. 258, the last 256 values correspond to the top 8 bits of
 * the 15 bit distances.
 */

const _length_code$1  = new Array(MAX_MATCH$1$1 - MIN_MATCH$1$1 + 1);
zero$1$1(_length_code$1);
/* length code for each normalized match length (0 == MIN_MATCH) */

const base_length$1   = new Array(LENGTH_CODES$1$1);
zero$1$1(base_length$1);
/* First normalized length for each code (0 = MIN_MATCH) */

const base_dist$1     = new Array(D_CODES$1$1);
zero$1$1(base_dist$1);
/* First normalized distance for each code (0 = distance of 1) */


function StaticTreeDesc$1(static_tree, extra_bits, extra_base, elems, max_length) {

  this.static_tree  = static_tree;  /* static tree or NULL */
  this.extra_bits   = extra_bits;   /* extra bits for each code or NULL */
  this.extra_base   = extra_base;   /* base index for extra_bits */
  this.elems        = elems;        /* max number of elements in the tree */
  this.max_length   = max_length;   /* max bit length for the codes */

  // show if `static_tree` has data or dummy - needed for monomorphic objects
  this.has_stree    = static_tree && static_tree.length;
}


let static_l_desc$1;
let static_d_desc$1;
let static_bl_desc$1;


function TreeDesc$1(dyn_tree, stat_desc) {
  this.dyn_tree = dyn_tree;     /* the dynamic tree */
  this.max_code = 0;            /* largest code with non zero frequency */
  this.stat_desc = stat_desc;   /* the corresponding static tree */
}



const d_code$1 = (dist) => {

  return dist < 256 ? _dist_code$1[dist] : _dist_code$1[256 + (dist >>> 7)];
};


/* ===========================================================================
 * Output a short LSB first on the stream.
 * IN assertion: there is enough room in pendingBuf.
 */
const put_short$1 = (s, w) => {
//    put_byte(s, (uch)((w) & 0xff));
//    put_byte(s, (uch)((ush)(w) >> 8));
  s.pending_buf[s.pending++] = (w) & 0xff;
  s.pending_buf[s.pending++] = (w >>> 8) & 0xff;
};


/* ===========================================================================
 * Send a value on a given number of bits.
 * IN assertion: length <= 16 and value fits in length bits.
 */
const send_bits$1 = (s, value, length) => {

  if (s.bi_valid > (Buf_size$1 - length)) {
    s.bi_buf |= (value << s.bi_valid) & 0xffff;
    put_short$1(s, s.bi_buf);
    s.bi_buf = value >> (Buf_size$1 - s.bi_valid);
    s.bi_valid += length - Buf_size$1;
  } else {
    s.bi_buf |= (value << s.bi_valid) & 0xffff;
    s.bi_valid += length;
  }
};


const send_code$1 = (s, c, tree) => {

  send_bits$1(s, tree[c * 2]/*.Code*/, tree[c * 2 + 1]/*.Len*/);
};


/* ===========================================================================
 * Reverse the first len bits of a code, using straightforward code (a faster
 * method would use a table)
 * IN assertion: 1 <= len <= 15
 */
const bi_reverse$1 = (code, len) => {

  let res = 0;
  do {
    res |= code & 1;
    code >>>= 1;
    res <<= 1;
  } while (--len > 0);
  return res >>> 1;
};


/* ===========================================================================
 * Flush the bit buffer, keeping at most 7 bits in it.
 */
const bi_flush$1 = (s) => {

  if (s.bi_valid === 16) {
    put_short$1(s, s.bi_buf);
    s.bi_buf = 0;
    s.bi_valid = 0;

  } else if (s.bi_valid >= 8) {
    s.pending_buf[s.pending++] = s.bi_buf & 0xff;
    s.bi_buf >>= 8;
    s.bi_valid -= 8;
  }
};


/* ===========================================================================
 * Compute the optimal bit lengths for a tree and update the total bit length
 * for the current block.
 * IN assertion: the fields freq and dad are set, heap[heap_max] and
 *    above are the tree nodes sorted by increasing frequency.
 * OUT assertions: the field len is set to the optimal bit length, the
 *     array bl_count contains the frequencies for each bit length.
 *     The length opt_len is updated; static_len is also updated if stree is
 *     not null.
 */
const gen_bitlen$1 = (s, desc) => {
//    deflate_state *s;
//    tree_desc *desc;    /* the tree descriptor */

  const tree            = desc.dyn_tree;
  const max_code        = desc.max_code;
  const stree           = desc.stat_desc.static_tree;
  const has_stree       = desc.stat_desc.has_stree;
  const extra           = desc.stat_desc.extra_bits;
  const base            = desc.stat_desc.extra_base;
  const max_length      = desc.stat_desc.max_length;
  let h;              /* heap index */
  let n, m;           /* iterate over the tree elements */
  let bits;           /* bit length */
  let xbits;          /* extra bits */
  let f;              /* frequency */
  let overflow = 0;   /* number of elements with bit length too large */

  for (bits = 0; bits <= MAX_BITS$1$1; bits++) {
    s.bl_count[bits] = 0;
  }

  /* In a first pass, compute the optimal bit lengths (which may
   * overflow in the case of the bit length tree).
   */
  tree[s.heap[s.heap_max] * 2 + 1]/*.Len*/ = 0; /* root of the heap */

  for (h = s.heap_max + 1; h < HEAP_SIZE$1$1; h++) {
    n = s.heap[h];
    bits = tree[tree[n * 2 + 1]/*.Dad*/ * 2 + 1]/*.Len*/ + 1;
    if (bits > max_length) {
      bits = max_length;
      overflow++;
    }
    tree[n * 2 + 1]/*.Len*/ = bits;
    /* We overwrite tree[n].Dad which is no longer needed */

    if (n > max_code) { continue; } /* not a leaf node */

    s.bl_count[bits]++;
    xbits = 0;
    if (n >= base) {
      xbits = extra[n - base];
    }
    f = tree[n * 2]/*.Freq*/;
    s.opt_len += f * (bits + xbits);
    if (has_stree) {
      s.static_len += f * (stree[n * 2 + 1]/*.Len*/ + xbits);
    }
  }
  if (overflow === 0) { return; }

  // Tracev((stderr,"\nbit length overflow\n"));
  /* This happens for example on obj2 and pic of the Calgary corpus */

  /* Find the first bit length which could increase: */
  do {
    bits = max_length - 1;
    while (s.bl_count[bits] === 0) { bits--; }
    s.bl_count[bits]--;      /* move one leaf down the tree */
    s.bl_count[bits + 1] += 2; /* move one overflow item as its brother */
    s.bl_count[max_length]--;
    /* The brother of the overflow item also moves one step up,
     * but this does not affect bl_count[max_length]
     */
    overflow -= 2;
  } while (overflow > 0);

  /* Now recompute all bit lengths, scanning in increasing frequency.
   * h is still equal to HEAP_SIZE. (It is simpler to reconstruct all
   * lengths instead of fixing only the wrong ones. This idea is taken
   * from 'ar' written by Haruhiko Okumura.)
   */
  for (bits = max_length; bits !== 0; bits--) {
    n = s.bl_count[bits];
    while (n !== 0) {
      m = s.heap[--h];
      if (m > max_code) { continue; }
      if (tree[m * 2 + 1]/*.Len*/ !== bits) {
        // Tracev((stderr,"code %d bits %d->%d\n", m, tree[m].Len, bits));
        s.opt_len += (bits - tree[m * 2 + 1]/*.Len*/) * tree[m * 2]/*.Freq*/;
        tree[m * 2 + 1]/*.Len*/ = bits;
      }
      n--;
    }
  }
};


/* ===========================================================================
 * Generate the codes for a given tree and bit counts (which need not be
 * optimal).
 * IN assertion: the array bl_count contains the bit length statistics for
 * the given tree and the field len is set for all tree elements.
 * OUT assertion: the field code is set for all tree elements of non
 *     zero code length.
 */
const gen_codes$1 = (tree, max_code, bl_count) => {
//    ct_data *tree;             /* the tree to decorate */
//    int max_code;              /* largest code with non zero frequency */
//    ushf *bl_count;            /* number of codes at each bit length */

  const next_code = new Array(MAX_BITS$1$1 + 1); /* next code value for each bit length */
  let code = 0;              /* running code value */
  let bits;                  /* bit index */
  let n;                     /* code index */

  /* The distribution counts are first used to generate the code values
   * without bit reversal.
   */
  for (bits = 1; bits <= MAX_BITS$1$1; bits++) {
    code = (code + bl_count[bits - 1]) << 1;
    next_code[bits] = code;
  }
  /* Check that the bit counts in bl_count are consistent. The last code
   * must be all ones.
   */
  //Assert (code + bl_count[MAX_BITS]-1 == (1<<MAX_BITS)-1,
  //        "inconsistent bit counts");
  //Tracev((stderr,"\ngen_codes: max_code %d ", max_code));

  for (n = 0;  n <= max_code; n++) {
    let len = tree[n * 2 + 1]/*.Len*/;
    if (len === 0) { continue; }
    /* Now reverse the bits */
    tree[n * 2]/*.Code*/ = bi_reverse$1(next_code[len]++, len);

    //Tracecv(tree != static_ltree, (stderr,"\nn %3d %c l %2d c %4x (%x) ",
    //     n, (isgraph(n) ? n : ' '), len, tree[n].Code, next_code[len]-1));
  }
};


/* ===========================================================================
 * Initialize the various 'constant' tables.
 */
const tr_static_init$1 = () => {

  let n;        /* iterates over tree elements */
  let bits;     /* bit counter */
  let length;   /* length value */
  let code;     /* code value */
  let dist;     /* distance index */
  const bl_count = new Array(MAX_BITS$1$1 + 1);
  /* number of codes at each bit length for an optimal tree */

  // do check in _tr_init()
  //if (static_init_done) return;

  /* For some embedded targets, global variables are not initialized: */
/*#ifdef NO_INIT_GLOBAL_POINTERS
  static_l_desc.static_tree = static_ltree;
  static_l_desc.extra_bits = extra_lbits;
  static_d_desc.static_tree = static_dtree;
  static_d_desc.extra_bits = extra_dbits;
  static_bl_desc.extra_bits = extra_blbits;
#endif*/

  /* Initialize the mapping length (0..255) -> length code (0..28) */
  length = 0;
  for (code = 0; code < LENGTH_CODES$1$1 - 1; code++) {
    base_length$1[code] = length;
    for (n = 0; n < (1 << extra_lbits$1[code]); n++) {
      _length_code$1[length++] = code;
    }
  }
  //Assert (length == 256, "tr_static_init: length != 256");
  /* Note that the length 255 (match length 258) can be represented
   * in two different ways: code 284 + 5 bits or code 285, so we
   * overwrite length_code[255] to use the best encoding:
   */
  _length_code$1[length - 1] = code;

  /* Initialize the mapping dist (0..32K) -> dist code (0..29) */
  dist = 0;
  for (code = 0; code < 16; code++) {
    base_dist$1[code] = dist;
    for (n = 0; n < (1 << extra_dbits$1[code]); n++) {
      _dist_code$1[dist++] = code;
    }
  }
  //Assert (dist == 256, "tr_static_init: dist != 256");
  dist >>= 7; /* from now on, all distances are divided by 128 */
  for (; code < D_CODES$1$1; code++) {
    base_dist$1[code] = dist << 7;
    for (n = 0; n < (1 << (extra_dbits$1[code] - 7)); n++) {
      _dist_code$1[256 + dist++] = code;
    }
  }
  //Assert (dist == 256, "tr_static_init: 256+dist != 512");

  /* Construct the codes of the static literal tree */
  for (bits = 0; bits <= MAX_BITS$1$1; bits++) {
    bl_count[bits] = 0;
  }

  n = 0;
  while (n <= 143) {
    static_ltree$1[n * 2 + 1]/*.Len*/ = 8;
    n++;
    bl_count[8]++;
  }
  while (n <= 255) {
    static_ltree$1[n * 2 + 1]/*.Len*/ = 9;
    n++;
    bl_count[9]++;
  }
  while (n <= 279) {
    static_ltree$1[n * 2 + 1]/*.Len*/ = 7;
    n++;
    bl_count[7]++;
  }
  while (n <= 287) {
    static_ltree$1[n * 2 + 1]/*.Len*/ = 8;
    n++;
    bl_count[8]++;
  }
  /* Codes 286 and 287 do not exist, but we must include them in the
   * tree construction to get a canonical Huffman tree (longest code
   * all ones)
   */
  gen_codes$1(static_ltree$1, L_CODES$1$1 + 1, bl_count);

  /* The static distance tree is trivial: */
  for (n = 0; n < D_CODES$1$1; n++) {
    static_dtree$1[n * 2 + 1]/*.Len*/ = 5;
    static_dtree$1[n * 2]/*.Code*/ = bi_reverse$1(n, 5);
  }

  // Now data ready and we can init static trees
  static_l_desc$1 = new StaticTreeDesc$1(static_ltree$1, extra_lbits$1, LITERALS$1$1 + 1, L_CODES$1$1, MAX_BITS$1$1);
  static_d_desc$1 = new StaticTreeDesc$1(static_dtree$1, extra_dbits$1, 0,          D_CODES$1$1, MAX_BITS$1$1);
  static_bl_desc$1 = new StaticTreeDesc$1(new Array(0), extra_blbits$1, 0,         BL_CODES$1$1, MAX_BL_BITS$1);

  //static_init_done = true;
};


/* ===========================================================================
 * Initialize a new block.
 */
const init_block$1 = (s) => {

  let n; /* iterates over tree elements */

  /* Initialize the trees. */
  for (n = 0; n < L_CODES$1$1;  n++) { s.dyn_ltree[n * 2]/*.Freq*/ = 0; }
  for (n = 0; n < D_CODES$1$1;  n++) { s.dyn_dtree[n * 2]/*.Freq*/ = 0; }
  for (n = 0; n < BL_CODES$1$1; n++) { s.bl_tree[n * 2]/*.Freq*/ = 0; }

  s.dyn_ltree[END_BLOCK$1 * 2]/*.Freq*/ = 1;
  s.opt_len = s.static_len = 0;
  s.sym_next = s.matches = 0;
};


/* ===========================================================================
 * Flush the bit buffer and align the output on a byte boundary
 */
const bi_windup$1 = (s) =>
{
  if (s.bi_valid > 8) {
    put_short$1(s, s.bi_buf);
  } else if (s.bi_valid > 0) {
    //put_byte(s, (Byte)s->bi_buf);
    s.pending_buf[s.pending++] = s.bi_buf;
  }
  s.bi_buf = 0;
  s.bi_valid = 0;
};

/* ===========================================================================
 * Compares to subtrees, using the tree depth as tie breaker when
 * the subtrees have equal frequency. This minimizes the worst case length.
 */
const smaller$1 = (tree, n, m, depth) => {

  const _n2 = n * 2;
  const _m2 = m * 2;
  return (tree[_n2]/*.Freq*/ < tree[_m2]/*.Freq*/ ||
         (tree[_n2]/*.Freq*/ === tree[_m2]/*.Freq*/ && depth[n] <= depth[m]));
};

/* ===========================================================================
 * Restore the heap property by moving down the tree starting at node k,
 * exchanging a node with the smallest of its two sons if necessary, stopping
 * when the heap property is re-established (each father smaller than its
 * two sons).
 */
const pqdownheap$1 = (s, tree, k) => {
//    deflate_state *s;
//    ct_data *tree;  /* the tree to restore */
//    int k;               /* node to move down */

  const v = s.heap[k];
  let j = k << 1;  /* left son of k */
  while (j <= s.heap_len) {
    /* Set j to the smallest of the two sons: */
    if (j < s.heap_len &&
      smaller$1(tree, s.heap[j + 1], s.heap[j], s.depth)) {
      j++;
    }
    /* Exit if v is smaller than both sons */
    if (smaller$1(tree, v, s.heap[j], s.depth)) { break; }

    /* Exchange v with the smallest son */
    s.heap[k] = s.heap[j];
    k = j;

    /* And continue down the tree, setting j to the left son of k */
    j <<= 1;
  }
  s.heap[k] = v;
};


// inlined manually
// const SMALLEST = 1;

/* ===========================================================================
 * Send the block data compressed using the given Huffman trees
 */
const compress_block$1 = (s, ltree, dtree) => {
//    deflate_state *s;
//    const ct_data *ltree; /* literal tree */
//    const ct_data *dtree; /* distance tree */

  let dist;           /* distance of matched string */
  let lc;             /* match length or unmatched char (if dist == 0) */
  let sx = 0;         /* running index in sym_buf */
  let code;           /* the code to send */
  let extra;          /* number of extra bits to send */

  if (s.sym_next !== 0) {
    do {
      dist = s.pending_buf[s.sym_buf + sx++] & 0xff;
      dist += (s.pending_buf[s.sym_buf + sx++] & 0xff) << 8;
      lc = s.pending_buf[s.sym_buf + sx++];
      if (dist === 0) {
        send_code$1(s, lc, ltree); /* send a literal byte */
        //Tracecv(isgraph(lc), (stderr," '%c' ", lc));
      } else {
        /* Here, lc is the match length - MIN_MATCH */
        code = _length_code$1[lc];
        send_code$1(s, code + LITERALS$1$1 + 1, ltree); /* send the length code */
        extra = extra_lbits$1[code];
        if (extra !== 0) {
          lc -= base_length$1[code];
          send_bits$1(s, lc, extra);       /* send the extra length bits */
        }
        dist--; /* dist is now the match distance - 1 */
        code = d_code$1(dist);
        //Assert (code < D_CODES, "bad d_code");

        send_code$1(s, code, dtree);       /* send the distance code */
        extra = extra_dbits$1[code];
        if (extra !== 0) {
          dist -= base_dist$1[code];
          send_bits$1(s, dist, extra);   /* send the extra distance bits */
        }
      } /* literal or match pair ? */

      /* Check that the overlay between pending_buf and sym_buf is ok: */
      //Assert(s->pending < s->lit_bufsize + sx, "pendingBuf overflow");

    } while (sx < s.sym_next);
  }

  send_code$1(s, END_BLOCK$1, ltree);
};


/* ===========================================================================
 * Construct one Huffman tree and assigns the code bit strings and lengths.
 * Update the total bit length for the current block.
 * IN assertion: the field freq is set for all tree elements.
 * OUT assertions: the fields len and code are set to the optimal bit length
 *     and corresponding code. The length opt_len is updated; static_len is
 *     also updated if stree is not null. The field max_code is set.
 */
const build_tree$1 = (s, desc) => {
//    deflate_state *s;
//    tree_desc *desc; /* the tree descriptor */

  const tree     = desc.dyn_tree;
  const stree    = desc.stat_desc.static_tree;
  const has_stree = desc.stat_desc.has_stree;
  const elems    = desc.stat_desc.elems;
  let n, m;          /* iterate over heap elements */
  let max_code = -1; /* largest code with non zero frequency */
  let node;          /* new node being created */

  /* Construct the initial heap, with least frequent element in
   * heap[SMALLEST]. The sons of heap[n] are heap[2*n] and heap[2*n+1].
   * heap[0] is not used.
   */
  s.heap_len = 0;
  s.heap_max = HEAP_SIZE$1$1;

  for (n = 0; n < elems; n++) {
    if (tree[n * 2]/*.Freq*/ !== 0) {
      s.heap[++s.heap_len] = max_code = n;
      s.depth[n] = 0;

    } else {
      tree[n * 2 + 1]/*.Len*/ = 0;
    }
  }

  /* The pkzip format requires that at least one distance code exists,
   * and that at least one bit should be sent even if there is only one
   * possible code. So to avoid special checks later on we force at least
   * two codes of non zero frequency.
   */
  while (s.heap_len < 2) {
    node = s.heap[++s.heap_len] = (max_code < 2 ? ++max_code : 0);
    tree[node * 2]/*.Freq*/ = 1;
    s.depth[node] = 0;
    s.opt_len--;

    if (has_stree) {
      s.static_len -= stree[node * 2 + 1]/*.Len*/;
    }
    /* node is 0 or 1 so it does not have extra bits */
  }
  desc.max_code = max_code;

  /* The elements heap[heap_len/2+1 .. heap_len] are leaves of the tree,
   * establish sub-heaps of increasing lengths:
   */
  for (n = (s.heap_len >> 1/*int /2*/); n >= 1; n--) { pqdownheap$1(s, tree, n); }

  /* Construct the Huffman tree by repeatedly combining the least two
   * frequent nodes.
   */
  node = elems;              /* next internal node of the tree */
  do {
    //pqremove(s, tree, n);  /* n = node of least frequency */
    /*** pqremove ***/
    n = s.heap[1/*SMALLEST*/];
    s.heap[1/*SMALLEST*/] = s.heap[s.heap_len--];
    pqdownheap$1(s, tree, 1/*SMALLEST*/);
    /***/

    m = s.heap[1/*SMALLEST*/]; /* m = node of next least frequency */

    s.heap[--s.heap_max] = n; /* keep the nodes sorted by frequency */
    s.heap[--s.heap_max] = m;

    /* Create a new node father of n and m */
    tree[node * 2]/*.Freq*/ = tree[n * 2]/*.Freq*/ + tree[m * 2]/*.Freq*/;
    s.depth[node] = (s.depth[n] >= s.depth[m] ? s.depth[n] : s.depth[m]) + 1;
    tree[n * 2 + 1]/*.Dad*/ = tree[m * 2 + 1]/*.Dad*/ = node;

    /* and insert the new node in the heap */
    s.heap[1/*SMALLEST*/] = node++;
    pqdownheap$1(s, tree, 1/*SMALLEST*/);

  } while (s.heap_len >= 2);

  s.heap[--s.heap_max] = s.heap[1/*SMALLEST*/];

  /* At this point, the fields freq and dad are set. We can now
   * generate the bit lengths.
   */
  gen_bitlen$1(s, desc);

  /* The field len is now set, we can generate the bit codes */
  gen_codes$1(tree, max_code, s.bl_count);
};


/* ===========================================================================
 * Scan a literal or distance tree to determine the frequencies of the codes
 * in the bit length tree.
 */
const scan_tree$1 = (s, tree, max_code) => {
//    deflate_state *s;
//    ct_data *tree;   /* the tree to be scanned */
//    int max_code;    /* and its largest code of non zero frequency */

  let n;                     /* iterates over all tree elements */
  let prevlen = -1;          /* last emitted length */
  let curlen;                /* length of current code */

  let nextlen = tree[0 * 2 + 1]/*.Len*/; /* length of next code */

  let count = 0;             /* repeat count of the current code */
  let max_count = 7;         /* max repeat count */
  let min_count = 4;         /* min repeat count */

  if (nextlen === 0) {
    max_count = 138;
    min_count = 3;
  }
  tree[(max_code + 1) * 2 + 1]/*.Len*/ = 0xffff; /* guard */

  for (n = 0; n <= max_code; n++) {
    curlen = nextlen;
    nextlen = tree[(n + 1) * 2 + 1]/*.Len*/;

    if (++count < max_count && curlen === nextlen) {
      continue;

    } else if (count < min_count) {
      s.bl_tree[curlen * 2]/*.Freq*/ += count;

    } else if (curlen !== 0) {

      if (curlen !== prevlen) { s.bl_tree[curlen * 2]/*.Freq*/++; }
      s.bl_tree[REP_3_6$1 * 2]/*.Freq*/++;

    } else if (count <= 10) {
      s.bl_tree[REPZ_3_10$1 * 2]/*.Freq*/++;

    } else {
      s.bl_tree[REPZ_11_138$1 * 2]/*.Freq*/++;
    }

    count = 0;
    prevlen = curlen;

    if (nextlen === 0) {
      max_count = 138;
      min_count = 3;

    } else if (curlen === nextlen) {
      max_count = 6;
      min_count = 3;

    } else {
      max_count = 7;
      min_count = 4;
    }
  }
};


/* ===========================================================================
 * Send a literal or distance tree in compressed form, using the codes in
 * bl_tree.
 */
const send_tree$1 = (s, tree, max_code) => {
//    deflate_state *s;
//    ct_data *tree; /* the tree to be scanned */
//    int max_code;       /* and its largest code of non zero frequency */

  let n;                     /* iterates over all tree elements */
  let prevlen = -1;          /* last emitted length */
  let curlen;                /* length of current code */

  let nextlen = tree[0 * 2 + 1]/*.Len*/; /* length of next code */

  let count = 0;             /* repeat count of the current code */
  let max_count = 7;         /* max repeat count */
  let min_count = 4;         /* min repeat count */

  /* tree[max_code+1].Len = -1; */  /* guard already set */
  if (nextlen === 0) {
    max_count = 138;
    min_count = 3;
  }

  for (n = 0; n <= max_code; n++) {
    curlen = nextlen;
    nextlen = tree[(n + 1) * 2 + 1]/*.Len*/;

    if (++count < max_count && curlen === nextlen) {
      continue;

    } else if (count < min_count) {
      do { send_code$1(s, curlen, s.bl_tree); } while (--count !== 0);

    } else if (curlen !== 0) {
      if (curlen !== prevlen) {
        send_code$1(s, curlen, s.bl_tree);
        count--;
      }
      //Assert(count >= 3 && count <= 6, " 3_6?");
      send_code$1(s, REP_3_6$1, s.bl_tree);
      send_bits$1(s, count - 3, 2);

    } else if (count <= 10) {
      send_code$1(s, REPZ_3_10$1, s.bl_tree);
      send_bits$1(s, count - 3, 3);

    } else {
      send_code$1(s, REPZ_11_138$1, s.bl_tree);
      send_bits$1(s, count - 11, 7);
    }

    count = 0;
    prevlen = curlen;
    if (nextlen === 0) {
      max_count = 138;
      min_count = 3;

    } else if (curlen === nextlen) {
      max_count = 6;
      min_count = 3;

    } else {
      max_count = 7;
      min_count = 4;
    }
  }
};


/* ===========================================================================
 * Construct the Huffman tree for the bit lengths and return the index in
 * bl_order of the last bit length code to send.
 */
const build_bl_tree$1 = (s) => {

  let max_blindex;  /* index of last bit length code of non zero freq */

  /* Determine the bit length frequencies for literal and distance trees */
  scan_tree$1(s, s.dyn_ltree, s.l_desc.max_code);
  scan_tree$1(s, s.dyn_dtree, s.d_desc.max_code);

  /* Build the bit length tree: */
  build_tree$1(s, s.bl_desc);
  /* opt_len now includes the length of the tree representations, except
   * the lengths of the bit lengths codes and the 5+5+4 bits for the counts.
   */

  /* Determine the number of bit length codes to send. The pkzip format
   * requires that at least 4 bit length codes be sent. (appnote.txt says
   * 3 but the actual value used is 4.)
   */
  for (max_blindex = BL_CODES$1$1 - 1; max_blindex >= 3; max_blindex--) {
    if (s.bl_tree[bl_order$1[max_blindex] * 2 + 1]/*.Len*/ !== 0) {
      break;
    }
  }
  /* Update opt_len to include the bit length tree and counts */
  s.opt_len += 3 * (max_blindex + 1) + 5 + 5 + 4;
  //Tracev((stderr, "\ndyn trees: dyn %ld, stat %ld",
  //        s->opt_len, s->static_len));

  return max_blindex;
};


/* ===========================================================================
 * Send the header for a block using dynamic Huffman trees: the counts, the
 * lengths of the bit length codes, the literal tree and the distance tree.
 * IN assertion: lcodes >= 257, dcodes >= 1, blcodes >= 4.
 */
const send_all_trees$1 = (s, lcodes, dcodes, blcodes) => {
//    deflate_state *s;
//    int lcodes, dcodes, blcodes; /* number of codes for each tree */

  let rank;                    /* index in bl_order */

  //Assert (lcodes >= 257 && dcodes >= 1 && blcodes >= 4, "not enough codes");
  //Assert (lcodes <= L_CODES && dcodes <= D_CODES && blcodes <= BL_CODES,
  //        "too many codes");
  //Tracev((stderr, "\nbl counts: "));
  send_bits$1(s, lcodes - 257, 5); /* not +255 as stated in appnote.txt */
  send_bits$1(s, dcodes - 1,   5);
  send_bits$1(s, blcodes - 4,  4); /* not -3 as stated in appnote.txt */
  for (rank = 0; rank < blcodes; rank++) {
    //Tracev((stderr, "\nbl code %2d ", bl_order[rank]));
    send_bits$1(s, s.bl_tree[bl_order$1[rank] * 2 + 1]/*.Len*/, 3);
  }
  //Tracev((stderr, "\nbl tree: sent %ld", s->bits_sent));

  send_tree$1(s, s.dyn_ltree, lcodes - 1); /* literal tree */
  //Tracev((stderr, "\nlit tree: sent %ld", s->bits_sent));

  send_tree$1(s, s.dyn_dtree, dcodes - 1); /* distance tree */
  //Tracev((stderr, "\ndist tree: sent %ld", s->bits_sent));
};


/* ===========================================================================
 * Check if the data type is TEXT or BINARY, using the following algorithm:
 * - TEXT if the two conditions below are satisfied:
 *    a) There are no non-portable control characters belonging to the
 *       "block list" (0..6, 14..25, 28..31).
 *    b) There is at least one printable character belonging to the
 *       "allow list" (9 {TAB}, 10 {LF}, 13 {CR}, 32..255).
 * - BINARY otherwise.
 * - The following partially-portable control characters form a
 *   "gray list" that is ignored in this detection algorithm:
 *   (7 {BEL}, 8 {BS}, 11 {VT}, 12 {FF}, 26 {SUB}, 27 {ESC}).
 * IN assertion: the fields Freq of dyn_ltree are set.
 */
const detect_data_type$1 = (s) => {
  /* block_mask is the bit mask of block-listed bytes
   * set bits 0..6, 14..25, and 28..31
   * 0xf3ffc07f = binary 11110011111111111100000001111111
   */
  let block_mask = 0xf3ffc07f;
  let n;

  /* Check for non-textual ("block-listed") bytes. */
  for (n = 0; n <= 31; n++, block_mask >>>= 1) {
    if ((block_mask & 1) && (s.dyn_ltree[n * 2]/*.Freq*/ !== 0)) {
      return Z_BINARY$1;
    }
  }

  /* Check for textual ("allow-listed") bytes. */
  if (s.dyn_ltree[9 * 2]/*.Freq*/ !== 0 || s.dyn_ltree[10 * 2]/*.Freq*/ !== 0 ||
      s.dyn_ltree[13 * 2]/*.Freq*/ !== 0) {
    return Z_TEXT$1;
  }
  for (n = 32; n < LITERALS$1$1; n++) {
    if (s.dyn_ltree[n * 2]/*.Freq*/ !== 0) {
      return Z_TEXT$1;
    }
  }

  /* There are no "block-listed" or "allow-listed" bytes:
   * this stream either is empty or has tolerated ("gray-listed") bytes only.
   */
  return Z_BINARY$1;
};


let static_init_done$1 = false;

/* ===========================================================================
 * Initialize the tree data structures for a new zlib stream.
 */
const _tr_init$1$1 = (s) =>
{

  if (!static_init_done$1) {
    tr_static_init$1();
    static_init_done$1 = true;
  }

  s.l_desc  = new TreeDesc$1(s.dyn_ltree, static_l_desc$1);
  s.d_desc  = new TreeDesc$1(s.dyn_dtree, static_d_desc$1);
  s.bl_desc = new TreeDesc$1(s.bl_tree, static_bl_desc$1);

  s.bi_buf = 0;
  s.bi_valid = 0;

  /* Initialize the first block of the first file: */
  init_block$1(s);
};


/* ===========================================================================
 * Send a stored block
 */
const _tr_stored_block$1$1 = (s, buf, stored_len, last) => {
//DeflateState *s;
//charf *buf;       /* input block */
//ulg stored_len;   /* length of input block */
//int last;         /* one if this is the last block for a file */

  send_bits$1(s, (STORED_BLOCK$1 << 1) + (last ? 1 : 0), 3);    /* send block type */
  bi_windup$1(s);        /* align on byte boundary */
  put_short$1(s, stored_len);
  put_short$1(s, ~stored_len);
  if (stored_len) {
    s.pending_buf.set(s.window.subarray(buf, buf + stored_len), s.pending);
  }
  s.pending += stored_len;
};


/* ===========================================================================
 * Send one empty static block to give enough lookahead for inflate.
 * This takes 10 bits, of which 7 may remain in the bit buffer.
 */
const _tr_align$1$1 = (s) => {
  send_bits$1(s, STATIC_TREES$1 << 1, 3);
  send_code$1(s, END_BLOCK$1, static_ltree$1);
  bi_flush$1(s);
};


/* ===========================================================================
 * Determine the best encoding for the current block: dynamic trees, static
 * trees or store, and write out the encoded block.
 */
const _tr_flush_block$1$1 = (s, buf, stored_len, last) => {
//DeflateState *s;
//charf *buf;       /* input block, or NULL if too old */
//ulg stored_len;   /* length of input block */
//int last;         /* one if this is the last block for a file */

  let opt_lenb, static_lenb;  /* opt_len and static_len in bytes */
  let max_blindex = 0;        /* index of last bit length code of non zero freq */

  /* Build the Huffman trees unless a stored block is forced */
  if (s.level > 0) {

    /* Check if the file is binary or text */
    if (s.strm.data_type === Z_UNKNOWN$1$1) {
      s.strm.data_type = detect_data_type$1(s);
    }

    /* Construct the literal and distance trees */
    build_tree$1(s, s.l_desc);
    // Tracev((stderr, "\nlit data: dyn %ld, stat %ld", s->opt_len,
    //        s->static_len));

    build_tree$1(s, s.d_desc);
    // Tracev((stderr, "\ndist data: dyn %ld, stat %ld", s->opt_len,
    //        s->static_len));
    /* At this point, opt_len and static_len are the total bit lengths of
     * the compressed block data, excluding the tree representations.
     */

    /* Build the bit length tree for the above two trees, and get the index
     * in bl_order of the last bit length code to send.
     */
    max_blindex = build_bl_tree$1(s);

    /* Determine the best encoding. Compute the block lengths in bytes. */
    opt_lenb = (s.opt_len + 3 + 7) >>> 3;
    static_lenb = (s.static_len + 3 + 7) >>> 3;

    // Tracev((stderr, "\nopt %lu(%lu) stat %lu(%lu) stored %lu lit %u ",
    //        opt_lenb, s->opt_len, static_lenb, s->static_len, stored_len,
    //        s->sym_next / 3));

    if (static_lenb <= opt_lenb) { opt_lenb = static_lenb; }

  } else {
    // Assert(buf != (char*)0, "lost buf");
    opt_lenb = static_lenb = stored_len + 5; /* force a stored block */
  }

  if ((stored_len + 4 <= opt_lenb) && (buf !== -1)) {
    /* 4: two words for the lengths */

    /* The test buf != NULL is only necessary if LIT_BUFSIZE > WSIZE.
     * Otherwise we can't have processed more than WSIZE input bytes since
     * the last block flush, because compression would have been
     * successful. If LIT_BUFSIZE <= WSIZE, it is never too late to
     * transform a block into a stored block.
     */
    _tr_stored_block$1$1(s, buf, stored_len, last);

  } else if (s.strategy === Z_FIXED$1$1 || static_lenb === opt_lenb) {

    send_bits$1(s, (STATIC_TREES$1 << 1) + (last ? 1 : 0), 3);
    compress_block$1(s, static_ltree$1, static_dtree$1);

  } else {
    send_bits$1(s, (DYN_TREES$1 << 1) + (last ? 1 : 0), 3);
    send_all_trees$1(s, s.l_desc.max_code + 1, s.d_desc.max_code + 1, max_blindex + 1);
    compress_block$1(s, s.dyn_ltree, s.dyn_dtree);
  }
  // Assert (s->compressed_len == s->bits_sent, "bad compressed size");
  /* The above check is made mod 2^32, for files larger than 512 MB
   * and uLong implemented on 32 bits.
   */
  init_block$1(s);

  if (last) {
    bi_windup$1(s);
  }
  // Tracev((stderr,"\ncomprlen %lu(%lu) ", s->compressed_len>>3,
  //       s->compressed_len-7*last));
};

/* ===========================================================================
 * Save the match info and tally the frequency counts. Return true if
 * the current block must be flushed.
 */
const _tr_tally$1$1 = (s, dist, lc) => {
//    deflate_state *s;
//    unsigned dist;  /* distance of matched string */
//    unsigned lc;    /* match length-MIN_MATCH or unmatched char (if dist==0) */

  s.pending_buf[s.sym_buf + s.sym_next++] = dist;
  s.pending_buf[s.sym_buf + s.sym_next++] = dist >> 8;
  s.pending_buf[s.sym_buf + s.sym_next++] = lc;
  if (dist === 0) {
    /* lc is the unmatched char */
    s.dyn_ltree[lc * 2]/*.Freq*/++;
  } else {
    s.matches++;
    /* Here, lc is the match length - MIN_MATCH */
    dist--;             /* dist = match distance - 1 */
    //Assert((ush)dist < (ush)MAX_DIST(s) &&
    //       (ush)lc <= (ush)(MAX_MATCH-MIN_MATCH) &&
    //       (ush)d_code(dist) < (ush)D_CODES,  "_tr_tally: bad match");

    s.dyn_ltree[(_length_code$1[lc] + LITERALS$1$1 + 1) * 2]/*.Freq*/++;
    s.dyn_dtree[d_code$1(dist) * 2]/*.Freq*/++;
  }

  return (s.sym_next === s.sym_end);
};

var _tr_init_1$1  = _tr_init$1$1;
var _tr_stored_block_1$1 = _tr_stored_block$1$1;
var _tr_flush_block_1$1  = _tr_flush_block$1$1;
var _tr_tally_1$1 = _tr_tally$1$1;
var _tr_align_1$1 = _tr_align$1$1;

var trees$1 = {
	_tr_init: _tr_init_1$1,
	_tr_stored_block: _tr_stored_block_1$1,
	_tr_flush_block: _tr_flush_block_1$1,
	_tr_tally: _tr_tally_1$1,
	_tr_align: _tr_align_1$1
};

// Note: adler32 takes 12% for level 0 and 2% for level 6.
// It isn't worth it to make additional optimizations as in original.
// Small size is preferable.

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

const adler32$1 = (adler, buf, len, pos) => {
  let s1 = (adler & 0xffff) |0,
      s2 = ((adler >>> 16) & 0xffff) |0,
      n = 0;

  while (len !== 0) {
    // Set limit ~ twice less than 5552, to keep
    // s2 in 31-bits, because we force signed ints.
    // in other case %= will fail.
    n = len > 2000 ? 2000 : len;
    len -= n;

    do {
      s1 = (s1 + buf[pos++]) |0;
      s2 = (s2 + s1) |0;
    } while (--n);

    s1 %= 65521;
    s2 %= 65521;
  }

  return (s1 | (s2 << 16)) |0;
};


var adler32_1$1 = adler32$1;

// Note: we can't get significant speed boost here.
// So write code to minimize size - no pregenerated tables
// and array tools dependencies.

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

// Use ordinary array, since untyped makes no boost here
const makeTable$1 = () => {
  let c, table = [];

  for (var n = 0; n < 256; n++) {
    c = n;
    for (var k = 0; k < 8; k++) {
      c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
    }
    table[n] = c;
  }

  return table;
};

// Create table on load. Just 255 signed longs. Not a problem.
const crcTable$1 = new Uint32Array(makeTable$1());


const crc32$1 = (crc, buf, len, pos) => {
  const t = crcTable$1;
  const end = pos + len;

  crc ^= -1;

  for (let i = pos; i < end; i++) {
    crc = (crc >>> 8) ^ t[(crc ^ buf[i]) & 0xFF];
  }

  return (crc ^ (-1)); // >>> 0;
};


var crc32_1$1 = crc32$1;

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

var messages$1 = {
  2:      'need dictionary',     /* Z_NEED_DICT       2  */
  1:      'stream end',          /* Z_STREAM_END      1  */
  0:      '',                    /* Z_OK              0  */
  '-1':   'file error',          /* Z_ERRNO         (-1) */
  '-2':   'stream error',        /* Z_STREAM_ERROR  (-2) */
  '-3':   'data error',          /* Z_DATA_ERROR    (-3) */
  '-4':   'insufficient memory', /* Z_MEM_ERROR     (-4) */
  '-5':   'buffer error',        /* Z_BUF_ERROR     (-5) */
  '-6':   'incompatible version' /* Z_VERSION_ERROR (-6) */
};

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

var constants$2$1 = {

  /* Allowed flush values; see deflate() and inflate() below for details */
  Z_NO_FLUSH:         0,
  Z_PARTIAL_FLUSH:    1,
  Z_SYNC_FLUSH:       2,
  Z_FULL_FLUSH:       3,
  Z_FINISH:           4,
  Z_BLOCK:            5,
  Z_TREES:            6,

  /* Return codes for the compression/decompression functions. Negative values
  * are errors, positive values are used for special but normal events.
  */
  Z_OK:               0,
  Z_STREAM_END:       1,
  Z_NEED_DICT:        2,
  Z_ERRNO:           -1,
  Z_STREAM_ERROR:    -2,
  Z_DATA_ERROR:      -3,
  Z_MEM_ERROR:       -4,
  Z_BUF_ERROR:       -5,
  //Z_VERSION_ERROR: -6,

  /* compression levels */
  Z_NO_COMPRESSION:         0,
  Z_BEST_SPEED:             1,
  Z_BEST_COMPRESSION:       9,
  Z_DEFAULT_COMPRESSION:   -1,


  Z_FILTERED:               1,
  Z_HUFFMAN_ONLY:           2,
  Z_RLE:                    3,
  Z_FIXED:                  4,
  Z_DEFAULT_STRATEGY:       0,

  /* Possible values of the data_type field (though see inflate()) */
  Z_BINARY:                 0,
  Z_TEXT:                   1,
  //Z_ASCII:                1, // = Z_TEXT (deprecated)
  Z_UNKNOWN:                2,

  /* The deflate compression method */
  Z_DEFLATED:               8
  //Z_NULL:                 null // Use -1 or null inline, depending on var type
};

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

const { _tr_init: _tr_init$2, _tr_stored_block: _tr_stored_block$2, _tr_flush_block: _tr_flush_block$2, _tr_tally: _tr_tally$2, _tr_align: _tr_align$2 } = trees$1;




/* Public constants ==========================================================*/
/* ===========================================================================*/

const {
  Z_NO_FLUSH: Z_NO_FLUSH$2$1, Z_PARTIAL_FLUSH: Z_PARTIAL_FLUSH$1, Z_FULL_FLUSH: Z_FULL_FLUSH$1$1, Z_FINISH: Z_FINISH$3$1, Z_BLOCK: Z_BLOCK$1$1,
  Z_OK: Z_OK$3$1, Z_STREAM_END: Z_STREAM_END$3$1, Z_STREAM_ERROR: Z_STREAM_ERROR$2$1, Z_DATA_ERROR: Z_DATA_ERROR$2$1, Z_BUF_ERROR: Z_BUF_ERROR$1$1,
  Z_DEFAULT_COMPRESSION: Z_DEFAULT_COMPRESSION$1$1,
  Z_FILTERED: Z_FILTERED$1, Z_HUFFMAN_ONLY: Z_HUFFMAN_ONLY$1, Z_RLE: Z_RLE$1, Z_FIXED: Z_FIXED$2, Z_DEFAULT_STRATEGY: Z_DEFAULT_STRATEGY$1$1,
  Z_UNKNOWN: Z_UNKNOWN$2,
  Z_DEFLATED: Z_DEFLATED$2$1
} = constants$2$1;

/*============================================================================*/


const MAX_MEM_LEVEL$1 = 9;
/* Maximum value for memLevel in deflateInit2 */
const MAX_WBITS$1$1 = 15;
/* 32K LZ77 window */
const DEF_MEM_LEVEL$1 = 8;


const LENGTH_CODES$2  = 29;
/* number of length codes, not counting the special END_BLOCK code */
const LITERALS$2      = 256;
/* number of literal bytes 0..255 */
const L_CODES$2       = LITERALS$2 + 1 + LENGTH_CODES$2;
/* number of Literal or Length codes, including the END_BLOCK code */
const D_CODES$2       = 30;
/* number of distance codes */
const BL_CODES$2      = 19;
/* number of codes used to transfer the bit lengths */
const HEAP_SIZE$2     = 2 * L_CODES$2 + 1;
/* maximum heap size */
const MAX_BITS$2  = 15;
/* All codes must not exceed MAX_BITS bits */

const MIN_MATCH$2 = 3;
const MAX_MATCH$2 = 258;
const MIN_LOOKAHEAD$1 = (MAX_MATCH$2 + MIN_MATCH$2 + 1);

const PRESET_DICT$1 = 0x20;

const INIT_STATE$1    =  42;    /* zlib header -> BUSY_STATE */
//#ifdef GZIP
const GZIP_STATE$1    =  57;    /* gzip header -> BUSY_STATE | EXTRA_STATE */
//#endif
const EXTRA_STATE$1   =  69;    /* gzip extra block -> NAME_STATE */
const NAME_STATE$1    =  73;    /* gzip file name -> COMMENT_STATE */
const COMMENT_STATE$1 =  91;    /* gzip comment -> HCRC_STATE */
const HCRC_STATE$1    = 103;    /* gzip header CRC -> BUSY_STATE */
const BUSY_STATE$1    = 113;    /* deflate -> FINISH_STATE */
const FINISH_STATE$1  = 666;    /* stream complete */

const BS_NEED_MORE$1      = 1; /* block not completed, need more input or more output */
const BS_BLOCK_DONE$1     = 2; /* block flush performed */
const BS_FINISH_STARTED$1 = 3; /* finish started, need only more output at next deflate */
const BS_FINISH_DONE$1    = 4; /* finish done, accept no more input or output */

const OS_CODE$1 = 0x03; // Unix :) . Don't detect, use this default.

const err$1 = (strm, errorCode) => {
  strm.msg = messages$1[errorCode];
  return errorCode;
};

const rank$1 = (f) => {
  return ((f) * 2) - ((f) > 4 ? 9 : 0);
};

const zero$2 = (buf) => {
  let len = buf.length; while (--len >= 0) { buf[len] = 0; }
};

/* ===========================================================================
 * Slide the hash table when sliding the window down (could be avoided with 32
 * bit values at the expense of memory usage). We slide even when level == 0 to
 * keep the hash table consistent if we switch back to level > 0 later.
 */
const slide_hash$1 = (s) => {
  let n, m;
  let p;
  let wsize = s.w_size;

  n = s.hash_size;
  p = n;
  do {
    m = s.head[--p];
    s.head[p] = (m >= wsize ? m - wsize : 0);
  } while (--n);
  n = wsize;
//#ifndef FASTEST
  p = n;
  do {
    m = s.prev[--p];
    s.prev[p] = (m >= wsize ? m - wsize : 0);
    /* If n is not on any hash chain, prev[n] is garbage but
     * its value will never be used.
     */
  } while (--n);
//#endif
};

/* eslint-disable new-cap */
let HASH_ZLIB$1 = (s, prev, data) => ((prev << s.hash_shift) ^ data) & s.hash_mask;
// This hash causes less collisions, https://github.com/nodeca/pako/issues/135
// But breaks binary compatibility
//let HASH_FAST = (s, prev, data) => ((prev << 8) + (prev >> 8) + (data << 4)) & s.hash_mask;
let HASH$1 = HASH_ZLIB$1;


/* =========================================================================
 * Flush as much pending output as possible. All deflate() output, except for
 * some deflate_stored() output, goes through this function so some
 * applications may wish to modify it to avoid allocating a large
 * strm->next_out buffer and copying into it. (See also read_buf()).
 */
const flush_pending$1 = (strm) => {
  const s = strm.state;

  //_tr_flush_bits(s);
  let len = s.pending;
  if (len > strm.avail_out) {
    len = strm.avail_out;
  }
  if (len === 0) { return; }

  strm.output.set(s.pending_buf.subarray(s.pending_out, s.pending_out + len), strm.next_out);
  strm.next_out  += len;
  s.pending_out  += len;
  strm.total_out += len;
  strm.avail_out -= len;
  s.pending      -= len;
  if (s.pending === 0) {
    s.pending_out = 0;
  }
};


const flush_block_only$1 = (s, last) => {
  _tr_flush_block$2(s, (s.block_start >= 0 ? s.block_start : -1), s.strstart - s.block_start, last);
  s.block_start = s.strstart;
  flush_pending$1(s.strm);
};


const put_byte$1 = (s, b) => {
  s.pending_buf[s.pending++] = b;
};


/* =========================================================================
 * Put a short in the pending buffer. The 16-bit value is put in MSB order.
 * IN assertion: the stream state is correct and there is enough room in
 * pending_buf.
 */
const putShortMSB$1 = (s, b) => {

  //  put_byte(s, (Byte)(b >> 8));
//  put_byte(s, (Byte)(b & 0xff));
  s.pending_buf[s.pending++] = (b >>> 8) & 0xff;
  s.pending_buf[s.pending++] = b & 0xff;
};


/* ===========================================================================
 * Read a new buffer from the current input stream, update the adler32
 * and total number of bytes read.  All deflate() input goes through
 * this function so some applications may wish to modify it to avoid
 * allocating a large strm->input buffer and copying from it.
 * (See also flush_pending()).
 */
const read_buf$1 = (strm, buf, start, size) => {

  let len = strm.avail_in;

  if (len > size) { len = size; }
  if (len === 0) { return 0; }

  strm.avail_in -= len;

  // zmemcpy(buf, strm->next_in, len);
  buf.set(strm.input.subarray(strm.next_in, strm.next_in + len), start);
  if (strm.state.wrap === 1) {
    strm.adler = adler32_1$1(strm.adler, buf, len, start);
  }

  else if (strm.state.wrap === 2) {
    strm.adler = crc32_1$1(strm.adler, buf, len, start);
  }

  strm.next_in += len;
  strm.total_in += len;

  return len;
};


/* ===========================================================================
 * Set match_start to the longest match starting at the given string and
 * return its length. Matches shorter or equal to prev_length are discarded,
 * in which case the result is equal to prev_length and match_start is
 * garbage.
 * IN assertions: cur_match is the head of the hash chain for the current
 *   string (strstart) and its distance is <= MAX_DIST, and prev_length >= 1
 * OUT assertion: the match length is not greater than s->lookahead.
 */
const longest_match$1 = (s, cur_match) => {

  let chain_length = s.max_chain_length;      /* max hash chain length */
  let scan = s.strstart; /* current string */
  let match;                       /* matched string */
  let len;                           /* length of current match */
  let best_len = s.prev_length;              /* best match length so far */
  let nice_match = s.nice_match;             /* stop if match long enough */
  const limit = (s.strstart > (s.w_size - MIN_LOOKAHEAD$1)) ?
      s.strstart - (s.w_size - MIN_LOOKAHEAD$1) : 0/*NIL*/;

  const _win = s.window; // shortcut

  const wmask = s.w_mask;
  const prev  = s.prev;

  /* Stop when cur_match becomes <= limit. To simplify the code,
   * we prevent matches with the string of window index 0.
   */

  const strend = s.strstart + MAX_MATCH$2;
  let scan_end1  = _win[scan + best_len - 1];
  let scan_end   = _win[scan + best_len];

  /* The code is optimized for HASH_BITS >= 8 and MAX_MATCH-2 multiple of 16.
   * It is easy to get rid of this optimization if necessary.
   */
  // Assert(s->hash_bits >= 8 && MAX_MATCH == 258, "Code too clever");

  /* Do not waste too much time if we already have a good match: */
  if (s.prev_length >= s.good_match) {
    chain_length >>= 2;
  }
  /* Do not look for matches beyond the end of the input. This is necessary
   * to make deflate deterministic.
   */
  if (nice_match > s.lookahead) { nice_match = s.lookahead; }

  // Assert((ulg)s->strstart <= s->window_size-MIN_LOOKAHEAD, "need lookahead");

  do {
    // Assert(cur_match < s->strstart, "no future");
    match = cur_match;

    /* Skip to next match if the match length cannot increase
     * or if the match length is less than 2.  Note that the checks below
     * for insufficient lookahead only occur occasionally for performance
     * reasons.  Therefore uninitialized memory will be accessed, and
     * conditional jumps will be made that depend on those values.
     * However the length of the match is limited to the lookahead, so
     * the output of deflate is not affected by the uninitialized values.
     */

    if (_win[match + best_len]     !== scan_end  ||
        _win[match + best_len - 1] !== scan_end1 ||
        _win[match]                !== _win[scan] ||
        _win[++match]              !== _win[scan + 1]) {
      continue;
    }

    /* The check at best_len-1 can be removed because it will be made
     * again later. (This heuristic is not always a win.)
     * It is not necessary to compare scan[2] and match[2] since they
     * are always equal when the other bytes match, given that
     * the hash keys are equal and that HASH_BITS >= 8.
     */
    scan += 2;
    match++;
    // Assert(*scan == *match, "match[2]?");

    /* We check for insufficient lookahead only every 8th comparison;
     * the 256th check will be made at strstart+258.
     */
    do {
      /*jshint noempty:false*/
    } while (_win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&
             _win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&
             _win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&
             _win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&
             scan < strend);

    // Assert(scan <= s->window+(unsigned)(s->window_size-1), "wild scan");

    len = MAX_MATCH$2 - (strend - scan);
    scan = strend - MAX_MATCH$2;

    if (len > best_len) {
      s.match_start = cur_match;
      best_len = len;
      if (len >= nice_match) {
        break;
      }
      scan_end1  = _win[scan + best_len - 1];
      scan_end   = _win[scan + best_len];
    }
  } while ((cur_match = prev[cur_match & wmask]) > limit && --chain_length !== 0);

  if (best_len <= s.lookahead) {
    return best_len;
  }
  return s.lookahead;
};


/* ===========================================================================
 * Fill the window when the lookahead becomes insufficient.
 * Updates strstart and lookahead.
 *
 * IN assertion: lookahead < MIN_LOOKAHEAD
 * OUT assertions: strstart <= window_size-MIN_LOOKAHEAD
 *    At least one byte has been read, or avail_in == 0; reads are
 *    performed for at least two bytes (required for the zip translate_eol
 *    option -- not supported here).
 */
const fill_window$1 = (s) => {

  const _w_size = s.w_size;
  let n, more, str;

  //Assert(s->lookahead < MIN_LOOKAHEAD, "already enough lookahead");

  do {
    more = s.window_size - s.lookahead - s.strstart;

    // JS ints have 32 bit, block below not needed
    /* Deal with !@#$% 64K limit: */
    //if (sizeof(int) <= 2) {
    //    if (more == 0 && s->strstart == 0 && s->lookahead == 0) {
    //        more = wsize;
    //
    //  } else if (more == (unsigned)(-1)) {
    //        /* Very unlikely, but possible on 16 bit machine if
    //         * strstart == 0 && lookahead == 1 (input done a byte at time)
    //         */
    //        more--;
    //    }
    //}


    /* If the window is almost full and there is insufficient lookahead,
     * move the upper half to the lower one to make room in the upper half.
     */
    if (s.strstart >= _w_size + (_w_size - MIN_LOOKAHEAD$1)) {

      s.window.set(s.window.subarray(_w_size, _w_size + _w_size - more), 0);
      s.match_start -= _w_size;
      s.strstart -= _w_size;
      /* we now have strstart >= MAX_DIST */
      s.block_start -= _w_size;
      if (s.insert > s.strstart) {
        s.insert = s.strstart;
      }
      slide_hash$1(s);
      more += _w_size;
    }
    if (s.strm.avail_in === 0) {
      break;
    }

    /* If there was no sliding:
     *    strstart <= WSIZE+MAX_DIST-1 && lookahead <= MIN_LOOKAHEAD - 1 &&
     *    more == window_size - lookahead - strstart
     * => more >= window_size - (MIN_LOOKAHEAD-1 + WSIZE + MAX_DIST-1)
     * => more >= window_size - 2*WSIZE + 2
     * In the BIG_MEM or MMAP case (not yet supported),
     *   window_size == input_size + MIN_LOOKAHEAD  &&
     *   strstart + s->lookahead <= input_size => more >= MIN_LOOKAHEAD.
     * Otherwise, window_size == 2*WSIZE so more >= 2.
     * If there was sliding, more >= WSIZE. So in all cases, more >= 2.
     */
    //Assert(more >= 2, "more < 2");
    n = read_buf$1(s.strm, s.window, s.strstart + s.lookahead, more);
    s.lookahead += n;

    /* Initialize the hash value now that we have some input: */
    if (s.lookahead + s.insert >= MIN_MATCH$2) {
      str = s.strstart - s.insert;
      s.ins_h = s.window[str];

      /* UPDATE_HASH(s, s->ins_h, s->window[str + 1]); */
      s.ins_h = HASH$1(s, s.ins_h, s.window[str + 1]);
//#if MIN_MATCH != 3
//        Call update_hash() MIN_MATCH-3 more times
//#endif
      while (s.insert) {
        /* UPDATE_HASH(s, s->ins_h, s->window[str + MIN_MATCH-1]); */
        s.ins_h = HASH$1(s, s.ins_h, s.window[str + MIN_MATCH$2 - 1]);

        s.prev[str & s.w_mask] = s.head[s.ins_h];
        s.head[s.ins_h] = str;
        str++;
        s.insert--;
        if (s.lookahead + s.insert < MIN_MATCH$2) {
          break;
        }
      }
    }
    /* If the whole input has less than MIN_MATCH bytes, ins_h is garbage,
     * but this is not important since only literal bytes will be emitted.
     */

  } while (s.lookahead < MIN_LOOKAHEAD$1 && s.strm.avail_in !== 0);

  /* If the WIN_INIT bytes after the end of the current data have never been
   * written, then zero those bytes in order to avoid memory check reports of
   * the use of uninitialized (or uninitialised as Julian writes) bytes by
   * the longest match routines.  Update the high water mark for the next
   * time through here.  WIN_INIT is set to MAX_MATCH since the longest match
   * routines allow scanning to strstart + MAX_MATCH, ignoring lookahead.
   */
//  if (s.high_water < s.window_size) {
//    const curr = s.strstart + s.lookahead;
//    let init = 0;
//
//    if (s.high_water < curr) {
//      /* Previous high water mark below current data -- zero WIN_INIT
//       * bytes or up to end of window, whichever is less.
//       */
//      init = s.window_size - curr;
//      if (init > WIN_INIT)
//        init = WIN_INIT;
//      zmemzero(s->window + curr, (unsigned)init);
//      s->high_water = curr + init;
//    }
//    else if (s->high_water < (ulg)curr + WIN_INIT) {
//      /* High water mark at or above current data, but below current data
//       * plus WIN_INIT -- zero out to current data plus WIN_INIT, or up
//       * to end of window, whichever is less.
//       */
//      init = (ulg)curr + WIN_INIT - s->high_water;
//      if (init > s->window_size - s->high_water)
//        init = s->window_size - s->high_water;
//      zmemzero(s->window + s->high_water, (unsigned)init);
//      s->high_water += init;
//    }
//  }
//
//  Assert((ulg)s->strstart <= s->window_size - MIN_LOOKAHEAD,
//    "not enough room for search");
};

/* ===========================================================================
 * Copy without compression as much as possible from the input stream, return
 * the current block state.
 *
 * In case deflateParams() is used to later switch to a non-zero compression
 * level, s->matches (otherwise unused when storing) keeps track of the number
 * of hash table slides to perform. If s->matches is 1, then one hash table
 * slide will be done when switching. If s->matches is 2, the maximum value
 * allowed here, then the hash table will be cleared, since two or more slides
 * is the same as a clear.
 *
 * deflate_stored() is written to minimize the number of times an input byte is
 * copied. It is most efficient with large input and output buffers, which
 * maximizes the opportunites to have a single copy from next_in to next_out.
 */
const deflate_stored$1 = (s, flush) => {

  /* Smallest worthy block size when not flushing or finishing. By default
   * this is 32K. This can be as small as 507 bytes for memLevel == 1. For
   * large input and output buffers, the stored block size will be larger.
   */
  let min_block = s.pending_buf_size - 5 > s.w_size ? s.w_size : s.pending_buf_size - 5;

  /* Copy as many min_block or larger stored blocks directly to next_out as
   * possible. If flushing, copy the remaining available input to next_out as
   * stored blocks, if there is enough space.
   */
  let len, left, have, last = 0;
  let used = s.strm.avail_in;
  do {
    /* Set len to the maximum size block that we can copy directly with the
     * available input data and output space. Set left to how much of that
     * would be copied from what's left in the window.
     */
    len = 65535/* MAX_STORED */;     /* maximum deflate stored block length */
    have = (s.bi_valid + 42) >> 3;     /* number of header bytes */
    if (s.strm.avail_out < have) {         /* need room for header */
      break;
    }
      /* maximum stored block length that will fit in avail_out: */
    have = s.strm.avail_out - have;
    left = s.strstart - s.block_start;  /* bytes left in window */
    if (len > left + s.strm.avail_in) {
      len = left + s.strm.avail_in;   /* limit len to the input */
    }
    if (len > have) {
      len = have;             /* limit len to the output */
    }

    /* If the stored block would be less than min_block in length, or if
     * unable to copy all of the available input when flushing, then try
     * copying to the window and the pending buffer instead. Also don't
     * write an empty block when flushing -- deflate() does that.
     */
    if (len < min_block && ((len === 0 && flush !== Z_FINISH$3$1) ||
                        flush === Z_NO_FLUSH$2$1 ||
                        len !== left + s.strm.avail_in)) {
      break;
    }

    /* Make a dummy stored block in pending to get the header bytes,
     * including any pending bits. This also updates the debugging counts.
     */
    last = flush === Z_FINISH$3$1 && len === left + s.strm.avail_in ? 1 : 0;
    _tr_stored_block$2(s, 0, 0, last);

    /* Replace the lengths in the dummy stored block with len. */
    s.pending_buf[s.pending - 4] = len;
    s.pending_buf[s.pending - 3] = len >> 8;
    s.pending_buf[s.pending - 2] = ~len;
    s.pending_buf[s.pending - 1] = ~len >> 8;

    /* Write the stored block header bytes. */
    flush_pending$1(s.strm);

//#ifdef ZLIB_DEBUG
//    /* Update debugging counts for the data about to be copied. */
//    s->compressed_len += len << 3;
//    s->bits_sent += len << 3;
//#endif

    /* Copy uncompressed bytes from the window to next_out. */
    if (left) {
      if (left > len) {
        left = len;
      }
      //zmemcpy(s->strm->next_out, s->window + s->block_start, left);
      s.strm.output.set(s.window.subarray(s.block_start, s.block_start + left), s.strm.next_out);
      s.strm.next_out += left;
      s.strm.avail_out -= left;
      s.strm.total_out += left;
      s.block_start += left;
      len -= left;
    }

    /* Copy uncompressed bytes directly from next_in to next_out, updating
     * the check value.
     */
    if (len) {
      read_buf$1(s.strm, s.strm.output, s.strm.next_out, len);
      s.strm.next_out += len;
      s.strm.avail_out -= len;
      s.strm.total_out += len;
    }
  } while (last === 0);

  /* Update the sliding window with the last s->w_size bytes of the copied
   * data, or append all of the copied data to the existing window if less
   * than s->w_size bytes were copied. Also update the number of bytes to
   * insert in the hash tables, in the event that deflateParams() switches to
   * a non-zero compression level.
   */
  used -= s.strm.avail_in;    /* number of input bytes directly copied */
  if (used) {
    /* If any input was used, then no unused input remains in the window,
     * therefore s->block_start == s->strstart.
     */
    if (used >= s.w_size) {  /* supplant the previous history */
      s.matches = 2;     /* clear hash */
      //zmemcpy(s->window, s->strm->next_in - s->w_size, s->w_size);
      s.window.set(s.strm.input.subarray(s.strm.next_in - s.w_size, s.strm.next_in), 0);
      s.strstart = s.w_size;
      s.insert = s.strstart;
    }
    else {
      if (s.window_size - s.strstart <= used) {
        /* Slide the window down. */
        s.strstart -= s.w_size;
        //zmemcpy(s->window, s->window + s->w_size, s->strstart);
        s.window.set(s.window.subarray(s.w_size, s.w_size + s.strstart), 0);
        if (s.matches < 2) {
          s.matches++;   /* add a pending slide_hash() */
        }
        if (s.insert > s.strstart) {
          s.insert = s.strstart;
        }
      }
      //zmemcpy(s->window + s->strstart, s->strm->next_in - used, used);
      s.window.set(s.strm.input.subarray(s.strm.next_in - used, s.strm.next_in), s.strstart);
      s.strstart += used;
      s.insert += used > s.w_size - s.insert ? s.w_size - s.insert : used;
    }
    s.block_start = s.strstart;
  }
  if (s.high_water < s.strstart) {
    s.high_water = s.strstart;
  }

  /* If the last block was written to next_out, then done. */
  if (last) {
    return BS_FINISH_DONE$1;
  }

  /* If flushing and all input has been consumed, then done. */
  if (flush !== Z_NO_FLUSH$2$1 && flush !== Z_FINISH$3$1 &&
    s.strm.avail_in === 0 && s.strstart === s.block_start) {
    return BS_BLOCK_DONE$1;
  }

  /* Fill the window with any remaining input. */
  have = s.window_size - s.strstart;
  if (s.strm.avail_in > have && s.block_start >= s.w_size) {
    /* Slide the window down. */
    s.block_start -= s.w_size;
    s.strstart -= s.w_size;
    //zmemcpy(s->window, s->window + s->w_size, s->strstart);
    s.window.set(s.window.subarray(s.w_size, s.w_size + s.strstart), 0);
    if (s.matches < 2) {
      s.matches++;       /* add a pending slide_hash() */
    }
    have += s.w_size;      /* more space now */
    if (s.insert > s.strstart) {
      s.insert = s.strstart;
    }
  }
  if (have > s.strm.avail_in) {
    have = s.strm.avail_in;
  }
  if (have) {
    read_buf$1(s.strm, s.window, s.strstart, have);
    s.strstart += have;
    s.insert += have > s.w_size - s.insert ? s.w_size - s.insert : have;
  }
  if (s.high_water < s.strstart) {
    s.high_water = s.strstart;
  }

  /* There was not enough avail_out to write a complete worthy or flushed
   * stored block to next_out. Write a stored block to pending instead, if we
   * have enough input for a worthy block, or if flushing and there is enough
   * room for the remaining input as a stored block in the pending buffer.
   */
  have = (s.bi_valid + 42) >> 3;     /* number of header bytes */
    /* maximum stored block length that will fit in pending: */
  have = s.pending_buf_size - have > 65535/* MAX_STORED */ ? 65535/* MAX_STORED */ : s.pending_buf_size - have;
  min_block = have > s.w_size ? s.w_size : have;
  left = s.strstart - s.block_start;
  if (left >= min_block ||
     ((left || flush === Z_FINISH$3$1) && flush !== Z_NO_FLUSH$2$1 &&
     s.strm.avail_in === 0 && left <= have)) {
    len = left > have ? have : left;
    last = flush === Z_FINISH$3$1 && s.strm.avail_in === 0 &&
         len === left ? 1 : 0;
    _tr_stored_block$2(s, s.block_start, len, last);
    s.block_start += len;
    flush_pending$1(s.strm);
  }

  /* We've done all we can with the available input and output. */
  return last ? BS_FINISH_STARTED$1 : BS_NEED_MORE$1;
};


/* ===========================================================================
 * Compress as much as possible from the input stream, return the current
 * block state.
 * This function does not perform lazy evaluation of matches and inserts
 * new strings in the dictionary only for unmatched strings or for short
 * matches. It is used only for the fast compression options.
 */
const deflate_fast$1 = (s, flush) => {

  let hash_head;        /* head of the hash chain */
  let bflush;           /* set if current block must be flushed */

  for (;;) {
    /* Make sure that we always have enough lookahead, except
     * at the end of the input file. We need MAX_MATCH bytes
     * for the next match, plus MIN_MATCH bytes to insert the
     * string following the next match.
     */
    if (s.lookahead < MIN_LOOKAHEAD$1) {
      fill_window$1(s);
      if (s.lookahead < MIN_LOOKAHEAD$1 && flush === Z_NO_FLUSH$2$1) {
        return BS_NEED_MORE$1;
      }
      if (s.lookahead === 0) {
        break; /* flush the current block */
      }
    }

    /* Insert the string window[strstart .. strstart+2] in the
     * dictionary, and set hash_head to the head of the hash chain:
     */
    hash_head = 0/*NIL*/;
    if (s.lookahead >= MIN_MATCH$2) {
      /*** INSERT_STRING(s, s.strstart, hash_head); ***/
      s.ins_h = HASH$1(s, s.ins_h, s.window[s.strstart + MIN_MATCH$2 - 1]);
      hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];
      s.head[s.ins_h] = s.strstart;
      /***/
    }

    /* Find the longest match, discarding those <= prev_length.
     * At this point we have always match_length < MIN_MATCH
     */
    if (hash_head !== 0/*NIL*/ && ((s.strstart - hash_head) <= (s.w_size - MIN_LOOKAHEAD$1))) {
      /* To simplify the code, we prevent matches with the string
       * of window index 0 (in particular we have to avoid a match
       * of the string with itself at the start of the input file).
       */
      s.match_length = longest_match$1(s, hash_head);
      /* longest_match() sets match_start */
    }
    if (s.match_length >= MIN_MATCH$2) {
      // check_match(s, s.strstart, s.match_start, s.match_length); // for debug only

      /*** _tr_tally_dist(s, s.strstart - s.match_start,
                     s.match_length - MIN_MATCH, bflush); ***/
      bflush = _tr_tally$2(s, s.strstart - s.match_start, s.match_length - MIN_MATCH$2);

      s.lookahead -= s.match_length;

      /* Insert new strings in the hash table only if the match length
       * is not too large. This saves time but degrades compression.
       */
      if (s.match_length <= s.max_lazy_match/*max_insert_length*/ && s.lookahead >= MIN_MATCH$2) {
        s.match_length--; /* string at strstart already in table */
        do {
          s.strstart++;
          /*** INSERT_STRING(s, s.strstart, hash_head); ***/
          s.ins_h = HASH$1(s, s.ins_h, s.window[s.strstart + MIN_MATCH$2 - 1]);
          hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];
          s.head[s.ins_h] = s.strstart;
          /***/
          /* strstart never exceeds WSIZE-MAX_MATCH, so there are
           * always MIN_MATCH bytes ahead.
           */
        } while (--s.match_length !== 0);
        s.strstart++;
      } else
      {
        s.strstart += s.match_length;
        s.match_length = 0;
        s.ins_h = s.window[s.strstart];
        /* UPDATE_HASH(s, s.ins_h, s.window[s.strstart+1]); */
        s.ins_h = HASH$1(s, s.ins_h, s.window[s.strstart + 1]);

//#if MIN_MATCH != 3
//                Call UPDATE_HASH() MIN_MATCH-3 more times
//#endif
        /* If lookahead < MIN_MATCH, ins_h is garbage, but it does not
         * matter since it will be recomputed at next deflate call.
         */
      }
    } else {
      /* No match, output a literal byte */
      //Tracevv((stderr,"%c", s.window[s.strstart]));
      /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/
      bflush = _tr_tally$2(s, 0, s.window[s.strstart]);

      s.lookahead--;
      s.strstart++;
    }
    if (bflush) {
      /*** FLUSH_BLOCK(s, 0); ***/
      flush_block_only$1(s, false);
      if (s.strm.avail_out === 0) {
        return BS_NEED_MORE$1;
      }
      /***/
    }
  }
  s.insert = ((s.strstart < (MIN_MATCH$2 - 1)) ? s.strstart : MIN_MATCH$2 - 1);
  if (flush === Z_FINISH$3$1) {
    /*** FLUSH_BLOCK(s, 1); ***/
    flush_block_only$1(s, true);
    if (s.strm.avail_out === 0) {
      return BS_FINISH_STARTED$1;
    }
    /***/
    return BS_FINISH_DONE$1;
  }
  if (s.sym_next) {
    /*** FLUSH_BLOCK(s, 0); ***/
    flush_block_only$1(s, false);
    if (s.strm.avail_out === 0) {
      return BS_NEED_MORE$1;
    }
    /***/
  }
  return BS_BLOCK_DONE$1;
};

/* ===========================================================================
 * Same as above, but achieves better compression. We use a lazy
 * evaluation for matches: a match is finally adopted only if there is
 * no better match at the next window position.
 */
const deflate_slow$1 = (s, flush) => {

  let hash_head;          /* head of hash chain */
  let bflush;              /* set if current block must be flushed */

  let max_insert;

  /* Process the input block. */
  for (;;) {
    /* Make sure that we always have enough lookahead, except
     * at the end of the input file. We need MAX_MATCH bytes
     * for the next match, plus MIN_MATCH bytes to insert the
     * string following the next match.
     */
    if (s.lookahead < MIN_LOOKAHEAD$1) {
      fill_window$1(s);
      if (s.lookahead < MIN_LOOKAHEAD$1 && flush === Z_NO_FLUSH$2$1) {
        return BS_NEED_MORE$1;
      }
      if (s.lookahead === 0) { break; } /* flush the current block */
    }

    /* Insert the string window[strstart .. strstart+2] in the
     * dictionary, and set hash_head to the head of the hash chain:
     */
    hash_head = 0/*NIL*/;
    if (s.lookahead >= MIN_MATCH$2) {
      /*** INSERT_STRING(s, s.strstart, hash_head); ***/
      s.ins_h = HASH$1(s, s.ins_h, s.window[s.strstart + MIN_MATCH$2 - 1]);
      hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];
      s.head[s.ins_h] = s.strstart;
      /***/
    }

    /* Find the longest match, discarding those <= prev_length.
     */
    s.prev_length = s.match_length;
    s.prev_match = s.match_start;
    s.match_length = MIN_MATCH$2 - 1;

    if (hash_head !== 0/*NIL*/ && s.prev_length < s.max_lazy_match &&
        s.strstart - hash_head <= (s.w_size - MIN_LOOKAHEAD$1)/*MAX_DIST(s)*/) {
      /* To simplify the code, we prevent matches with the string
       * of window index 0 (in particular we have to avoid a match
       * of the string with itself at the start of the input file).
       */
      s.match_length = longest_match$1(s, hash_head);
      /* longest_match() sets match_start */

      if (s.match_length <= 5 &&
         (s.strategy === Z_FILTERED$1 || (s.match_length === MIN_MATCH$2 && s.strstart - s.match_start > 4096/*TOO_FAR*/))) {

        /* If prev_match is also MIN_MATCH, match_start is garbage
         * but we will ignore the current match anyway.
         */
        s.match_length = MIN_MATCH$2 - 1;
      }
    }
    /* If there was a match at the previous step and the current
     * match is not better, output the previous match:
     */
    if (s.prev_length >= MIN_MATCH$2 && s.match_length <= s.prev_length) {
      max_insert = s.strstart + s.lookahead - MIN_MATCH$2;
      /* Do not insert strings in hash table beyond this. */

      //check_match(s, s.strstart-1, s.prev_match, s.prev_length);

      /***_tr_tally_dist(s, s.strstart - 1 - s.prev_match,
                     s.prev_length - MIN_MATCH, bflush);***/
      bflush = _tr_tally$2(s, s.strstart - 1 - s.prev_match, s.prev_length - MIN_MATCH$2);
      /* Insert in hash table all strings up to the end of the match.
       * strstart-1 and strstart are already inserted. If there is not
       * enough lookahead, the last two strings are not inserted in
       * the hash table.
       */
      s.lookahead -= s.prev_length - 1;
      s.prev_length -= 2;
      do {
        if (++s.strstart <= max_insert) {
          /*** INSERT_STRING(s, s.strstart, hash_head); ***/
          s.ins_h = HASH$1(s, s.ins_h, s.window[s.strstart + MIN_MATCH$2 - 1]);
          hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];
          s.head[s.ins_h] = s.strstart;
          /***/
        }
      } while (--s.prev_length !== 0);
      s.match_available = 0;
      s.match_length = MIN_MATCH$2 - 1;
      s.strstart++;

      if (bflush) {
        /*** FLUSH_BLOCK(s, 0); ***/
        flush_block_only$1(s, false);
        if (s.strm.avail_out === 0) {
          return BS_NEED_MORE$1;
        }
        /***/
      }

    } else if (s.match_available) {
      /* If there was no match at the previous position, output a
       * single literal. If there was a match but the current match
       * is longer, truncate the previous match to a single literal.
       */
      //Tracevv((stderr,"%c", s->window[s->strstart-1]));
      /*** _tr_tally_lit(s, s.window[s.strstart-1], bflush); ***/
      bflush = _tr_tally$2(s, 0, s.window[s.strstart - 1]);

      if (bflush) {
        /*** FLUSH_BLOCK_ONLY(s, 0) ***/
        flush_block_only$1(s, false);
        /***/
      }
      s.strstart++;
      s.lookahead--;
      if (s.strm.avail_out === 0) {
        return BS_NEED_MORE$1;
      }
    } else {
      /* There is no previous match to compare with, wait for
       * the next step to decide.
       */
      s.match_available = 1;
      s.strstart++;
      s.lookahead--;
    }
  }
  //Assert (flush != Z_NO_FLUSH, "no flush?");
  if (s.match_available) {
    //Tracevv((stderr,"%c", s->window[s->strstart-1]));
    /*** _tr_tally_lit(s, s.window[s.strstart-1], bflush); ***/
    bflush = _tr_tally$2(s, 0, s.window[s.strstart - 1]);

    s.match_available = 0;
  }
  s.insert = s.strstart < MIN_MATCH$2 - 1 ? s.strstart : MIN_MATCH$2 - 1;
  if (flush === Z_FINISH$3$1) {
    /*** FLUSH_BLOCK(s, 1); ***/
    flush_block_only$1(s, true);
    if (s.strm.avail_out === 0) {
      return BS_FINISH_STARTED$1;
    }
    /***/
    return BS_FINISH_DONE$1;
  }
  if (s.sym_next) {
    /*** FLUSH_BLOCK(s, 0); ***/
    flush_block_only$1(s, false);
    if (s.strm.avail_out === 0) {
      return BS_NEED_MORE$1;
    }
    /***/
  }

  return BS_BLOCK_DONE$1;
};


/* ===========================================================================
 * For Z_RLE, simply look for runs of bytes, generate matches only of distance
 * one.  Do not maintain a hash table.  (It will be regenerated if this run of
 * deflate switches away from Z_RLE.)
 */
const deflate_rle$1 = (s, flush) => {

  let bflush;            /* set if current block must be flushed */
  let prev;              /* byte at distance one to match */
  let scan, strend;      /* scan goes up to strend for length of run */

  const _win = s.window;

  for (;;) {
    /* Make sure that we always have enough lookahead, except
     * at the end of the input file. We need MAX_MATCH bytes
     * for the longest run, plus one for the unrolled loop.
     */
    if (s.lookahead <= MAX_MATCH$2) {
      fill_window$1(s);
      if (s.lookahead <= MAX_MATCH$2 && flush === Z_NO_FLUSH$2$1) {
        return BS_NEED_MORE$1;
      }
      if (s.lookahead === 0) { break; } /* flush the current block */
    }

    /* See how many times the previous byte repeats */
    s.match_length = 0;
    if (s.lookahead >= MIN_MATCH$2 && s.strstart > 0) {
      scan = s.strstart - 1;
      prev = _win[scan];
      if (prev === _win[++scan] && prev === _win[++scan] && prev === _win[++scan]) {
        strend = s.strstart + MAX_MATCH$2;
        do {
          /*jshint noempty:false*/
        } while (prev === _win[++scan] && prev === _win[++scan] &&
                 prev === _win[++scan] && prev === _win[++scan] &&
                 prev === _win[++scan] && prev === _win[++scan] &&
                 prev === _win[++scan] && prev === _win[++scan] &&
                 scan < strend);
        s.match_length = MAX_MATCH$2 - (strend - scan);
        if (s.match_length > s.lookahead) {
          s.match_length = s.lookahead;
        }
      }
      //Assert(scan <= s->window+(uInt)(s->window_size-1), "wild scan");
    }

    /* Emit match if have run of MIN_MATCH or longer, else emit literal */
    if (s.match_length >= MIN_MATCH$2) {
      //check_match(s, s.strstart, s.strstart - 1, s.match_length);

      /*** _tr_tally_dist(s, 1, s.match_length - MIN_MATCH, bflush); ***/
      bflush = _tr_tally$2(s, 1, s.match_length - MIN_MATCH$2);

      s.lookahead -= s.match_length;
      s.strstart += s.match_length;
      s.match_length = 0;
    } else {
      /* No match, output a literal byte */
      //Tracevv((stderr,"%c", s->window[s->strstart]));
      /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/
      bflush = _tr_tally$2(s, 0, s.window[s.strstart]);

      s.lookahead--;
      s.strstart++;
    }
    if (bflush) {
      /*** FLUSH_BLOCK(s, 0); ***/
      flush_block_only$1(s, false);
      if (s.strm.avail_out === 0) {
        return BS_NEED_MORE$1;
      }
      /***/
    }
  }
  s.insert = 0;
  if (flush === Z_FINISH$3$1) {
    /*** FLUSH_BLOCK(s, 1); ***/
    flush_block_only$1(s, true);
    if (s.strm.avail_out === 0) {
      return BS_FINISH_STARTED$1;
    }
    /***/
    return BS_FINISH_DONE$1;
  }
  if (s.sym_next) {
    /*** FLUSH_BLOCK(s, 0); ***/
    flush_block_only$1(s, false);
    if (s.strm.avail_out === 0) {
      return BS_NEED_MORE$1;
    }
    /***/
  }
  return BS_BLOCK_DONE$1;
};

/* ===========================================================================
 * For Z_HUFFMAN_ONLY, do not look for matches.  Do not maintain a hash table.
 * (It will be regenerated if this run of deflate switches away from Huffman.)
 */
const deflate_huff$1 = (s, flush) => {

  let bflush;             /* set if current block must be flushed */

  for (;;) {
    /* Make sure that we have a literal to write. */
    if (s.lookahead === 0) {
      fill_window$1(s);
      if (s.lookahead === 0) {
        if (flush === Z_NO_FLUSH$2$1) {
          return BS_NEED_MORE$1;
        }
        break;      /* flush the current block */
      }
    }

    /* Output a literal byte */
    s.match_length = 0;
    //Tracevv((stderr,"%c", s->window[s->strstart]));
    /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/
    bflush = _tr_tally$2(s, 0, s.window[s.strstart]);
    s.lookahead--;
    s.strstart++;
    if (bflush) {
      /*** FLUSH_BLOCK(s, 0); ***/
      flush_block_only$1(s, false);
      if (s.strm.avail_out === 0) {
        return BS_NEED_MORE$1;
      }
      /***/
    }
  }
  s.insert = 0;
  if (flush === Z_FINISH$3$1) {
    /*** FLUSH_BLOCK(s, 1); ***/
    flush_block_only$1(s, true);
    if (s.strm.avail_out === 0) {
      return BS_FINISH_STARTED$1;
    }
    /***/
    return BS_FINISH_DONE$1;
  }
  if (s.sym_next) {
    /*** FLUSH_BLOCK(s, 0); ***/
    flush_block_only$1(s, false);
    if (s.strm.avail_out === 0) {
      return BS_NEED_MORE$1;
    }
    /***/
  }
  return BS_BLOCK_DONE$1;
};

/* Values for max_lazy_match, good_match and max_chain_length, depending on
 * the desired pack level (0..9). The values given below have been tuned to
 * exclude worst case performance for pathological files. Better values may be
 * found for specific files.
 */
function Config$1(good_length, max_lazy, nice_length, max_chain, func) {

  this.good_length = good_length;
  this.max_lazy = max_lazy;
  this.nice_length = nice_length;
  this.max_chain = max_chain;
  this.func = func;
}

const configuration_table$1 = [
  /*      good lazy nice chain */
  new Config$1(0, 0, 0, 0, deflate_stored$1),          /* 0 store only */
  new Config$1(4, 4, 8, 4, deflate_fast$1),            /* 1 max speed, no lazy matches */
  new Config$1(4, 5, 16, 8, deflate_fast$1),           /* 2 */
  new Config$1(4, 6, 32, 32, deflate_fast$1),          /* 3 */

  new Config$1(4, 4, 16, 16, deflate_slow$1),          /* 4 lazy matches */
  new Config$1(8, 16, 32, 32, deflate_slow$1),         /* 5 */
  new Config$1(8, 16, 128, 128, deflate_slow$1),       /* 6 */
  new Config$1(8, 32, 128, 256, deflate_slow$1),       /* 7 */
  new Config$1(32, 128, 258, 1024, deflate_slow$1),    /* 8 */
  new Config$1(32, 258, 258, 4096, deflate_slow$1)     /* 9 max compression */
];


/* ===========================================================================
 * Initialize the "longest match" routines for a new zlib stream
 */
const lm_init$1 = (s) => {

  s.window_size = 2 * s.w_size;

  /*** CLEAR_HASH(s); ***/
  zero$2(s.head); // Fill with NIL (= 0);

  /* Set the default configuration parameters:
   */
  s.max_lazy_match = configuration_table$1[s.level].max_lazy;
  s.good_match = configuration_table$1[s.level].good_length;
  s.nice_match = configuration_table$1[s.level].nice_length;
  s.max_chain_length = configuration_table$1[s.level].max_chain;

  s.strstart = 0;
  s.block_start = 0;
  s.lookahead = 0;
  s.insert = 0;
  s.match_length = s.prev_length = MIN_MATCH$2 - 1;
  s.match_available = 0;
  s.ins_h = 0;
};


function DeflateState$1() {
  this.strm = null;            /* pointer back to this zlib stream */
  this.status = 0;            /* as the name implies */
  this.pending_buf = null;      /* output still pending */
  this.pending_buf_size = 0;  /* size of pending_buf */
  this.pending_out = 0;       /* next pending byte to output to the stream */
  this.pending = 0;           /* nb of bytes in the pending buffer */
  this.wrap = 0;              /* bit 0 true for zlib, bit 1 true for gzip */
  this.gzhead = null;         /* gzip header information to write */
  this.gzindex = 0;           /* where in extra, name, or comment */
  this.method = Z_DEFLATED$2$1; /* can only be DEFLATED */
  this.last_flush = -1;   /* value of flush param for previous deflate call */

  this.w_size = 0;  /* LZ77 window size (32K by default) */
  this.w_bits = 0;  /* log2(w_size)  (8..16) */
  this.w_mask = 0;  /* w_size - 1 */

  this.window = null;
  /* Sliding window. Input bytes are read into the second half of the window,
   * and move to the first half later to keep a dictionary of at least wSize
   * bytes. With this organization, matches are limited to a distance of
   * wSize-MAX_MATCH bytes, but this ensures that IO is always
   * performed with a length multiple of the block size.
   */

  this.window_size = 0;
  /* Actual size of window: 2*wSize, except when the user input buffer
   * is directly used as sliding window.
   */

  this.prev = null;
  /* Link to older string with same hash index. To limit the size of this
   * array to 64K, this link is maintained only for the last 32K strings.
   * An index in this array is thus a window index modulo 32K.
   */

  this.head = null;   /* Heads of the hash chains or NIL. */

  this.ins_h = 0;       /* hash index of string to be inserted */
  this.hash_size = 0;   /* number of elements in hash table */
  this.hash_bits = 0;   /* log2(hash_size) */
  this.hash_mask = 0;   /* hash_size-1 */

  this.hash_shift = 0;
  /* Number of bits by which ins_h must be shifted at each input
   * step. It must be such that after MIN_MATCH steps, the oldest
   * byte no longer takes part in the hash key, that is:
   *   hash_shift * MIN_MATCH >= hash_bits
   */

  this.block_start = 0;
  /* Window position at the beginning of the current output block. Gets
   * negative when the window is moved backwards.
   */

  this.match_length = 0;      /* length of best match */
  this.prev_match = 0;        /* previous match */
  this.match_available = 0;   /* set if previous match exists */
  this.strstart = 0;          /* start of string to insert */
  this.match_start = 0;       /* start of matching string */
  this.lookahead = 0;         /* number of valid bytes ahead in window */

  this.prev_length = 0;
  /* Length of the best match at previous step. Matches not greater than this
   * are discarded. This is used in the lazy match evaluation.
   */

  this.max_chain_length = 0;
  /* To speed up deflation, hash chains are never searched beyond this
   * length.  A higher limit improves compression ratio but degrades the
   * speed.
   */

  this.max_lazy_match = 0;
  /* Attempt to find a better match only when the current match is strictly
   * smaller than this value. This mechanism is used only for compression
   * levels >= 4.
   */
  // That's alias to max_lazy_match, don't use directly
  //this.max_insert_length = 0;
  /* Insert new strings in the hash table only if the match length is not
   * greater than this length. This saves time but degrades compression.
   * max_insert_length is used only for compression levels <= 3.
   */

  this.level = 0;     /* compression level (1..9) */
  this.strategy = 0;  /* favor or force Huffman coding*/

  this.good_match = 0;
  /* Use a faster search when the previous match is longer than this */

  this.nice_match = 0; /* Stop searching when current match exceeds this */

              /* used by trees.c: */

  /* Didn't use ct_data typedef below to suppress compiler warning */

  // struct ct_data_s dyn_ltree[HEAP_SIZE];   /* literal and length tree */
  // struct ct_data_s dyn_dtree[2*D_CODES+1]; /* distance tree */
  // struct ct_data_s bl_tree[2*BL_CODES+1];  /* Huffman tree for bit lengths */

  // Use flat array of DOUBLE size, with interleaved fata,
  // because JS does not support effective
  this.dyn_ltree  = new Uint16Array(HEAP_SIZE$2 * 2);
  this.dyn_dtree  = new Uint16Array((2 * D_CODES$2 + 1) * 2);
  this.bl_tree    = new Uint16Array((2 * BL_CODES$2 + 1) * 2);
  zero$2(this.dyn_ltree);
  zero$2(this.dyn_dtree);
  zero$2(this.bl_tree);

  this.l_desc   = null;         /* desc. for literal tree */
  this.d_desc   = null;         /* desc. for distance tree */
  this.bl_desc  = null;         /* desc. for bit length tree */

  //ush bl_count[MAX_BITS+1];
  this.bl_count = new Uint16Array(MAX_BITS$2 + 1);
  /* number of codes at each bit length for an optimal tree */

  //int heap[2*L_CODES+1];      /* heap used to build the Huffman trees */
  this.heap = new Uint16Array(2 * L_CODES$2 + 1);  /* heap used to build the Huffman trees */
  zero$2(this.heap);

  this.heap_len = 0;               /* number of elements in the heap */
  this.heap_max = 0;               /* element of largest frequency */
  /* The sons of heap[n] are heap[2*n] and heap[2*n+1]. heap[0] is not used.
   * The same heap array is used to build all trees.
   */

  this.depth = new Uint16Array(2 * L_CODES$2 + 1); //uch depth[2*L_CODES+1];
  zero$2(this.depth);
  /* Depth of each subtree used as tie breaker for trees of equal frequency
   */

  this.sym_buf = 0;        /* buffer for distances and literals/lengths */

  this.lit_bufsize = 0;
  /* Size of match buffer for literals/lengths.  There are 4 reasons for
   * limiting lit_bufsize to 64K:
   *   - frequencies can be kept in 16 bit counters
   *   - if compression is not successful for the first block, all input
   *     data is still in the window so we can still emit a stored block even
   *     when input comes from standard input.  (This can also be done for
   *     all blocks if lit_bufsize is not greater than 32K.)
   *   - if compression is not successful for a file smaller than 64K, we can
   *     even emit a stored file instead of a stored block (saving 5 bytes).
   *     This is applicable only for zip (not gzip or zlib).
   *   - creating new Huffman trees less frequently may not provide fast
   *     adaptation to changes in the input data statistics. (Take for
   *     example a binary file with poorly compressible code followed by
   *     a highly compressible string table.) Smaller buffer sizes give
   *     fast adaptation but have of course the overhead of transmitting
   *     trees more frequently.
   *   - I can't count above 4
   */

  this.sym_next = 0;      /* running index in sym_buf */
  this.sym_end = 0;       /* symbol table full when sym_next reaches this */

  this.opt_len = 0;       /* bit length of current block with optimal trees */
  this.static_len = 0;    /* bit length of current block with static trees */
  this.matches = 0;       /* number of string matches in current block */
  this.insert = 0;        /* bytes at end of window left to insert */


  this.bi_buf = 0;
  /* Output buffer. bits are inserted starting at the bottom (least
   * significant bits).
   */
  this.bi_valid = 0;
  /* Number of valid bits in bi_buf.  All bits above the last valid bit
   * are always zero.
   */

  // Used for window memory init. We safely ignore it for JS. That makes
  // sense only for pointers and memory check tools.
  //this.high_water = 0;
  /* High water mark offset in window for initialized bytes -- bytes above
   * this are set to zero in order to avoid memory check warnings when
   * longest match routines access bytes past the input.  This is then
   * updated to the new high water mark.
   */
}


/* =========================================================================
 * Check for a valid deflate stream state. Return 0 if ok, 1 if not.
 */
const deflateStateCheck$1 = (strm) => {

  if (!strm) {
    return 1;
  }
  const s = strm.state;
  if (!s || s.strm !== strm || (s.status !== INIT_STATE$1 &&
//#ifdef GZIP
                                s.status !== GZIP_STATE$1 &&
//#endif
                                s.status !== EXTRA_STATE$1 &&
                                s.status !== NAME_STATE$1 &&
                                s.status !== COMMENT_STATE$1 &&
                                s.status !== HCRC_STATE$1 &&
                                s.status !== BUSY_STATE$1 &&
                                s.status !== FINISH_STATE$1)) {
    return 1;
  }
  return 0;
};


const deflateResetKeep$1 = (strm) => {

  if (deflateStateCheck$1(strm)) {
    return err$1(strm, Z_STREAM_ERROR$2$1);
  }

  strm.total_in = strm.total_out = 0;
  strm.data_type = Z_UNKNOWN$2;

  const s = strm.state;
  s.pending = 0;
  s.pending_out = 0;

  if (s.wrap < 0) {
    s.wrap = -s.wrap;
    /* was made negative by deflate(..., Z_FINISH); */
  }
  s.status =
//#ifdef GZIP
    s.wrap === 2 ? GZIP_STATE$1 :
//#endif
    s.wrap ? INIT_STATE$1 : BUSY_STATE$1;
  strm.adler = (s.wrap === 2) ?
    0  // crc32(0, Z_NULL, 0)
  :
    1; // adler32(0, Z_NULL, 0)
  s.last_flush = -2;
  _tr_init$2(s);
  return Z_OK$3$1;
};


const deflateReset$1 = (strm) => {

  const ret = deflateResetKeep$1(strm);
  if (ret === Z_OK$3$1) {
    lm_init$1(strm.state);
  }
  return ret;
};


const deflateSetHeader$1 = (strm, head) => {

  if (deflateStateCheck$1(strm) || strm.state.wrap !== 2) {
    return Z_STREAM_ERROR$2$1;
  }
  strm.state.gzhead = head;
  return Z_OK$3$1;
};


const deflateInit2$1 = (strm, level, method, windowBits, memLevel, strategy) => {

  if (!strm) { // === Z_NULL
    return Z_STREAM_ERROR$2$1;
  }
  let wrap = 1;

  if (level === Z_DEFAULT_COMPRESSION$1$1) {
    level = 6;
  }

  if (windowBits < 0) { /* suppress zlib wrapper */
    wrap = 0;
    windowBits = -windowBits;
  }

  else if (windowBits > 15) {
    wrap = 2;           /* write gzip wrapper instead */
    windowBits -= 16;
  }


  if (memLevel < 1 || memLevel > MAX_MEM_LEVEL$1 || method !== Z_DEFLATED$2$1 ||
    windowBits < 8 || windowBits > 15 || level < 0 || level > 9 ||
    strategy < 0 || strategy > Z_FIXED$2 || (windowBits === 8 && wrap !== 1)) {
    return err$1(strm, Z_STREAM_ERROR$2$1);
  }


  if (windowBits === 8) {
    windowBits = 9;
  }
  /* until 256-byte window bug fixed */

  const s = new DeflateState$1();

  strm.state = s;
  s.strm = strm;
  s.status = INIT_STATE$1;     /* to pass state test in deflateReset() */

  s.wrap = wrap;
  s.gzhead = null;
  s.w_bits = windowBits;
  s.w_size = 1 << s.w_bits;
  s.w_mask = s.w_size - 1;

  s.hash_bits = memLevel + 7;
  s.hash_size = 1 << s.hash_bits;
  s.hash_mask = s.hash_size - 1;
  s.hash_shift = ~~((s.hash_bits + MIN_MATCH$2 - 1) / MIN_MATCH$2);

  s.window = new Uint8Array(s.w_size * 2);
  s.head = new Uint16Array(s.hash_size);
  s.prev = new Uint16Array(s.w_size);

  // Don't need mem init magic for JS.
  //s.high_water = 0;  /* nothing written to s->window yet */

  s.lit_bufsize = 1 << (memLevel + 6); /* 16K elements by default */

  /* We overlay pending_buf and sym_buf. This works since the average size
   * for length/distance pairs over any compressed block is assured to be 31
   * bits or less.
   *
   * Analysis: The longest fixed codes are a length code of 8 bits plus 5
   * extra bits, for lengths 131 to 257. The longest fixed distance codes are
   * 5 bits plus 13 extra bits, for distances 16385 to 32768. The longest
   * possible fixed-codes length/distance pair is then 31 bits total.
   *
   * sym_buf starts one-fourth of the way into pending_buf. So there are
   * three bytes in sym_buf for every four bytes in pending_buf. Each symbol
   * in sym_buf is three bytes -- two for the distance and one for the
   * literal/length. As each symbol is consumed, the pointer to the next
   * sym_buf value to read moves forward three bytes. From that symbol, up to
   * 31 bits are written to pending_buf. The closest the written pending_buf
   * bits gets to the next sym_buf symbol to read is just before the last
   * code is written. At that time, 31*(n-2) bits have been written, just
   * after 24*(n-2) bits have been consumed from sym_buf. sym_buf starts at
   * 8*n bits into pending_buf. (Note that the symbol buffer fills when n-1
   * symbols are written.) The closest the writing gets to what is unread is
   * then n+14 bits. Here n is lit_bufsize, which is 16384 by default, and
   * can range from 128 to 32768.
   *
   * Therefore, at a minimum, there are 142 bits of space between what is
   * written and what is read in the overlain buffers, so the symbols cannot
   * be overwritten by the compressed data. That space is actually 139 bits,
   * due to the three-bit fixed-code block header.
   *
   * That covers the case where either Z_FIXED is specified, forcing fixed
   * codes, or when the use of fixed codes is chosen, because that choice
   * results in a smaller compressed block than dynamic codes. That latter
   * condition then assures that the above analysis also covers all dynamic
   * blocks. A dynamic-code block will only be chosen to be emitted if it has
   * fewer bits than a fixed-code block would for the same set of symbols.
   * Therefore its average symbol length is assured to be less than 31. So
   * the compressed data for a dynamic block also cannot overwrite the
   * symbols from which it is being constructed.
   */

  s.pending_buf_size = s.lit_bufsize * 4;
  s.pending_buf = new Uint8Array(s.pending_buf_size);

  // It is offset from `s.pending_buf` (size is `s.lit_bufsize * 2`)
  //s->sym_buf = s->pending_buf + s->lit_bufsize;
  s.sym_buf = s.lit_bufsize;

  //s->sym_end = (s->lit_bufsize - 1) * 3;
  s.sym_end = (s.lit_bufsize - 1) * 3;
  /* We avoid equality with lit_bufsize*3 because of wraparound at 64K
   * on 16 bit machines and because stored blocks are restricted to
   * 64K-1 bytes.
   */

  s.level = level;
  s.strategy = strategy;
  s.method = method;

  return deflateReset$1(strm);
};

const deflateInit$1 = (strm, level) => {

  return deflateInit2$1(strm, level, Z_DEFLATED$2$1, MAX_WBITS$1$1, DEF_MEM_LEVEL$1, Z_DEFAULT_STRATEGY$1$1);
};


/* ========================================================================= */
const deflate$2$1 = (strm, flush) => {

  if (deflateStateCheck$1(strm) || flush > Z_BLOCK$1$1 || flush < 0) {
    return strm ? err$1(strm, Z_STREAM_ERROR$2$1) : Z_STREAM_ERROR$2$1;
  }

  const s = strm.state;

  if (!strm.output ||
      (strm.avail_in !== 0 && !strm.input) ||
      (s.status === FINISH_STATE$1 && flush !== Z_FINISH$3$1)) {
    return err$1(strm, (strm.avail_out === 0) ? Z_BUF_ERROR$1$1 : Z_STREAM_ERROR$2$1);
  }

  const old_flush = s.last_flush;
  s.last_flush = flush;

  /* Flush as much pending output as possible */
  if (s.pending !== 0) {
    flush_pending$1(strm);
    if (strm.avail_out === 0) {
      /* Since avail_out is 0, deflate will be called again with
       * more output space, but possibly with both pending and
       * avail_in equal to zero. There won't be anything to do,
       * but this is not an error situation so make sure we
       * return OK instead of BUF_ERROR at next call of deflate:
       */
      s.last_flush = -1;
      return Z_OK$3$1;
    }

    /* Make sure there is something to do and avoid duplicate consecutive
     * flushes. For repeated and useless calls with Z_FINISH, we keep
     * returning Z_STREAM_END instead of Z_BUF_ERROR.
     */
  } else if (strm.avail_in === 0 && rank$1(flush) <= rank$1(old_flush) &&
    flush !== Z_FINISH$3$1) {
    return err$1(strm, Z_BUF_ERROR$1$1);
  }

  /* User must not provide more input after the first FINISH: */
  if (s.status === FINISH_STATE$1 && strm.avail_in !== 0) {
    return err$1(strm, Z_BUF_ERROR$1$1);
  }

  /* Write the header */
  if (s.status === INIT_STATE$1 && s.wrap === 0) {
    s.status = BUSY_STATE$1;
  }
  if (s.status === INIT_STATE$1) {
    /* zlib header */
    let header = (Z_DEFLATED$2$1 + ((s.w_bits - 8) << 4)) << 8;
    let level_flags = -1;

    if (s.strategy >= Z_HUFFMAN_ONLY$1 || s.level < 2) {
      level_flags = 0;
    } else if (s.level < 6) {
      level_flags = 1;
    } else if (s.level === 6) {
      level_flags = 2;
    } else {
      level_flags = 3;
    }
    header |= (level_flags << 6);
    if (s.strstart !== 0) { header |= PRESET_DICT$1; }
    header += 31 - (header % 31);

    putShortMSB$1(s, header);

    /* Save the adler32 of the preset dictionary: */
    if (s.strstart !== 0) {
      putShortMSB$1(s, strm.adler >>> 16);
      putShortMSB$1(s, strm.adler & 0xffff);
    }
    strm.adler = 1; // adler32(0L, Z_NULL, 0);
    s.status = BUSY_STATE$1;

    /* Compression must start with an empty pending buffer */
    flush_pending$1(strm);
    if (s.pending !== 0) {
      s.last_flush = -1;
      return Z_OK$3$1;
    }
  }
//#ifdef GZIP
  if (s.status === GZIP_STATE$1) {
    /* gzip header */
    strm.adler = 0;  //crc32(0L, Z_NULL, 0);
    put_byte$1(s, 31);
    put_byte$1(s, 139);
    put_byte$1(s, 8);
    if (!s.gzhead) { // s->gzhead == Z_NULL
      put_byte$1(s, 0);
      put_byte$1(s, 0);
      put_byte$1(s, 0);
      put_byte$1(s, 0);
      put_byte$1(s, 0);
      put_byte$1(s, s.level === 9 ? 2 :
                  (s.strategy >= Z_HUFFMAN_ONLY$1 || s.level < 2 ?
                   4 : 0));
      put_byte$1(s, OS_CODE$1);
      s.status = BUSY_STATE$1;

      /* Compression must start with an empty pending buffer */
      flush_pending$1(strm);
      if (s.pending !== 0) {
        s.last_flush = -1;
        return Z_OK$3$1;
      }
    }
    else {
      put_byte$1(s, (s.gzhead.text ? 1 : 0) +
                  (s.gzhead.hcrc ? 2 : 0) +
                  (!s.gzhead.extra ? 0 : 4) +
                  (!s.gzhead.name ? 0 : 8) +
                  (!s.gzhead.comment ? 0 : 16)
      );
      put_byte$1(s, s.gzhead.time & 0xff);
      put_byte$1(s, (s.gzhead.time >> 8) & 0xff);
      put_byte$1(s, (s.gzhead.time >> 16) & 0xff);
      put_byte$1(s, (s.gzhead.time >> 24) & 0xff);
      put_byte$1(s, s.level === 9 ? 2 :
                  (s.strategy >= Z_HUFFMAN_ONLY$1 || s.level < 2 ?
                   4 : 0));
      put_byte$1(s, s.gzhead.os & 0xff);
      if (s.gzhead.extra && s.gzhead.extra.length) {
        put_byte$1(s, s.gzhead.extra.length & 0xff);
        put_byte$1(s, (s.gzhead.extra.length >> 8) & 0xff);
      }
      if (s.gzhead.hcrc) {
        strm.adler = crc32_1$1(strm.adler, s.pending_buf, s.pending, 0);
      }
      s.gzindex = 0;
      s.status = EXTRA_STATE$1;
    }
  }
  if (s.status === EXTRA_STATE$1) {
    if (s.gzhead.extra/* != Z_NULL*/) {
      let beg = s.pending;   /* start of bytes to update crc */
      let left = (s.gzhead.extra.length & 0xffff) - s.gzindex;
      while (s.pending + left > s.pending_buf_size) {
        let copy = s.pending_buf_size - s.pending;
        // zmemcpy(s.pending_buf + s.pending,
        //    s.gzhead.extra + s.gzindex, copy);
        s.pending_buf.set(s.gzhead.extra.subarray(s.gzindex, s.gzindex + copy), s.pending);
        s.pending = s.pending_buf_size;
        //--- HCRC_UPDATE(beg) ---//
        if (s.gzhead.hcrc && s.pending > beg) {
          strm.adler = crc32_1$1(strm.adler, s.pending_buf, s.pending - beg, beg);
        }
        //---//
        s.gzindex += copy;
        flush_pending$1(strm);
        if (s.pending !== 0) {
          s.last_flush = -1;
          return Z_OK$3$1;
        }
        beg = 0;
        left -= copy;
      }
      // JS specific: s.gzhead.extra may be TypedArray or Array for backward compatibility
      //              TypedArray.slice and TypedArray.from don't exist in IE10-IE11
      let gzhead_extra = new Uint8Array(s.gzhead.extra);
      // zmemcpy(s->pending_buf + s->pending,
      //     s->gzhead->extra + s->gzindex, left);
      s.pending_buf.set(gzhead_extra.subarray(s.gzindex, s.gzindex + left), s.pending);
      s.pending += left;
      //--- HCRC_UPDATE(beg) ---//
      if (s.gzhead.hcrc && s.pending > beg) {
        strm.adler = crc32_1$1(strm.adler, s.pending_buf, s.pending - beg, beg);
      }
      //---//
      s.gzindex = 0;
    }
    s.status = NAME_STATE$1;
  }
  if (s.status === NAME_STATE$1) {
    if (s.gzhead.name/* != Z_NULL*/) {
      let beg = s.pending;   /* start of bytes to update crc */
      let val;
      do {
        if (s.pending === s.pending_buf_size) {
          //--- HCRC_UPDATE(beg) ---//
          if (s.gzhead.hcrc && s.pending > beg) {
            strm.adler = crc32_1$1(strm.adler, s.pending_buf, s.pending - beg, beg);
          }
          //---//
          flush_pending$1(strm);
          if (s.pending !== 0) {
            s.last_flush = -1;
            return Z_OK$3$1;
          }
          beg = 0;
        }
        // JS specific: little magic to add zero terminator to end of string
        if (s.gzindex < s.gzhead.name.length) {
          val = s.gzhead.name.charCodeAt(s.gzindex++) & 0xff;
        } else {
          val = 0;
        }
        put_byte$1(s, val);
      } while (val !== 0);
      //--- HCRC_UPDATE(beg) ---//
      if (s.gzhead.hcrc && s.pending > beg) {
        strm.adler = crc32_1$1(strm.adler, s.pending_buf, s.pending - beg, beg);
      }
      //---//
      s.gzindex = 0;
    }
    s.status = COMMENT_STATE$1;
  }
  if (s.status === COMMENT_STATE$1) {
    if (s.gzhead.comment/* != Z_NULL*/) {
      let beg = s.pending;   /* start of bytes to update crc */
      let val;
      do {
        if (s.pending === s.pending_buf_size) {
          //--- HCRC_UPDATE(beg) ---//
          if (s.gzhead.hcrc && s.pending > beg) {
            strm.adler = crc32_1$1(strm.adler, s.pending_buf, s.pending - beg, beg);
          }
          //---//
          flush_pending$1(strm);
          if (s.pending !== 0) {
            s.last_flush = -1;
            return Z_OK$3$1;
          }
          beg = 0;
        }
        // JS specific: little magic to add zero terminator to end of string
        if (s.gzindex < s.gzhead.comment.length) {
          val = s.gzhead.comment.charCodeAt(s.gzindex++) & 0xff;
        } else {
          val = 0;
        }
        put_byte$1(s, val);
      } while (val !== 0);
      //--- HCRC_UPDATE(beg) ---//
      if (s.gzhead.hcrc && s.pending > beg) {
        strm.adler = crc32_1$1(strm.adler, s.pending_buf, s.pending - beg, beg);
      }
      //---//
    }
    s.status = HCRC_STATE$1;
  }
  if (s.status === HCRC_STATE$1) {
    if (s.gzhead.hcrc) {
      if (s.pending + 2 > s.pending_buf_size) {
        flush_pending$1(strm);
        if (s.pending !== 0) {
          s.last_flush = -1;
          return Z_OK$3$1;
        }
      }
      put_byte$1(s, strm.adler & 0xff);
      put_byte$1(s, (strm.adler >> 8) & 0xff);
      strm.adler = 0; //crc32(0L, Z_NULL, 0);
    }
    s.status = BUSY_STATE$1;

    /* Compression must start with an empty pending buffer */
    flush_pending$1(strm);
    if (s.pending !== 0) {
      s.last_flush = -1;
      return Z_OK$3$1;
    }
  }
//#endif

  /* Start a new block or continue the current one.
   */
  if (strm.avail_in !== 0 || s.lookahead !== 0 ||
    (flush !== Z_NO_FLUSH$2$1 && s.status !== FINISH_STATE$1)) {
    let bstate = s.level === 0 ? deflate_stored$1(s, flush) :
                 s.strategy === Z_HUFFMAN_ONLY$1 ? deflate_huff$1(s, flush) :
                 s.strategy === Z_RLE$1 ? deflate_rle$1(s, flush) :
                 configuration_table$1[s.level].func(s, flush);

    if (bstate === BS_FINISH_STARTED$1 || bstate === BS_FINISH_DONE$1) {
      s.status = FINISH_STATE$1;
    }
    if (bstate === BS_NEED_MORE$1 || bstate === BS_FINISH_STARTED$1) {
      if (strm.avail_out === 0) {
        s.last_flush = -1;
        /* avoid BUF_ERROR next call, see above */
      }
      return Z_OK$3$1;
      /* If flush != Z_NO_FLUSH && avail_out == 0, the next call
       * of deflate should use the same flush parameter to make sure
       * that the flush is complete. So we don't have to output an
       * empty block here, this will be done at next call. This also
       * ensures that for a very small output buffer, we emit at most
       * one empty block.
       */
    }
    if (bstate === BS_BLOCK_DONE$1) {
      if (flush === Z_PARTIAL_FLUSH$1) {
        _tr_align$2(s);
      }
      else if (flush !== Z_BLOCK$1$1) { /* FULL_FLUSH or SYNC_FLUSH */

        _tr_stored_block$2(s, 0, 0, false);
        /* For a full flush, this empty block will be recognized
         * as a special marker by inflate_sync().
         */
        if (flush === Z_FULL_FLUSH$1$1) {
          /*** CLEAR_HASH(s); ***/             /* forget history */
          zero$2(s.head); // Fill with NIL (= 0);

          if (s.lookahead === 0) {
            s.strstart = 0;
            s.block_start = 0;
            s.insert = 0;
          }
        }
      }
      flush_pending$1(strm);
      if (strm.avail_out === 0) {
        s.last_flush = -1; /* avoid BUF_ERROR at next call, see above */
        return Z_OK$3$1;
      }
    }
  }

  if (flush !== Z_FINISH$3$1) { return Z_OK$3$1; }
  if (s.wrap <= 0) { return Z_STREAM_END$3$1; }

  /* Write the trailer */
  if (s.wrap === 2) {
    put_byte$1(s, strm.adler & 0xff);
    put_byte$1(s, (strm.adler >> 8) & 0xff);
    put_byte$1(s, (strm.adler >> 16) & 0xff);
    put_byte$1(s, (strm.adler >> 24) & 0xff);
    put_byte$1(s, strm.total_in & 0xff);
    put_byte$1(s, (strm.total_in >> 8) & 0xff);
    put_byte$1(s, (strm.total_in >> 16) & 0xff);
    put_byte$1(s, (strm.total_in >> 24) & 0xff);
  }
  else
  {
    putShortMSB$1(s, strm.adler >>> 16);
    putShortMSB$1(s, strm.adler & 0xffff);
  }

  flush_pending$1(strm);
  /* If avail_out is zero, the application will call deflate again
   * to flush the rest.
   */
  if (s.wrap > 0) { s.wrap = -s.wrap; }
  /* write the trailer only once! */
  return s.pending !== 0 ? Z_OK$3$1 : Z_STREAM_END$3$1;
};


const deflateEnd$1 = (strm) => {

  if (deflateStateCheck$1(strm)) {
    return Z_STREAM_ERROR$2$1;
  }

  const status = strm.state.status;

  strm.state = null;

  return status === BUSY_STATE$1 ? err$1(strm, Z_DATA_ERROR$2$1) : Z_OK$3$1;
};


/* =========================================================================
 * Initializes the compression dictionary from the given byte
 * sequence without producing any compressed output.
 */
const deflateSetDictionary$1 = (strm, dictionary) => {

  let dictLength = dictionary.length;

  if (deflateStateCheck$1(strm)) {
    return Z_STREAM_ERROR$2$1;
  }

  const s = strm.state;
  const wrap = s.wrap;

  if (wrap === 2 || (wrap === 1 && s.status !== INIT_STATE$1) || s.lookahead) {
    return Z_STREAM_ERROR$2$1;
  }

  /* when using zlib wrappers, compute Adler-32 for provided dictionary */
  if (wrap === 1) {
    /* adler32(strm->adler, dictionary, dictLength); */
    strm.adler = adler32_1$1(strm.adler, dictionary, dictLength, 0);
  }

  s.wrap = 0;   /* avoid computing Adler-32 in read_buf */

  /* if dictionary would fill window, just replace the history */
  if (dictLength >= s.w_size) {
    if (wrap === 0) {            /* already empty otherwise */
      /*** CLEAR_HASH(s); ***/
      zero$2(s.head); // Fill with NIL (= 0);
      s.strstart = 0;
      s.block_start = 0;
      s.insert = 0;
    }
    /* use the tail */
    // dictionary = dictionary.slice(dictLength - s.w_size);
    let tmpDict = new Uint8Array(s.w_size);
    tmpDict.set(dictionary.subarray(dictLength - s.w_size, dictLength), 0);
    dictionary = tmpDict;
    dictLength = s.w_size;
  }
  /* insert dictionary into window and hash */
  const avail = strm.avail_in;
  const next = strm.next_in;
  const input = strm.input;
  strm.avail_in = dictLength;
  strm.next_in = 0;
  strm.input = dictionary;
  fill_window$1(s);
  while (s.lookahead >= MIN_MATCH$2) {
    let str = s.strstart;
    let n = s.lookahead - (MIN_MATCH$2 - 1);
    do {
      /* UPDATE_HASH(s, s->ins_h, s->window[str + MIN_MATCH-1]); */
      s.ins_h = HASH$1(s, s.ins_h, s.window[str + MIN_MATCH$2 - 1]);

      s.prev[str & s.w_mask] = s.head[s.ins_h];

      s.head[s.ins_h] = str;
      str++;
    } while (--n);
    s.strstart = str;
    s.lookahead = MIN_MATCH$2 - 1;
    fill_window$1(s);
  }
  s.strstart += s.lookahead;
  s.block_start = s.strstart;
  s.insert = s.lookahead;
  s.lookahead = 0;
  s.match_length = s.prev_length = MIN_MATCH$2 - 1;
  s.match_available = 0;
  strm.next_in = next;
  strm.input = input;
  strm.avail_in = avail;
  s.wrap = wrap;
  return Z_OK$3$1;
};


var deflateInit_1$1 = deflateInit$1;
var deflateInit2_1$1 = deflateInit2$1;
var deflateReset_1$1 = deflateReset$1;
var deflateResetKeep_1$1 = deflateResetKeep$1;
var deflateSetHeader_1$1 = deflateSetHeader$1;
var deflate_2$1$1 = deflate$2$1;
var deflateEnd_1$1 = deflateEnd$1;
var deflateSetDictionary_1$1 = deflateSetDictionary$1;
var deflateInfo$1 = 'pako deflate (from Nodeca project)';

/* Not implemented
module.exports.deflateBound = deflateBound;
module.exports.deflateCopy = deflateCopy;
module.exports.deflateGetDictionary = deflateGetDictionary;
module.exports.deflateParams = deflateParams;
module.exports.deflatePending = deflatePending;
module.exports.deflatePrime = deflatePrime;
module.exports.deflateTune = deflateTune;
*/

var deflate_1$2$1 = {
	deflateInit: deflateInit_1$1,
	deflateInit2: deflateInit2_1$1,
	deflateReset: deflateReset_1$1,
	deflateResetKeep: deflateResetKeep_1$1,
	deflateSetHeader: deflateSetHeader_1$1,
	deflate: deflate_2$1$1,
	deflateEnd: deflateEnd_1$1,
	deflateSetDictionary: deflateSetDictionary_1$1,
	deflateInfo: deflateInfo$1
};

const _has$1 = (obj, key) => {
  return Object.prototype.hasOwnProperty.call(obj, key);
};

var assign$1 = function (obj /*from1, from2, from3, ...*/) {
  const sources = Array.prototype.slice.call(arguments, 1);
  while (sources.length) {
    const source = sources.shift();
    if (!source) { continue; }

    if (typeof source !== 'object') {
      throw new TypeError(source + 'must be non-object');
    }

    for (const p in source) {
      if (_has$1(source, p)) {
        obj[p] = source[p];
      }
    }
  }

  return obj;
};


// Join array of chunks to single array.
var flattenChunks$1 = (chunks) => {
  // calculate data length
  let len = 0;

  for (let i = 0, l = chunks.length; i < l; i++) {
    len += chunks[i].length;
  }

  // join chunks
  const result = new Uint8Array(len);

  for (let i = 0, pos = 0, l = chunks.length; i < l; i++) {
    let chunk = chunks[i];
    result.set(chunk, pos);
    pos += chunk.length;
  }

  return result;
};

var common$1 = {
	assign: assign$1,
	flattenChunks: flattenChunks$1
};

// String encode/decode helpers


// Quick check if we can use fast array to bin string conversion
//
// - apply(Array) can fail on Android 2.2
// - apply(Uint8Array) can fail on iOS 5.1 Safari
//
let STR_APPLY_UIA_OK$1 = true;

try { String.fromCharCode.apply(null, new Uint8Array(1)); } catch (__) { STR_APPLY_UIA_OK$1 = false; }


// Table with utf8 lengths (calculated by first byte of sequence)
// Note, that 5 & 6-byte values and some 4-byte values can not be represented in JS,
// because max possible codepoint is 0x10ffff
const _utf8len$1 = new Uint8Array(256);
for (let q = 0; q < 256; q++) {
  _utf8len$1[q] = (q >= 252 ? 6 : q >= 248 ? 5 : q >= 240 ? 4 : q >= 224 ? 3 : q >= 192 ? 2 : 1);
}
_utf8len$1[254] = _utf8len$1[254] = 1; // Invalid sequence start


// convert string to array (typed, when possible)
var string2buf$1 = (str) => {
  if (typeof TextEncoder === 'function' && TextEncoder.prototype.encode) {
    return new TextEncoder().encode(str);
  }

  let buf, c, c2, m_pos, i, str_len = str.length, buf_len = 0;

  // count binary size
  for (m_pos = 0; m_pos < str_len; m_pos++) {
    c = str.charCodeAt(m_pos);
    if ((c & 0xfc00) === 0xd800 && (m_pos + 1 < str_len)) {
      c2 = str.charCodeAt(m_pos + 1);
      if ((c2 & 0xfc00) === 0xdc00) {
        c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
        m_pos++;
      }
    }
    buf_len += c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0x10000 ? 3 : 4;
  }

  // allocate buffer
  buf = new Uint8Array(buf_len);

  // convert
  for (i = 0, m_pos = 0; i < buf_len; m_pos++) {
    c = str.charCodeAt(m_pos);
    if ((c & 0xfc00) === 0xd800 && (m_pos + 1 < str_len)) {
      c2 = str.charCodeAt(m_pos + 1);
      if ((c2 & 0xfc00) === 0xdc00) {
        c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
        m_pos++;
      }
    }
    if (c < 0x80) {
      /* one byte */
      buf[i++] = c;
    } else if (c < 0x800) {
      /* two bytes */
      buf[i++] = 0xC0 | (c >>> 6);
      buf[i++] = 0x80 | (c & 0x3f);
    } else if (c < 0x10000) {
      /* three bytes */
      buf[i++] = 0xE0 | (c >>> 12);
      buf[i++] = 0x80 | (c >>> 6 & 0x3f);
      buf[i++] = 0x80 | (c & 0x3f);
    } else {
      /* four bytes */
      buf[i++] = 0xf0 | (c >>> 18);
      buf[i++] = 0x80 | (c >>> 12 & 0x3f);
      buf[i++] = 0x80 | (c >>> 6 & 0x3f);
      buf[i++] = 0x80 | (c & 0x3f);
    }
  }

  return buf;
};

// Helper
const buf2binstring$1 = (buf, len) => {
  // On Chrome, the arguments in a function call that are allowed is `65534`.
  // If the length of the buffer is smaller than that, we can use this optimization,
  // otherwise we will take a slower path.
  if (len < 65534) {
    if (buf.subarray && STR_APPLY_UIA_OK$1) {
      return String.fromCharCode.apply(null, buf.length === len ? buf : buf.subarray(0, len));
    }
  }

  let result = '';
  for (let i = 0; i < len; i++) {
    result += String.fromCharCode(buf[i]);
  }
  return result;
};


// convert array to string
var buf2string$1 = (buf, max) => {
  const len = max || buf.length;

  if (typeof TextDecoder === 'function' && TextDecoder.prototype.decode) {
    return new TextDecoder().decode(buf.subarray(0, max));
  }

  let i, out;

  // Reserve max possible length (2 words per char)
  // NB: by unknown reasons, Array is significantly faster for
  //     String.fromCharCode.apply than Uint16Array.
  const utf16buf = new Array(len * 2);

  for (out = 0, i = 0; i < len;) {
    let c = buf[i++];
    // quick process ascii
    if (c < 0x80) { utf16buf[out++] = c; continue; }

    let c_len = _utf8len$1[c];
    // skip 5 & 6 byte codes
    if (c_len > 4) { utf16buf[out++] = 0xfffd; i += c_len - 1; continue; }

    // apply mask on first byte
    c &= c_len === 2 ? 0x1f : c_len === 3 ? 0x0f : 0x07;
    // join the rest
    while (c_len > 1 && i < len) {
      c = (c << 6) | (buf[i++] & 0x3f);
      c_len--;
    }

    // terminated by end of string?
    if (c_len > 1) { utf16buf[out++] = 0xfffd; continue; }

    if (c < 0x10000) {
      utf16buf[out++] = c;
    } else {
      c -= 0x10000;
      utf16buf[out++] = 0xd800 | ((c >> 10) & 0x3ff);
      utf16buf[out++] = 0xdc00 | (c & 0x3ff);
    }
  }

  return buf2binstring$1(utf16buf, out);
};


// Calculate max possible position in utf8 buffer,
// that will not break sequence. If that's not possible
// - (very small limits) return max size as is.
//
// buf[] - utf8 bytes array
// max   - length limit (mandatory);
var utf8border$1 = (buf, max) => {

  max = max || buf.length;
  if (max > buf.length) { max = buf.length; }

  // go back from last position, until start of sequence found
  let pos = max - 1;
  while (pos >= 0 && (buf[pos] & 0xC0) === 0x80) { pos--; }

  // Very small and broken sequence,
  // return max, because we should return something anyway.
  if (pos < 0) { return max; }

  // If we came to start of buffer - that means buffer is too small,
  // return max too.
  if (pos === 0) { return max; }

  return (pos + _utf8len$1[buf[pos]] > max) ? pos : max;
};

var strings$1 = {
	string2buf: string2buf$1,
	buf2string: buf2string$1,
	utf8border: utf8border$1
};

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

function ZStream$1() {
  /* next input byte */
  this.input = null; // JS specific, because we have no pointers
  this.next_in = 0;
  /* number of bytes available at input */
  this.avail_in = 0;
  /* total number of input bytes read so far */
  this.total_in = 0;
  /* next output byte should be put there */
  this.output = null; // JS specific, because we have no pointers
  this.next_out = 0;
  /* remaining free space at output */
  this.avail_out = 0;
  /* total number of bytes output so far */
  this.total_out = 0;
  /* last error message, NULL if no error */
  this.msg = ''/*Z_NULL*/;
  /* not visible by applications */
  this.state = null;
  /* best guess about the data type: binary or text */
  this.data_type = 2/*Z_UNKNOWN*/;
  /* adler32 value of the uncompressed data */
  this.adler = 0;
}

var zstream$1 = ZStream$1;

const toString$1$1 = Object.prototype.toString;

/* Public constants ==========================================================*/
/* ===========================================================================*/

const {
  Z_NO_FLUSH: Z_NO_FLUSH$1$1, Z_SYNC_FLUSH: Z_SYNC_FLUSH$1, Z_FULL_FLUSH: Z_FULL_FLUSH$2, Z_FINISH: Z_FINISH$2$1,
  Z_OK: Z_OK$2$1, Z_STREAM_END: Z_STREAM_END$2$1,
  Z_DEFAULT_COMPRESSION: Z_DEFAULT_COMPRESSION$2,
  Z_DEFAULT_STRATEGY: Z_DEFAULT_STRATEGY$2,
  Z_DEFLATED: Z_DEFLATED$1$1
} = constants$2$1;

/* ===========================================================================*/


/**
 * class Deflate
 *
 * Generic JS-style wrapper for zlib calls. If you don't need
 * streaming behaviour - use more simple functions: [[deflate]],
 * [[deflateRaw]] and [[gzip]].
 **/

/* internal
 * Deflate.chunks -> Array
 *
 * Chunks of output data, if [[Deflate#onData]] not overridden.
 **/

/**
 * Deflate.result -> Uint8Array
 *
 * Compressed result, generated by default [[Deflate#onData]]
 * and [[Deflate#onEnd]] handlers. Filled after you push last chunk
 * (call [[Deflate#push]] with `Z_FINISH` / `true` param).
 **/

/**
 * Deflate.err -> Number
 *
 * Error code after deflate finished. 0 (Z_OK) on success.
 * You will not need it in real life, because deflate errors
 * are possible only on wrong options or bad `onData` / `onEnd`
 * custom handlers.
 **/

/**
 * Deflate.msg -> String
 *
 * Error message, if [[Deflate.err]] != 0
 **/


/**
 * new Deflate(options)
 * - options (Object): zlib deflate options.
 *
 * Creates new deflator instance with specified params. Throws exception
 * on bad params. Supported options:
 *
 * - `level`
 * - `windowBits`
 * - `memLevel`
 * - `strategy`
 * - `dictionary`
 *
 * [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced)
 * for more information on these.
 *
 * Additional options, for internal needs:
 *
 * - `chunkSize` - size of generated data chunks (16K by default)
 * - `raw` (Boolean) - do raw deflate
 * - `gzip` (Boolean) - create gzip wrapper
 * - `header` (Object) - custom header for gzip
 *   - `text` (Boolean) - true if compressed data believed to be text
 *   - `time` (Number) - modification time, unix timestamp
 *   - `os` (Number) - operation system code
 *   - `extra` (Array) - array of bytes with extra data (max 65536)
 *   - `name` (String) - file name (binary string)
 *   - `comment` (String) - comment (binary string)
 *   - `hcrc` (Boolean) - true if header crc should be added
 *
 * ##### Example:
 *
 * ```javascript
 * const pako = require('pako')
 *   , chunk1 = new Uint8Array([1,2,3,4,5,6,7,8,9])
 *   , chunk2 = new Uint8Array([10,11,12,13,14,15,16,17,18,19]);
 *
 * const deflate = new pako.Deflate({ level: 3});
 *
 * deflate.push(chunk1, false);
 * deflate.push(chunk2, true);  // true -> last chunk
 *
 * if (deflate.err) { throw new Error(deflate.err); }
 *
 * console.log(deflate.result);
 * ```
 **/
function Deflate$1$1(options) {
  this.options = common$1.assign({
    level: Z_DEFAULT_COMPRESSION$2,
    method: Z_DEFLATED$1$1,
    chunkSize: 16384,
    windowBits: 15,
    memLevel: 8,
    strategy: Z_DEFAULT_STRATEGY$2
  }, options || {});

  let opt = this.options;

  if (opt.raw && (opt.windowBits > 0)) {
    opt.windowBits = -opt.windowBits;
  }

  else if (opt.gzip && (opt.windowBits > 0) && (opt.windowBits < 16)) {
    opt.windowBits += 16;
  }

  this.err    = 0;      // error code, if happens (0 = Z_OK)
  this.msg    = '';     // error message
  this.ended  = false;  // used to avoid multiple onEnd() calls
  this.chunks = [];     // chunks of compressed data

  this.strm = new zstream$1();
  this.strm.avail_out = 0;

  let status = deflate_1$2$1.deflateInit2(
    this.strm,
    opt.level,
    opt.method,
    opt.windowBits,
    opt.memLevel,
    opt.strategy
  );

  if (status !== Z_OK$2$1) {
    throw new Error(messages$1[status]);
  }

  if (opt.header) {
    deflate_1$2$1.deflateSetHeader(this.strm, opt.header);
  }

  if (opt.dictionary) {
    let dict;
    // Convert data if needed
    if (typeof opt.dictionary === 'string') {
      // If we need to compress text, change encoding to utf8.
      dict = strings$1.string2buf(opt.dictionary);
    } else if (toString$1$1.call(opt.dictionary) === '[object ArrayBuffer]') {
      dict = new Uint8Array(opt.dictionary);
    } else {
      dict = opt.dictionary;
    }

    status = deflate_1$2$1.deflateSetDictionary(this.strm, dict);

    if (status !== Z_OK$2$1) {
      throw new Error(messages$1[status]);
    }

    this._dict_set = true;
  }
}

/**
 * Deflate#push(data[, flush_mode]) -> Boolean
 * - data (Uint8Array|ArrayBuffer|String): input data. Strings will be
 *   converted to utf8 byte sequence.
 * - flush_mode (Number|Boolean): 0..6 for corresponding Z_NO_FLUSH..Z_TREE modes.
 *   See constants. Skipped or `false` means Z_NO_FLUSH, `true` means Z_FINISH.
 *
 * Sends input data to deflate pipe, generating [[Deflate#onData]] calls with
 * new compressed chunks. Returns `true` on success. The last data block must
 * have `flush_mode` Z_FINISH (or `true`). That will flush internal pending
 * buffers and call [[Deflate#onEnd]].
 *
 * On fail call [[Deflate#onEnd]] with error code and return false.
 *
 * ##### Example
 *
 * ```javascript
 * push(chunk, false); // push one of data chunks
 * ...
 * push(chunk, true);  // push last chunk
 * ```
 **/
Deflate$1$1.prototype.push = function (data, flush_mode) {
  const strm = this.strm;
  const chunkSize = this.options.chunkSize;
  let status, _flush_mode;

  if (this.ended) { return false; }

  if (flush_mode === ~~flush_mode) _flush_mode = flush_mode;
  else _flush_mode = flush_mode === true ? Z_FINISH$2$1 : Z_NO_FLUSH$1$1;

  // Convert data if needed
  if (typeof data === 'string') {
    // If we need to compress text, change encoding to utf8.
    strm.input = strings$1.string2buf(data);
  } else if (toString$1$1.call(data) === '[object ArrayBuffer]') {
    strm.input = new Uint8Array(data);
  } else {
    strm.input = data;
  }

  strm.next_in = 0;
  strm.avail_in = strm.input.length;

  for (;;) {
    if (strm.avail_out === 0) {
      strm.output = new Uint8Array(chunkSize);
      strm.next_out = 0;
      strm.avail_out = chunkSize;
    }

    // Make sure avail_out > 6 to avoid repeating markers
    if ((_flush_mode === Z_SYNC_FLUSH$1 || _flush_mode === Z_FULL_FLUSH$2) && strm.avail_out <= 6) {
      this.onData(strm.output.subarray(0, strm.next_out));
      strm.avail_out = 0;
      continue;
    }

    status = deflate_1$2$1.deflate(strm, _flush_mode);

    // Ended => flush and finish
    if (status === Z_STREAM_END$2$1) {
      if (strm.next_out > 0) {
        this.onData(strm.output.subarray(0, strm.next_out));
      }
      status = deflate_1$2$1.deflateEnd(this.strm);
      this.onEnd(status);
      this.ended = true;
      return status === Z_OK$2$1;
    }

    // Flush if out buffer full
    if (strm.avail_out === 0) {
      this.onData(strm.output);
      continue;
    }

    // Flush if requested and has data
    if (_flush_mode > 0 && strm.next_out > 0) {
      this.onData(strm.output.subarray(0, strm.next_out));
      strm.avail_out = 0;
      continue;
    }

    if (strm.avail_in === 0) break;
  }

  return true;
};


/**
 * Deflate#onData(chunk) -> Void
 * - chunk (Uint8Array): output data.
 *
 * By default, stores data blocks in `chunks[]` property and glue
 * those in `onEnd`. Override this handler, if you need another behaviour.
 **/
Deflate$1$1.prototype.onData = function (chunk) {
  this.chunks.push(chunk);
};


/**
 * Deflate#onEnd(status) -> Void
 * - status (Number): deflate status. 0 (Z_OK) on success,
 *   other if not.
 *
 * Called once after you tell deflate that the input stream is
 * complete (Z_FINISH). By default - join collected chunks,
 * free memory and fill `results` / `err` properties.
 **/
Deflate$1$1.prototype.onEnd = function (status) {
  // On success - join
  if (status === Z_OK$2$1) {
    this.result = common$1.flattenChunks(this.chunks);
  }
  this.chunks = [];
  this.err = status;
  this.msg = this.strm.msg;
};


/**
 * deflate(data[, options]) -> Uint8Array
 * - data (Uint8Array|ArrayBuffer|String): input data to compress.
 * - options (Object): zlib deflate options.
 *
 * Compress `data` with deflate algorithm and `options`.
 *
 * Supported options are:
 *
 * - level
 * - windowBits
 * - memLevel
 * - strategy
 * - dictionary
 *
 * [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced)
 * for more information on these.
 *
 * Sugar (options):
 *
 * - `raw` (Boolean) - say that we work with raw stream, if you don't wish to specify
 *   negative windowBits implicitly.
 *
 * ##### Example:
 *
 * ```javascript
 * const pako = require('pako')
 * const data = new Uint8Array([1,2,3,4,5,6,7,8,9]);
 *
 * console.log(pako.deflate(data));
 * ```
 **/
function deflate$1(input, options) {
  const deflator = new Deflate$1$1(options);

  deflator.push(input, true);

  // That will never happens, if you don't cheat with options :)
  if (deflator.err) { throw deflator.msg || messages$1[deflator.err]; }

  return deflator.result;
}


/**
 * deflateRaw(data[, options]) -> Uint8Array
 * - data (Uint8Array|ArrayBuffer|String): input data to compress.
 * - options (Object): zlib deflate options.
 *
 * The same as [[deflate]], but creates raw data, without wrapper
 * (header and adler32 crc).
 **/
function deflateRaw$1(input, options) {
  options = options || {};
  options.raw = true;
  return deflate$1(input, options);
}


/**
 * gzip(data[, options]) -> Uint8Array
 * - data (Uint8Array|ArrayBuffer|String): input data to compress.
 * - options (Object): zlib deflate options.
 *
 * The same as [[deflate]], but create gzip wrapper instead of
 * deflate one.
 **/
function gzip$1(input, options) {
  options = options || {};
  options.gzip = true;
  return deflate$1(input, options);
}


var Deflate_1$1 = Deflate$1$1;
var deflate_2 = deflate$1;
var deflateRaw_1$1 = deflateRaw$1;
var gzip_1$1 = gzip$1;
var constants$1 = constants$2$1;

var deflate_1$1 = {
	Deflate: Deflate_1$1,
	deflate: deflate_2,
	deflateRaw: deflateRaw_1$1,
	gzip: gzip_1$1,
	constants: constants$1
};

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

// See state defs from inflate.js
const BAD$1$1 = 16209;       /* got a data error -- remain here until reset */
const TYPE$1$1 = 16191;      /* i: waiting for type bits, including last-flag bit */

/*
   Decode literal, length, and distance codes and write out the resulting
   literal and match bytes until either not enough input or output is
   available, an end-of-block is encountered, or a data error is encountered.
   When large enough input and output buffers are supplied to inflate(), for
   example, a 16K input buffer and a 64K output buffer, more than 95% of the
   inflate execution time is spent in this routine.

   Entry assumptions:

        state.mode === LEN
        strm.avail_in >= 6
        strm.avail_out >= 258
        start >= strm.avail_out
        state.bits < 8

   On return, state.mode is one of:

        LEN -- ran out of enough output space or enough available input
        TYPE -- reached end of block code, inflate() to interpret next block
        BAD -- error in block data

   Notes:

    - The maximum input bits used by a length/distance pair is 15 bits for the
      length code, 5 bits for the length extra, 15 bits for the distance code,
      and 13 bits for the distance extra.  This totals 48 bits, or six bytes.
      Therefore if strm.avail_in >= 6, then there is enough input to avoid
      checking for available input while decoding.

    - The maximum bytes that a single length/distance pair can output is 258
      bytes, which is the maximum length that can be coded.  inflate_fast()
      requires strm.avail_out >= 258 for each loop to avoid checking for
      output space.
 */
var inffast$1 = function inflate_fast(strm, start) {
  let _in;                    /* local strm.input */
  let last;                   /* have enough input while in < last */
  let _out;                   /* local strm.output */
  let beg;                    /* inflate()'s initial strm.output */
  let end;                    /* while out < end, enough space available */
//#ifdef INFLATE_STRICT
  let dmax;                   /* maximum distance from zlib header */
//#endif
  let wsize;                  /* window size or zero if not using window */
  let whave;                  /* valid bytes in the window */
  let wnext;                  /* window write index */
  // Use `s_window` instead `window`, avoid conflict with instrumentation tools
  let s_window;               /* allocated sliding window, if wsize != 0 */
  let hold;                   /* local strm.hold */
  let bits;                   /* local strm.bits */
  let lcode;                  /* local strm.lencode */
  let dcode;                  /* local strm.distcode */
  let lmask;                  /* mask for first level of length codes */
  let dmask;                  /* mask for first level of distance codes */
  let here;                   /* retrieved table entry */
  let op;                     /* code bits, operation, extra bits, or */
                              /*  window position, window bytes to copy */
  let len;                    /* match length, unused bytes */
  let dist;                   /* match distance */
  let from;                   /* where to copy match from */
  let from_source;


  let input, output; // JS specific, because we have no pointers

  /* copy state to local variables */
  const state = strm.state;
  //here = state.here;
  _in = strm.next_in;
  input = strm.input;
  last = _in + (strm.avail_in - 5);
  _out = strm.next_out;
  output = strm.output;
  beg = _out - (start - strm.avail_out);
  end = _out + (strm.avail_out - 257);
//#ifdef INFLATE_STRICT
  dmax = state.dmax;
//#endif
  wsize = state.wsize;
  whave = state.whave;
  wnext = state.wnext;
  s_window = state.window;
  hold = state.hold;
  bits = state.bits;
  lcode = state.lencode;
  dcode = state.distcode;
  lmask = (1 << state.lenbits) - 1;
  dmask = (1 << state.distbits) - 1;


  /* decode literals and length/distances until end-of-block or not enough
     input data or output space */

  top:
  do {
    if (bits < 15) {
      hold += input[_in++] << bits;
      bits += 8;
      hold += input[_in++] << bits;
      bits += 8;
    }

    here = lcode[hold & lmask];

    dolen:
    for (;;) { // Goto emulation
      op = here >>> 24/*here.bits*/;
      hold >>>= op;
      bits -= op;
      op = (here >>> 16) & 0xff/*here.op*/;
      if (op === 0) {                          /* literal */
        //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ?
        //        "inflate:         literal '%c'\n" :
        //        "inflate:         literal 0x%02x\n", here.val));
        output[_out++] = here & 0xffff/*here.val*/;
      }
      else if (op & 16) {                     /* length base */
        len = here & 0xffff/*here.val*/;
        op &= 15;                           /* number of extra bits */
        if (op) {
          if (bits < op) {
            hold += input[_in++] << bits;
            bits += 8;
          }
          len += hold & ((1 << op) - 1);
          hold >>>= op;
          bits -= op;
        }
        //Tracevv((stderr, "inflate:         length %u\n", len));
        if (bits < 15) {
          hold += input[_in++] << bits;
          bits += 8;
          hold += input[_in++] << bits;
          bits += 8;
        }
        here = dcode[hold & dmask];

        dodist:
        for (;;) { // goto emulation
          op = here >>> 24/*here.bits*/;
          hold >>>= op;
          bits -= op;
          op = (here >>> 16) & 0xff/*here.op*/;

          if (op & 16) {                      /* distance base */
            dist = here & 0xffff/*here.val*/;
            op &= 15;                       /* number of extra bits */
            if (bits < op) {
              hold += input[_in++] << bits;
              bits += 8;
              if (bits < op) {
                hold += input[_in++] << bits;
                bits += 8;
              }
            }
            dist += hold & ((1 << op) - 1);
//#ifdef INFLATE_STRICT
            if (dist > dmax) {
              strm.msg = 'invalid distance too far back';
              state.mode = BAD$1$1;
              break top;
            }
//#endif
            hold >>>= op;
            bits -= op;
            //Tracevv((stderr, "inflate:         distance %u\n", dist));
            op = _out - beg;                /* max distance in output */
            if (dist > op) {                /* see if copy from window */
              op = dist - op;               /* distance back in window */
              if (op > whave) {
                if (state.sane) {
                  strm.msg = 'invalid distance too far back';
                  state.mode = BAD$1$1;
                  break top;
                }

// (!) This block is disabled in zlib defaults,
// don't enable it for binary compatibility
//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR
//                if (len <= op - whave) {
//                  do {
//                    output[_out++] = 0;
//                  } while (--len);
//                  continue top;
//                }
//                len -= op - whave;
//                do {
//                  output[_out++] = 0;
//                } while (--op > whave);
//                if (op === 0) {
//                  from = _out - dist;
//                  do {
//                    output[_out++] = output[from++];
//                  } while (--len);
//                  continue top;
//                }
//#endif
              }
              from = 0; // window index
              from_source = s_window;
              if (wnext === 0) {           /* very common case */
                from += wsize - op;
                if (op < len) {         /* some from window */
                  len -= op;
                  do {
                    output[_out++] = s_window[from++];
                  } while (--op);
                  from = _out - dist;  /* rest from output */
                  from_source = output;
                }
              }
              else if (wnext < op) {      /* wrap around window */
                from += wsize + wnext - op;
                op -= wnext;
                if (op < len) {         /* some from end of window */
                  len -= op;
                  do {
                    output[_out++] = s_window[from++];
                  } while (--op);
                  from = 0;
                  if (wnext < len) {  /* some from start of window */
                    op = wnext;
                    len -= op;
                    do {
                      output[_out++] = s_window[from++];
                    } while (--op);
                    from = _out - dist;      /* rest from output */
                    from_source = output;
                  }
                }
              }
              else {                      /* contiguous in window */
                from += wnext - op;
                if (op < len) {         /* some from window */
                  len -= op;
                  do {
                    output[_out++] = s_window[from++];
                  } while (--op);
                  from = _out - dist;  /* rest from output */
                  from_source = output;
                }
              }
              while (len > 2) {
                output[_out++] = from_source[from++];
                output[_out++] = from_source[from++];
                output[_out++] = from_source[from++];
                len -= 3;
              }
              if (len) {
                output[_out++] = from_source[from++];
                if (len > 1) {
                  output[_out++] = from_source[from++];
                }
              }
            }
            else {
              from = _out - dist;          /* copy direct from output */
              do {                        /* minimum length is three */
                output[_out++] = output[from++];
                output[_out++] = output[from++];
                output[_out++] = output[from++];
                len -= 3;
              } while (len > 2);
              if (len) {
                output[_out++] = output[from++];
                if (len > 1) {
                  output[_out++] = output[from++];
                }
              }
            }
          }
          else if ((op & 64) === 0) {          /* 2nd level distance code */
            here = dcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
            continue dodist;
          }
          else {
            strm.msg = 'invalid distance code';
            state.mode = BAD$1$1;
            break top;
          }

          break; // need to emulate goto via "continue"
        }
      }
      else if ((op & 64) === 0) {              /* 2nd level length code */
        here = lcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
        continue dolen;
      }
      else if (op & 32) {                     /* end-of-block */
        //Tracevv((stderr, "inflate:         end of block\n"));
        state.mode = TYPE$1$1;
        break top;
      }
      else {
        strm.msg = 'invalid literal/length code';
        state.mode = BAD$1$1;
        break top;
      }

      break; // need to emulate goto via "continue"
    }
  } while (_in < last && _out < end);

  /* return unused bytes (on entry, bits < 8, so in won't go too far back) */
  len = bits >> 3;
  _in -= len;
  bits -= len << 3;
  hold &= (1 << bits) - 1;

  /* update state and return */
  strm.next_in = _in;
  strm.next_out = _out;
  strm.avail_in = (_in < last ? 5 + (last - _in) : 5 - (_in - last));
  strm.avail_out = (_out < end ? 257 + (end - _out) : 257 - (_out - end));
  state.hold = hold;
  state.bits = bits;
  return;
};

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

const MAXBITS$1 = 15;
const ENOUGH_LENS$1$1 = 852;
const ENOUGH_DISTS$1$1 = 592;
//const ENOUGH = (ENOUGH_LENS+ENOUGH_DISTS);

const CODES$1$1 = 0;
const LENS$1$1 = 1;
const DISTS$1$1 = 2;

const lbase$1 = new Uint16Array([ /* Length codes 257..285 base */
  3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
  35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0
]);

const lext$1 = new Uint8Array([ /* Length codes 257..285 extra */
  16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18,
  19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 16, 72, 78
]);

const dbase$1 = new Uint16Array([ /* Distance codes 0..29 base */
  1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
  257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,
  8193, 12289, 16385, 24577, 0, 0
]);

const dext$1 = new Uint8Array([ /* Distance codes 0..29 extra */
  16, 16, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22,
  23, 23, 24, 24, 25, 25, 26, 26, 27, 27,
  28, 28, 29, 29, 64, 64
]);

const inflate_table$1 = (type, lens, lens_index, codes, table, table_index, work, opts) =>
{
  const bits = opts.bits;
      //here = opts.here; /* table entry for duplication */

  let len = 0;               /* a code's length in bits */
  let sym = 0;               /* index of code symbols */
  let min = 0, max = 0;          /* minimum and maximum code lengths */
  let root = 0;              /* number of index bits for root table */
  let curr = 0;              /* number of index bits for current table */
  let drop = 0;              /* code bits to drop for sub-table */
  let left = 0;                   /* number of prefix codes available */
  let used = 0;              /* code entries in table used */
  let huff = 0;              /* Huffman code */
  let incr;              /* for incrementing code, index */
  let fill;              /* index for replicating entries */
  let low;               /* low bits for current root entry */
  let mask;              /* mask for low root bits */
  let next;             /* next available space in table */
  let base = null;     /* base value table to use */
//  let shoextra;    /* extra bits table to use */
  let match;                  /* use base and extra for symbol >= match */
  const count = new Uint16Array(MAXBITS$1 + 1); //[MAXBITS+1];    /* number of codes of each length */
  const offs = new Uint16Array(MAXBITS$1 + 1); //[MAXBITS+1];     /* offsets in table for each length */
  let extra = null;

  let here_bits, here_op, here_val;

  /*
   Process a set of code lengths to create a canonical Huffman code.  The
   code lengths are lens[0..codes-1].  Each length corresponds to the
   symbols 0..codes-1.  The Huffman code is generated by first sorting the
   symbols by length from short to long, and retaining the symbol order
   for codes with equal lengths.  Then the code starts with all zero bits
   for the first code of the shortest length, and the codes are integer
   increments for the same length, and zeros are appended as the length
   increases.  For the deflate format, these bits are stored backwards
   from their more natural integer increment ordering, and so when the
   decoding tables are built in the large loop below, the integer codes
   are incremented backwards.

   This routine assumes, but does not check, that all of the entries in
   lens[] are in the range 0..MAXBITS.  The caller must assure this.
   1..MAXBITS is interpreted as that code length.  zero means that that
   symbol does not occur in this code.

   The codes are sorted by computing a count of codes for each length,
   creating from that a table of starting indices for each length in the
   sorted table, and then entering the symbols in order in the sorted
   table.  The sorted table is work[], with that space being provided by
   the caller.

   The length counts are used for other purposes as well, i.e. finding
   the minimum and maximum length codes, determining if there are any
   codes at all, checking for a valid set of lengths, and looking ahead
   at length counts to determine sub-table sizes when building the
   decoding tables.
   */

  /* accumulate lengths for codes (assumes lens[] all in 0..MAXBITS) */
  for (len = 0; len <= MAXBITS$1; len++) {
    count[len] = 0;
  }
  for (sym = 0; sym < codes; sym++) {
    count[lens[lens_index + sym]]++;
  }

  /* bound code lengths, force root to be within code lengths */
  root = bits;
  for (max = MAXBITS$1; max >= 1; max--) {
    if (count[max] !== 0) { break; }
  }
  if (root > max) {
    root = max;
  }
  if (max === 0) {                     /* no symbols to code at all */
    //table.op[opts.table_index] = 64;  //here.op = (var char)64;    /* invalid code marker */
    //table.bits[opts.table_index] = 1;   //here.bits = (var char)1;
    //table.val[opts.table_index++] = 0;   //here.val = (var short)0;
    table[table_index++] = (1 << 24) | (64 << 16) | 0;


    //table.op[opts.table_index] = 64;
    //table.bits[opts.table_index] = 1;
    //table.val[opts.table_index++] = 0;
    table[table_index++] = (1 << 24) | (64 << 16) | 0;

    opts.bits = 1;
    return 0;     /* no symbols, but wait for decoding to report error */
  }
  for (min = 1; min < max; min++) {
    if (count[min] !== 0) { break; }
  }
  if (root < min) {
    root = min;
  }

  /* check for an over-subscribed or incomplete set of lengths */
  left = 1;
  for (len = 1; len <= MAXBITS$1; len++) {
    left <<= 1;
    left -= count[len];
    if (left < 0) {
      return -1;
    }        /* over-subscribed */
  }
  if (left > 0 && (type === CODES$1$1 || max !== 1)) {
    return -1;                      /* incomplete set */
  }

  /* generate offsets into symbol table for each length for sorting */
  offs[1] = 0;
  for (len = 1; len < MAXBITS$1; len++) {
    offs[len + 1] = offs[len] + count[len];
  }

  /* sort symbols by length, by symbol order within each length */
  for (sym = 0; sym < codes; sym++) {
    if (lens[lens_index + sym] !== 0) {
      work[offs[lens[lens_index + sym]]++] = sym;
    }
  }

  /*
   Create and fill in decoding tables.  In this loop, the table being
   filled is at next and has curr index bits.  The code being used is huff
   with length len.  That code is converted to an index by dropping drop
   bits off of the bottom.  For codes where len is less than drop + curr,
   those top drop + curr - len bits are incremented through all values to
   fill the table with replicated entries.

   root is the number of index bits for the root table.  When len exceeds
   root, sub-tables are created pointed to by the root entry with an index
   of the low root bits of huff.  This is saved in low to check for when a
   new sub-table should be started.  drop is zero when the root table is
   being filled, and drop is root when sub-tables are being filled.

   When a new sub-table is needed, it is necessary to look ahead in the
   code lengths to determine what size sub-table is needed.  The length
   counts are used for this, and so count[] is decremented as codes are
   entered in the tables.

   used keeps track of how many table entries have been allocated from the
   provided *table space.  It is checked for LENS and DIST tables against
   the constants ENOUGH_LENS and ENOUGH_DISTS to guard against changes in
   the initial root table size constants.  See the comments in inftrees.h
   for more information.

   sym increments through all symbols, and the loop terminates when
   all codes of length max, i.e. all codes, have been processed.  This
   routine permits incomplete codes, so another loop after this one fills
   in the rest of the decoding tables with invalid code markers.
   */

  /* set up for code type */
  // poor man optimization - use if-else instead of switch,
  // to avoid deopts in old v8
  if (type === CODES$1$1) {
    base = extra = work;    /* dummy value--not used */
    match = 20;

  } else if (type === LENS$1$1) {
    base = lbase$1;
    extra = lext$1;
    match = 257;

  } else {                    /* DISTS */
    base = dbase$1;
    extra = dext$1;
    match = 0;
  }

  /* initialize opts for loop */
  huff = 0;                   /* starting code */
  sym = 0;                    /* starting code symbol */
  len = min;                  /* starting code length */
  next = table_index;              /* current table to fill in */
  curr = root;                /* current table index bits */
  drop = 0;                   /* current bits to drop from code for index */
  low = -1;                   /* trigger new sub-table when len > root */
  used = 1 << root;          /* use root table entries */
  mask = used - 1;            /* mask for comparing low */

  /* check available table space */
  if ((type === LENS$1$1 && used > ENOUGH_LENS$1$1) ||
    (type === DISTS$1$1 && used > ENOUGH_DISTS$1$1)) {
    return 1;
  }

  /* process all codes and make table entries */
  for (;;) {
    /* create table entry */
    here_bits = len - drop;
    if (work[sym] + 1 < match) {
      here_op = 0;
      here_val = work[sym];
    }
    else if (work[sym] >= match) {
      here_op = extra[work[sym] - match];
      here_val = base[work[sym] - match];
    }
    else {
      here_op = 32 + 64;         /* end of block */
      here_val = 0;
    }

    /* replicate for those indices with low len bits equal to huff */
    incr = 1 << (len - drop);
    fill = 1 << curr;
    min = fill;                 /* save offset to next table */
    do {
      fill -= incr;
      table[next + (huff >> drop) + fill] = (here_bits << 24) | (here_op << 16) | here_val |0;
    } while (fill !== 0);

    /* backwards increment the len-bit code huff */
    incr = 1 << (len - 1);
    while (huff & incr) {
      incr >>= 1;
    }
    if (incr !== 0) {
      huff &= incr - 1;
      huff += incr;
    } else {
      huff = 0;
    }

    /* go to next symbol, update count, len */
    sym++;
    if (--count[len] === 0) {
      if (len === max) { break; }
      len = lens[lens_index + work[sym]];
    }

    /* create new sub-table if needed */
    if (len > root && (huff & mask) !== low) {
      /* if first time, transition to sub-tables */
      if (drop === 0) {
        drop = root;
      }

      /* increment past last table */
      next += min;            /* here min is 1 << curr */

      /* determine length of next table */
      curr = len - drop;
      left = 1 << curr;
      while (curr + drop < max) {
        left -= count[curr + drop];
        if (left <= 0) { break; }
        curr++;
        left <<= 1;
      }

      /* check for enough space */
      used += 1 << curr;
      if ((type === LENS$1$1 && used > ENOUGH_LENS$1$1) ||
        (type === DISTS$1$1 && used > ENOUGH_DISTS$1$1)) {
        return 1;
      }

      /* point entry in root table to sub-table */
      low = huff & mask;
      /*table.op[low] = curr;
      table.bits[low] = root;
      table.val[low] = next - opts.table_index;*/
      table[low] = (root << 24) | (curr << 16) | (next - table_index) |0;
    }
  }

  /* fill in remaining table entry if code is incomplete (guaranteed to have
   at most one remaining entry, since if the code is incomplete, the
   maximum code length that was allowed to get this far is one bit) */
  if (huff !== 0) {
    //table.op[next + huff] = 64;            /* invalid code marker */
    //table.bits[next + huff] = len - drop;
    //table.val[next + huff] = 0;
    table[next + huff] = ((len - drop) << 24) | (64 << 16) |0;
  }

  /* set return parameters */
  //opts.table_index += used;
  opts.bits = root;
  return 0;
};


var inftrees$1 = inflate_table$1;

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.






const CODES$2 = 0;
const LENS$2 = 1;
const DISTS$2 = 2;

/* Public constants ==========================================================*/
/* ===========================================================================*/

const {
  Z_FINISH: Z_FINISH$1$1, Z_BLOCK: Z_BLOCK$2, Z_TREES: Z_TREES$1,
  Z_OK: Z_OK$1$1, Z_STREAM_END: Z_STREAM_END$1$1, Z_NEED_DICT: Z_NEED_DICT$1$1, Z_STREAM_ERROR: Z_STREAM_ERROR$1$1, Z_DATA_ERROR: Z_DATA_ERROR$1$1, Z_MEM_ERROR: Z_MEM_ERROR$1$1, Z_BUF_ERROR: Z_BUF_ERROR$2,
  Z_DEFLATED: Z_DEFLATED$3
} = constants$2$1;


/* STATES ====================================================================*/
/* ===========================================================================*/


const    HEAD$1 = 16180;       /* i: waiting for magic header */
const    FLAGS$1 = 16181;      /* i: waiting for method and flags (gzip) */
const    TIME$1 = 16182;       /* i: waiting for modification time (gzip) */
const    OS$1 = 16183;         /* i: waiting for extra flags and operating system (gzip) */
const    EXLEN$1 = 16184;      /* i: waiting for extra length (gzip) */
const    EXTRA$1 = 16185;      /* i: waiting for extra bytes (gzip) */
const    NAME$1 = 16186;       /* i: waiting for end of file name (gzip) */
const    COMMENT$1 = 16187;    /* i: waiting for end of comment (gzip) */
const    HCRC$1 = 16188;       /* i: waiting for header crc (gzip) */
const    DICTID$1 = 16189;    /* i: waiting for dictionary check value */
const    DICT$1 = 16190;      /* waiting for inflateSetDictionary() call */
const        TYPE$2 = 16191;      /* i: waiting for type bits, including last-flag bit */
const        TYPEDO$1 = 16192;    /* i: same, but skip check to exit inflate on new block */
const        STORED$1 = 16193;    /* i: waiting for stored size (length and complement) */
const        COPY_$1 = 16194;     /* i/o: same as COPY below, but only first time in */
const        COPY$1 = 16195;      /* i/o: waiting for input or output to copy stored block */
const        TABLE$1 = 16196;     /* i: waiting for dynamic block table lengths */
const        LENLENS$1 = 16197;   /* i: waiting for code length code lengths */
const        CODELENS$1 = 16198;  /* i: waiting for length/lit and distance code lengths */
const            LEN_$1 = 16199;      /* i: same as LEN below, but only first time in */
const            LEN$1 = 16200;       /* i: waiting for length/lit/eob code */
const            LENEXT$1 = 16201;    /* i: waiting for length extra bits */
const            DIST$1 = 16202;      /* i: waiting for distance code */
const            DISTEXT$1 = 16203;   /* i: waiting for distance extra bits */
const            MATCH$1 = 16204;     /* o: waiting for output space to copy string */
const            LIT$1 = 16205;       /* o: waiting for output space to write literal */
const    CHECK$1 = 16206;     /* i: waiting for 32-bit check value */
const    LENGTH$1 = 16207;    /* i: waiting for 32-bit length (gzip) */
const    DONE$1 = 16208;      /* finished check, done -- remain here until reset */
const    BAD$2 = 16209;       /* got a data error -- remain here until reset */
const    MEM$1 = 16210;       /* got an inflate() memory error -- remain here until reset */
const    SYNC$1 = 16211;      /* looking for synchronization bytes to restart inflate() */

/* ===========================================================================*/



const ENOUGH_LENS$2 = 852;
const ENOUGH_DISTS$2 = 592;
//const ENOUGH =  (ENOUGH_LENS+ENOUGH_DISTS);

const MAX_WBITS$2 = 15;
/* 32K LZ77 window */
const DEF_WBITS$1 = MAX_WBITS$2;


const zswap32$1 = (q) => {

  return  (((q >>> 24) & 0xff) +
          ((q >>> 8) & 0xff00) +
          ((q & 0xff00) << 8) +
          ((q & 0xff) << 24));
};


function InflateState$1() {
  this.strm = null;           /* pointer back to this zlib stream */
  this.mode = 0;              /* current inflate mode */
  this.last = false;          /* true if processing last block */
  this.wrap = 0;              /* bit 0 true for zlib, bit 1 true for gzip,
                                 bit 2 true to validate check value */
  this.havedict = false;      /* true if dictionary provided */
  this.flags = 0;             /* gzip header method and flags (0 if zlib), or
                                 -1 if raw or no header yet */
  this.dmax = 0;              /* zlib header max distance (INFLATE_STRICT) */
  this.check = 0;             /* protected copy of check value */
  this.total = 0;             /* protected copy of output count */
  // TODO: may be {}
  this.head = null;           /* where to save gzip header information */

  /* sliding window */
  this.wbits = 0;             /* log base 2 of requested window size */
  this.wsize = 0;             /* window size or zero if not using window */
  this.whave = 0;             /* valid bytes in the window */
  this.wnext = 0;             /* window write index */
  this.window = null;         /* allocated sliding window, if needed */

  /* bit accumulator */
  this.hold = 0;              /* input bit accumulator */
  this.bits = 0;              /* number of bits in "in" */

  /* for string and stored block copying */
  this.length = 0;            /* literal or length of data to copy */
  this.offset = 0;            /* distance back to copy string from */

  /* for table and code decoding */
  this.extra = 0;             /* extra bits needed */

  /* fixed and dynamic code tables */
  this.lencode = null;          /* starting table for length/literal codes */
  this.distcode = null;         /* starting table for distance codes */
  this.lenbits = 0;           /* index bits for lencode */
  this.distbits = 0;          /* index bits for distcode */

  /* dynamic table building */
  this.ncode = 0;             /* number of code length code lengths */
  this.nlen = 0;              /* number of length code lengths */
  this.ndist = 0;             /* number of distance code lengths */
  this.have = 0;              /* number of code lengths in lens[] */
  this.next = null;              /* next available space in codes[] */

  this.lens = new Uint16Array(320); /* temporary storage for code lengths */
  this.work = new Uint16Array(288); /* work area for code table building */

  /*
   because we don't have pointers in js, we use lencode and distcode directly
   as buffers so we don't need codes
  */
  //this.codes = new Int32Array(ENOUGH);       /* space for code tables */
  this.lendyn = null;              /* dynamic table for length/literal codes (JS specific) */
  this.distdyn = null;             /* dynamic table for distance codes (JS specific) */
  this.sane = 0;                   /* if false, allow invalid distance too far */
  this.back = 0;                   /* bits back of last unprocessed length/lit */
  this.was = 0;                    /* initial length of match */
}


const inflateStateCheck$1 = (strm) => {

  if (!strm) {
    return 1;
  }
  const state = strm.state;
  if (!state || state.strm !== strm ||
    state.mode < HEAD$1 || state.mode > SYNC$1) {
    return 1;
  }
  return 0;
};


const inflateResetKeep$1 = (strm) => {

  if (inflateStateCheck$1(strm)) { return Z_STREAM_ERROR$1$1; }
  const state = strm.state;
  strm.total_in = strm.total_out = state.total = 0;
  strm.msg = ''; /*Z_NULL*/
  if (state.wrap) {       /* to support ill-conceived Java test suite */
    strm.adler = state.wrap & 1;
  }
  state.mode = HEAD$1;
  state.last = 0;
  state.havedict = 0;
  state.flags = -1;
  state.dmax = 32768;
  state.head = null/*Z_NULL*/;
  state.hold = 0;
  state.bits = 0;
  //state.lencode = state.distcode = state.next = state.codes;
  state.lencode = state.lendyn = new Int32Array(ENOUGH_LENS$2);
  state.distcode = state.distdyn = new Int32Array(ENOUGH_DISTS$2);

  state.sane = 1;
  state.back = -1;
  //Tracev((stderr, "inflate: reset\n"));
  return Z_OK$1$1;
};


const inflateReset$1 = (strm) => {

  if (inflateStateCheck$1(strm)) { return Z_STREAM_ERROR$1$1; }
  const state = strm.state;
  state.wsize = 0;
  state.whave = 0;
  state.wnext = 0;
  return inflateResetKeep$1(strm);

};


const inflateReset2$1 = (strm, windowBits) => {
  let wrap;

  /* get the state */
  if (inflateStateCheck$1(strm)) { return Z_STREAM_ERROR$1$1; }
  const state = strm.state;

  /* extract wrap request from windowBits parameter */
  if (windowBits < 0) {
    wrap = 0;
    windowBits = -windowBits;
  }
  else {
    wrap = (windowBits >> 4) + 5;
    if (windowBits < 48) {
      windowBits &= 15;
    }
  }

  /* set number of window bits, free window if different */
  if (windowBits && (windowBits < 8 || windowBits > 15)) {
    return Z_STREAM_ERROR$1$1;
  }
  if (state.window !== null && state.wbits !== windowBits) {
    state.window = null;
  }

  /* update state and reset the rest of it */
  state.wrap = wrap;
  state.wbits = windowBits;
  return inflateReset$1(strm);
};


const inflateInit2$1 = (strm, windowBits) => {

  if (!strm) { return Z_STREAM_ERROR$1$1; }
  //strm.msg = Z_NULL;                 /* in case we return an error */

  const state = new InflateState$1();

  //if (state === Z_NULL) return Z_MEM_ERROR;
  //Tracev((stderr, "inflate: allocated\n"));
  strm.state = state;
  state.strm = strm;
  state.window = null/*Z_NULL*/;
  state.mode = HEAD$1;     /* to pass state test in inflateReset2() */
  const ret = inflateReset2$1(strm, windowBits);
  if (ret !== Z_OK$1$1) {
    strm.state = null/*Z_NULL*/;
  }
  return ret;
};


const inflateInit$1 = (strm) => {

  return inflateInit2$1(strm, DEF_WBITS$1);
};


/*
 Return state with length and distance decoding tables and index sizes set to
 fixed code decoding.  Normally this returns fixed tables from inffixed.h.
 If BUILDFIXED is defined, then instead this routine builds the tables the
 first time it's called, and returns those tables the first time and
 thereafter.  This reduces the size of the code by about 2K bytes, in
 exchange for a little execution time.  However, BUILDFIXED should not be
 used for threaded applications, since the rewriting of the tables and virgin
 may not be thread-safe.
 */
let virgin$1 = true;

let lenfix$1, distfix$1; // We have no pointers in JS, so keep tables separate


const fixedtables$1 = (state) => {

  /* build fixed huffman tables if first call (may not be thread safe) */
  if (virgin$1) {
    lenfix$1 = new Int32Array(512);
    distfix$1 = new Int32Array(32);

    /* literal/length table */
    let sym = 0;
    while (sym < 144) { state.lens[sym++] = 8; }
    while (sym < 256) { state.lens[sym++] = 9; }
    while (sym < 280) { state.lens[sym++] = 7; }
    while (sym < 288) { state.lens[sym++] = 8; }

    inftrees$1(LENS$2,  state.lens, 0, 288, lenfix$1,   0, state.work, { bits: 9 });

    /* distance table */
    sym = 0;
    while (sym < 32) { state.lens[sym++] = 5; }

    inftrees$1(DISTS$2, state.lens, 0, 32,   distfix$1, 0, state.work, { bits: 5 });

    /* do this just once */
    virgin$1 = false;
  }

  state.lencode = lenfix$1;
  state.lenbits = 9;
  state.distcode = distfix$1;
  state.distbits = 5;
};


/*
 Update the window with the last wsize (normally 32K) bytes written before
 returning.  If window does not exist yet, create it.  This is only called
 when a window is already in use, or when output has been written during this
 inflate call, but the end of the deflate stream has not been reached yet.
 It is also called to create a window for dictionary data when a dictionary
 is loaded.

 Providing output buffers larger than 32K to inflate() should provide a speed
 advantage, since only the last 32K of output is copied to the sliding window
 upon return from inflate(), and since all distances after the first 32K of
 output will fall in the output data, making match copies simpler and faster.
 The advantage may be dependent on the size of the processor's data caches.
 */
const updatewindow$1 = (strm, src, end, copy) => {

  let dist;
  const state = strm.state;

  /* if it hasn't been done already, allocate space for the window */
  if (state.window === null) {
    state.wsize = 1 << state.wbits;
    state.wnext = 0;
    state.whave = 0;

    state.window = new Uint8Array(state.wsize);
  }

  /* copy state->wsize or less output bytes into the circular window */
  if (copy >= state.wsize) {
    state.window.set(src.subarray(end - state.wsize, end), 0);
    state.wnext = 0;
    state.whave = state.wsize;
  }
  else {
    dist = state.wsize - state.wnext;
    if (dist > copy) {
      dist = copy;
    }
    //zmemcpy(state->window + state->wnext, end - copy, dist);
    state.window.set(src.subarray(end - copy, end - copy + dist), state.wnext);
    copy -= dist;
    if (copy) {
      //zmemcpy(state->window, end - copy, copy);
      state.window.set(src.subarray(end - copy, end), 0);
      state.wnext = copy;
      state.whave = state.wsize;
    }
    else {
      state.wnext += dist;
      if (state.wnext === state.wsize) { state.wnext = 0; }
      if (state.whave < state.wsize) { state.whave += dist; }
    }
  }
  return 0;
};


const inflate$2$1 = (strm, flush) => {

  let state;
  let input, output;          // input/output buffers
  let next;                   /* next input INDEX */
  let put;                    /* next output INDEX */
  let have, left;             /* available input and output */
  let hold;                   /* bit buffer */
  let bits;                   /* bits in bit buffer */
  let _in, _out;              /* save starting available input and output */
  let copy;                   /* number of stored or match bytes to copy */
  let from;                   /* where to copy match bytes from */
  let from_source;
  let here = 0;               /* current decoding table entry */
  let here_bits, here_op, here_val; // paked "here" denormalized (JS specific)
  //let last;                   /* parent table entry */
  let last_bits, last_op, last_val; // paked "last" denormalized (JS specific)
  let len;                    /* length to copy for repeats, bits to drop */
  let ret;                    /* return code */
  const hbuf = new Uint8Array(4);    /* buffer for gzip header crc calculation */
  let opts;

  let n; // temporary variable for NEED_BITS

  const order = /* permutation of code lengths */
    new Uint8Array([ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ]);


  if (inflateStateCheck$1(strm) || !strm.output ||
      (!strm.input && strm.avail_in !== 0)) {
    return Z_STREAM_ERROR$1$1;
  }

  state = strm.state;
  if (state.mode === TYPE$2) { state.mode = TYPEDO$1; }    /* skip check */


  //--- LOAD() ---
  put = strm.next_out;
  output = strm.output;
  left = strm.avail_out;
  next = strm.next_in;
  input = strm.input;
  have = strm.avail_in;
  hold = state.hold;
  bits = state.bits;
  //---

  _in = have;
  _out = left;
  ret = Z_OK$1$1;

  inf_leave: // goto emulation
  for (;;) {
    switch (state.mode) {
      case HEAD$1:
        if (state.wrap === 0) {
          state.mode = TYPEDO$1;
          break;
        }
        //=== NEEDBITS(16);
        while (bits < 16) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        if ((state.wrap & 2) && hold === 0x8b1f) {  /* gzip header */
          if (state.wbits === 0) {
            state.wbits = 15;
          }
          state.check = 0/*crc32(0L, Z_NULL, 0)*/;
          //=== CRC2(state.check, hold);
          hbuf[0] = hold & 0xff;
          hbuf[1] = (hold >>> 8) & 0xff;
          state.check = crc32_1$1(state.check, hbuf, 2, 0);
          //===//

          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
          state.mode = FLAGS$1;
          break;
        }
        if (state.head) {
          state.head.done = false;
        }
        if (!(state.wrap & 1) ||   /* check if zlib header allowed */
          (((hold & 0xff)/*BITS(8)*/ << 8) + (hold >> 8)) % 31) {
          strm.msg = 'incorrect header check';
          state.mode = BAD$2;
          break;
        }
        if ((hold & 0x0f)/*BITS(4)*/ !== Z_DEFLATED$3) {
          strm.msg = 'unknown compression method';
          state.mode = BAD$2;
          break;
        }
        //--- DROPBITS(4) ---//
        hold >>>= 4;
        bits -= 4;
        //---//
        len = (hold & 0x0f)/*BITS(4)*/ + 8;
        if (state.wbits === 0) {
          state.wbits = len;
        }
        if (len > 15 || len > state.wbits) {
          strm.msg = 'invalid window size';
          state.mode = BAD$2;
          break;
        }

        // !!! pako patch. Force use `options.windowBits` if passed.
        // Required to always use max window size by default.
        state.dmax = 1 << state.wbits;
        //state.dmax = 1 << len;

        state.flags = 0;               /* indicate zlib header */
        //Tracev((stderr, "inflate:   zlib header ok\n"));
        strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/;
        state.mode = hold & 0x200 ? DICTID$1 : TYPE$2;
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        break;
      case FLAGS$1:
        //=== NEEDBITS(16); */
        while (bits < 16) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        state.flags = hold;
        if ((state.flags & 0xff) !== Z_DEFLATED$3) {
          strm.msg = 'unknown compression method';
          state.mode = BAD$2;
          break;
        }
        if (state.flags & 0xe000) {
          strm.msg = 'unknown header flags set';
          state.mode = BAD$2;
          break;
        }
        if (state.head) {
          state.head.text = ((hold >> 8) & 1);
        }
        if ((state.flags & 0x0200) && (state.wrap & 4)) {
          //=== CRC2(state.check, hold);
          hbuf[0] = hold & 0xff;
          hbuf[1] = (hold >>> 8) & 0xff;
          state.check = crc32_1$1(state.check, hbuf, 2, 0);
          //===//
        }
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        state.mode = TIME$1;
        /* falls through */
      case TIME$1:
        //=== NEEDBITS(32); */
        while (bits < 32) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        if (state.head) {
          state.head.time = hold;
        }
        if ((state.flags & 0x0200) && (state.wrap & 4)) {
          //=== CRC4(state.check, hold)
          hbuf[0] = hold & 0xff;
          hbuf[1] = (hold >>> 8) & 0xff;
          hbuf[2] = (hold >>> 16) & 0xff;
          hbuf[3] = (hold >>> 24) & 0xff;
          state.check = crc32_1$1(state.check, hbuf, 4, 0);
          //===
        }
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        state.mode = OS$1;
        /* falls through */
      case OS$1:
        //=== NEEDBITS(16); */
        while (bits < 16) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        if (state.head) {
          state.head.xflags = (hold & 0xff);
          state.head.os = (hold >> 8);
        }
        if ((state.flags & 0x0200) && (state.wrap & 4)) {
          //=== CRC2(state.check, hold);
          hbuf[0] = hold & 0xff;
          hbuf[1] = (hold >>> 8) & 0xff;
          state.check = crc32_1$1(state.check, hbuf, 2, 0);
          //===//
        }
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        state.mode = EXLEN$1;
        /* falls through */
      case EXLEN$1:
        if (state.flags & 0x0400) {
          //=== NEEDBITS(16); */
          while (bits < 16) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          state.length = hold;
          if (state.head) {
            state.head.extra_len = hold;
          }
          if ((state.flags & 0x0200) && (state.wrap & 4)) {
            //=== CRC2(state.check, hold);
            hbuf[0] = hold & 0xff;
            hbuf[1] = (hold >>> 8) & 0xff;
            state.check = crc32_1$1(state.check, hbuf, 2, 0);
            //===//
          }
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
        }
        else if (state.head) {
          state.head.extra = null/*Z_NULL*/;
        }
        state.mode = EXTRA$1;
        /* falls through */
      case EXTRA$1:
        if (state.flags & 0x0400) {
          copy = state.length;
          if (copy > have) { copy = have; }
          if (copy) {
            if (state.head) {
              len = state.head.extra_len - state.length;
              if (!state.head.extra) {
                // Use untyped array for more convenient processing later
                state.head.extra = new Uint8Array(state.head.extra_len);
              }
              state.head.extra.set(
                input.subarray(
                  next,
                  // extra field is limited to 65536 bytes
                  // - no need for additional size check
                  next + copy
                ),
                /*len + copy > state.head.extra_max - len ? state.head.extra_max : copy,*/
                len
              );
              //zmemcpy(state.head.extra + len, next,
              //        len + copy > state.head.extra_max ?
              //        state.head.extra_max - len : copy);
            }
            if ((state.flags & 0x0200) && (state.wrap & 4)) {
              state.check = crc32_1$1(state.check, input, copy, next);
            }
            have -= copy;
            next += copy;
            state.length -= copy;
          }
          if (state.length) { break inf_leave; }
        }
        state.length = 0;
        state.mode = NAME$1;
        /* falls through */
      case NAME$1:
        if (state.flags & 0x0800) {
          if (have === 0) { break inf_leave; }
          copy = 0;
          do {
            // TODO: 2 or 1 bytes?
            len = input[next + copy++];
            /* use constant limit because in js we should not preallocate memory */
            if (state.head && len &&
                (state.length < 65536 /*state.head.name_max*/)) {
              state.head.name += String.fromCharCode(len);
            }
          } while (len && copy < have);

          if ((state.flags & 0x0200) && (state.wrap & 4)) {
            state.check = crc32_1$1(state.check, input, copy, next);
          }
          have -= copy;
          next += copy;
          if (len) { break inf_leave; }
        }
        else if (state.head) {
          state.head.name = null;
        }
        state.length = 0;
        state.mode = COMMENT$1;
        /* falls through */
      case COMMENT$1:
        if (state.flags & 0x1000) {
          if (have === 0) { break inf_leave; }
          copy = 0;
          do {
            len = input[next + copy++];
            /* use constant limit because in js we should not preallocate memory */
            if (state.head && len &&
                (state.length < 65536 /*state.head.comm_max*/)) {
              state.head.comment += String.fromCharCode(len);
            }
          } while (len && copy < have);
          if ((state.flags & 0x0200) && (state.wrap & 4)) {
            state.check = crc32_1$1(state.check, input, copy, next);
          }
          have -= copy;
          next += copy;
          if (len) { break inf_leave; }
        }
        else if (state.head) {
          state.head.comment = null;
        }
        state.mode = HCRC$1;
        /* falls through */
      case HCRC$1:
        if (state.flags & 0x0200) {
          //=== NEEDBITS(16); */
          while (bits < 16) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          if ((state.wrap & 4) && hold !== (state.check & 0xffff)) {
            strm.msg = 'header crc mismatch';
            state.mode = BAD$2;
            break;
          }
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
        }
        if (state.head) {
          state.head.hcrc = ((state.flags >> 9) & 1);
          state.head.done = true;
        }
        strm.adler = state.check = 0;
        state.mode = TYPE$2;
        break;
      case DICTID$1:
        //=== NEEDBITS(32); */
        while (bits < 32) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        strm.adler = state.check = zswap32$1(hold);
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        state.mode = DICT$1;
        /* falls through */
      case DICT$1:
        if (state.havedict === 0) {
          //--- RESTORE() ---
          strm.next_out = put;
          strm.avail_out = left;
          strm.next_in = next;
          strm.avail_in = have;
          state.hold = hold;
          state.bits = bits;
          //---
          return Z_NEED_DICT$1$1;
        }
        strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/;
        state.mode = TYPE$2;
        /* falls through */
      case TYPE$2:
        if (flush === Z_BLOCK$2 || flush === Z_TREES$1) { break inf_leave; }
        /* falls through */
      case TYPEDO$1:
        if (state.last) {
          //--- BYTEBITS() ---//
          hold >>>= bits & 7;
          bits -= bits & 7;
          //---//
          state.mode = CHECK$1;
          break;
        }
        //=== NEEDBITS(3); */
        while (bits < 3) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        state.last = (hold & 0x01)/*BITS(1)*/;
        //--- DROPBITS(1) ---//
        hold >>>= 1;
        bits -= 1;
        //---//

        switch ((hold & 0x03)/*BITS(2)*/) {
          case 0:                             /* stored block */
            //Tracev((stderr, "inflate:     stored block%s\n",
            //        state.last ? " (last)" : ""));
            state.mode = STORED$1;
            break;
          case 1:                             /* fixed block */
            fixedtables$1(state);
            //Tracev((stderr, "inflate:     fixed codes block%s\n",
            //        state.last ? " (last)" : ""));
            state.mode = LEN_$1;             /* decode codes */
            if (flush === Z_TREES$1) {
              //--- DROPBITS(2) ---//
              hold >>>= 2;
              bits -= 2;
              //---//
              break inf_leave;
            }
            break;
          case 2:                             /* dynamic block */
            //Tracev((stderr, "inflate:     dynamic codes block%s\n",
            //        state.last ? " (last)" : ""));
            state.mode = TABLE$1;
            break;
          case 3:
            strm.msg = 'invalid block type';
            state.mode = BAD$2;
        }
        //--- DROPBITS(2) ---//
        hold >>>= 2;
        bits -= 2;
        //---//
        break;
      case STORED$1:
        //--- BYTEBITS() ---// /* go to byte boundary */
        hold >>>= bits & 7;
        bits -= bits & 7;
        //---//
        //=== NEEDBITS(32); */
        while (bits < 32) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        if ((hold & 0xffff) !== ((hold >>> 16) ^ 0xffff)) {
          strm.msg = 'invalid stored block lengths';
          state.mode = BAD$2;
          break;
        }
        state.length = hold & 0xffff;
        //Tracev((stderr, "inflate:       stored length %u\n",
        //        state.length));
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        state.mode = COPY_$1;
        if (flush === Z_TREES$1) { break inf_leave; }
        /* falls through */
      case COPY_$1:
        state.mode = COPY$1;
        /* falls through */
      case COPY$1:
        copy = state.length;
        if (copy) {
          if (copy > have) { copy = have; }
          if (copy > left) { copy = left; }
          if (copy === 0) { break inf_leave; }
          //--- zmemcpy(put, next, copy); ---
          output.set(input.subarray(next, next + copy), put);
          //---//
          have -= copy;
          next += copy;
          left -= copy;
          put += copy;
          state.length -= copy;
          break;
        }
        //Tracev((stderr, "inflate:       stored end\n"));
        state.mode = TYPE$2;
        break;
      case TABLE$1:
        //=== NEEDBITS(14); */
        while (bits < 14) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        state.nlen = (hold & 0x1f)/*BITS(5)*/ + 257;
        //--- DROPBITS(5) ---//
        hold >>>= 5;
        bits -= 5;
        //---//
        state.ndist = (hold & 0x1f)/*BITS(5)*/ + 1;
        //--- DROPBITS(5) ---//
        hold >>>= 5;
        bits -= 5;
        //---//
        state.ncode = (hold & 0x0f)/*BITS(4)*/ + 4;
        //--- DROPBITS(4) ---//
        hold >>>= 4;
        bits -= 4;
        //---//
//#ifndef PKZIP_BUG_WORKAROUND
        if (state.nlen > 286 || state.ndist > 30) {
          strm.msg = 'too many length or distance symbols';
          state.mode = BAD$2;
          break;
        }
//#endif
        //Tracev((stderr, "inflate:       table sizes ok\n"));
        state.have = 0;
        state.mode = LENLENS$1;
        /* falls through */
      case LENLENS$1:
        while (state.have < state.ncode) {
          //=== NEEDBITS(3);
          while (bits < 3) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          state.lens[order[state.have++]] = (hold & 0x07);//BITS(3);
          //--- DROPBITS(3) ---//
          hold >>>= 3;
          bits -= 3;
          //---//
        }
        while (state.have < 19) {
          state.lens[order[state.have++]] = 0;
        }
        // We have separate tables & no pointers. 2 commented lines below not needed.
        //state.next = state.codes;
        //state.lencode = state.next;
        // Switch to use dynamic table
        state.lencode = state.lendyn;
        state.lenbits = 7;

        opts = { bits: state.lenbits };
        ret = inftrees$1(CODES$2, state.lens, 0, 19, state.lencode, 0, state.work, opts);
        state.lenbits = opts.bits;

        if (ret) {
          strm.msg = 'invalid code lengths set';
          state.mode = BAD$2;
          break;
        }
        //Tracev((stderr, "inflate:       code lengths ok\n"));
        state.have = 0;
        state.mode = CODELENS$1;
        /* falls through */
      case CODELENS$1:
        while (state.have < state.nlen + state.ndist) {
          for (;;) {
            here = state.lencode[hold & ((1 << state.lenbits) - 1)];/*BITS(state.lenbits)*/
            here_bits = here >>> 24;
            here_op = (here >>> 16) & 0xff;
            here_val = here & 0xffff;

            if ((here_bits) <= bits) { break; }
            //--- PULLBYTE() ---//
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
            //---//
          }
          if (here_val < 16) {
            //--- DROPBITS(here.bits) ---//
            hold >>>= here_bits;
            bits -= here_bits;
            //---//
            state.lens[state.have++] = here_val;
          }
          else {
            if (here_val === 16) {
              //=== NEEDBITS(here.bits + 2);
              n = here_bits + 2;
              while (bits < n) {
                if (have === 0) { break inf_leave; }
                have--;
                hold += input[next++] << bits;
                bits += 8;
              }
              //===//
              //--- DROPBITS(here.bits) ---//
              hold >>>= here_bits;
              bits -= here_bits;
              //---//
              if (state.have === 0) {
                strm.msg = 'invalid bit length repeat';
                state.mode = BAD$2;
                break;
              }
              len = state.lens[state.have - 1];
              copy = 3 + (hold & 0x03);//BITS(2);
              //--- DROPBITS(2) ---//
              hold >>>= 2;
              bits -= 2;
              //---//
            }
            else if (here_val === 17) {
              //=== NEEDBITS(here.bits + 3);
              n = here_bits + 3;
              while (bits < n) {
                if (have === 0) { break inf_leave; }
                have--;
                hold += input[next++] << bits;
                bits += 8;
              }
              //===//
              //--- DROPBITS(here.bits) ---//
              hold >>>= here_bits;
              bits -= here_bits;
              //---//
              len = 0;
              copy = 3 + (hold & 0x07);//BITS(3);
              //--- DROPBITS(3) ---//
              hold >>>= 3;
              bits -= 3;
              //---//
            }
            else {
              //=== NEEDBITS(here.bits + 7);
              n = here_bits + 7;
              while (bits < n) {
                if (have === 0) { break inf_leave; }
                have--;
                hold += input[next++] << bits;
                bits += 8;
              }
              //===//
              //--- DROPBITS(here.bits) ---//
              hold >>>= here_bits;
              bits -= here_bits;
              //---//
              len = 0;
              copy = 11 + (hold & 0x7f);//BITS(7);
              //--- DROPBITS(7) ---//
              hold >>>= 7;
              bits -= 7;
              //---//
            }
            if (state.have + copy > state.nlen + state.ndist) {
              strm.msg = 'invalid bit length repeat';
              state.mode = BAD$2;
              break;
            }
            while (copy--) {
              state.lens[state.have++] = len;
            }
          }
        }

        /* handle error breaks in while */
        if (state.mode === BAD$2) { break; }

        /* check for end-of-block code (better have one) */
        if (state.lens[256] === 0) {
          strm.msg = 'invalid code -- missing end-of-block';
          state.mode = BAD$2;
          break;
        }

        /* build code tables -- note: do not change the lenbits or distbits
           values here (9 and 6) without reading the comments in inftrees.h
           concerning the ENOUGH constants, which depend on those values */
        state.lenbits = 9;

        opts = { bits: state.lenbits };
        ret = inftrees$1(LENS$2, state.lens, 0, state.nlen, state.lencode, 0, state.work, opts);
        // We have separate tables & no pointers. 2 commented lines below not needed.
        // state.next_index = opts.table_index;
        state.lenbits = opts.bits;
        // state.lencode = state.next;

        if (ret) {
          strm.msg = 'invalid literal/lengths set';
          state.mode = BAD$2;
          break;
        }

        state.distbits = 6;
        //state.distcode.copy(state.codes);
        // Switch to use dynamic table
        state.distcode = state.distdyn;
        opts = { bits: state.distbits };
        ret = inftrees$1(DISTS$2, state.lens, state.nlen, state.ndist, state.distcode, 0, state.work, opts);
        // We have separate tables & no pointers. 2 commented lines below not needed.
        // state.next_index = opts.table_index;
        state.distbits = opts.bits;
        // state.distcode = state.next;

        if (ret) {
          strm.msg = 'invalid distances set';
          state.mode = BAD$2;
          break;
        }
        //Tracev((stderr, 'inflate:       codes ok\n'));
        state.mode = LEN_$1;
        if (flush === Z_TREES$1) { break inf_leave; }
        /* falls through */
      case LEN_$1:
        state.mode = LEN$1;
        /* falls through */
      case LEN$1:
        if (have >= 6 && left >= 258) {
          //--- RESTORE() ---
          strm.next_out = put;
          strm.avail_out = left;
          strm.next_in = next;
          strm.avail_in = have;
          state.hold = hold;
          state.bits = bits;
          //---
          inffast$1(strm, _out);
          //--- LOAD() ---
          put = strm.next_out;
          output = strm.output;
          left = strm.avail_out;
          next = strm.next_in;
          input = strm.input;
          have = strm.avail_in;
          hold = state.hold;
          bits = state.bits;
          //---

          if (state.mode === TYPE$2) {
            state.back = -1;
          }
          break;
        }
        state.back = 0;
        for (;;) {
          here = state.lencode[hold & ((1 << state.lenbits) - 1)];  /*BITS(state.lenbits)*/
          here_bits = here >>> 24;
          here_op = (here >>> 16) & 0xff;
          here_val = here & 0xffff;

          if (here_bits <= bits) { break; }
          //--- PULLBYTE() ---//
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
          //---//
        }
        if (here_op && (here_op & 0xf0) === 0) {
          last_bits = here_bits;
          last_op = here_op;
          last_val = here_val;
          for (;;) {
            here = state.lencode[last_val +
                    ((hold & ((1 << (last_bits + last_op)) - 1))/*BITS(last.bits + last.op)*/ >> last_bits)];
            here_bits = here >>> 24;
            here_op = (here >>> 16) & 0xff;
            here_val = here & 0xffff;

            if ((last_bits + here_bits) <= bits) { break; }
            //--- PULLBYTE() ---//
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
            //---//
          }
          //--- DROPBITS(last.bits) ---//
          hold >>>= last_bits;
          bits -= last_bits;
          //---//
          state.back += last_bits;
        }
        //--- DROPBITS(here.bits) ---//
        hold >>>= here_bits;
        bits -= here_bits;
        //---//
        state.back += here_bits;
        state.length = here_val;
        if (here_op === 0) {
          //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ?
          //        "inflate:         literal '%c'\n" :
          //        "inflate:         literal 0x%02x\n", here.val));
          state.mode = LIT$1;
          break;
        }
        if (here_op & 32) {
          //Tracevv((stderr, "inflate:         end of block\n"));
          state.back = -1;
          state.mode = TYPE$2;
          break;
        }
        if (here_op & 64) {
          strm.msg = 'invalid literal/length code';
          state.mode = BAD$2;
          break;
        }
        state.extra = here_op & 15;
        state.mode = LENEXT$1;
        /* falls through */
      case LENEXT$1:
        if (state.extra) {
          //=== NEEDBITS(state.extra);
          n = state.extra;
          while (bits < n) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          state.length += hold & ((1 << state.extra) - 1)/*BITS(state.extra)*/;
          //--- DROPBITS(state.extra) ---//
          hold >>>= state.extra;
          bits -= state.extra;
          //---//
          state.back += state.extra;
        }
        //Tracevv((stderr, "inflate:         length %u\n", state.length));
        state.was = state.length;
        state.mode = DIST$1;
        /* falls through */
      case DIST$1:
        for (;;) {
          here = state.distcode[hold & ((1 << state.distbits) - 1)];/*BITS(state.distbits)*/
          here_bits = here >>> 24;
          here_op = (here >>> 16) & 0xff;
          here_val = here & 0xffff;

          if ((here_bits) <= bits) { break; }
          //--- PULLBYTE() ---//
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
          //---//
        }
        if ((here_op & 0xf0) === 0) {
          last_bits = here_bits;
          last_op = here_op;
          last_val = here_val;
          for (;;) {
            here = state.distcode[last_val +
                    ((hold & ((1 << (last_bits + last_op)) - 1))/*BITS(last.bits + last.op)*/ >> last_bits)];
            here_bits = here >>> 24;
            here_op = (here >>> 16) & 0xff;
            here_val = here & 0xffff;

            if ((last_bits + here_bits) <= bits) { break; }
            //--- PULLBYTE() ---//
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
            //---//
          }
          //--- DROPBITS(last.bits) ---//
          hold >>>= last_bits;
          bits -= last_bits;
          //---//
          state.back += last_bits;
        }
        //--- DROPBITS(here.bits) ---//
        hold >>>= here_bits;
        bits -= here_bits;
        //---//
        state.back += here_bits;
        if (here_op & 64) {
          strm.msg = 'invalid distance code';
          state.mode = BAD$2;
          break;
        }
        state.offset = here_val;
        state.extra = (here_op) & 15;
        state.mode = DISTEXT$1;
        /* falls through */
      case DISTEXT$1:
        if (state.extra) {
          //=== NEEDBITS(state.extra);
          n = state.extra;
          while (bits < n) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          state.offset += hold & ((1 << state.extra) - 1)/*BITS(state.extra)*/;
          //--- DROPBITS(state.extra) ---//
          hold >>>= state.extra;
          bits -= state.extra;
          //---//
          state.back += state.extra;
        }
//#ifdef INFLATE_STRICT
        if (state.offset > state.dmax) {
          strm.msg = 'invalid distance too far back';
          state.mode = BAD$2;
          break;
        }
//#endif
        //Tracevv((stderr, "inflate:         distance %u\n", state.offset));
        state.mode = MATCH$1;
        /* falls through */
      case MATCH$1:
        if (left === 0) { break inf_leave; }
        copy = _out - left;
        if (state.offset > copy) {         /* copy from window */
          copy = state.offset - copy;
          if (copy > state.whave) {
            if (state.sane) {
              strm.msg = 'invalid distance too far back';
              state.mode = BAD$2;
              break;
            }
// (!) This block is disabled in zlib defaults,
// don't enable it for binary compatibility
//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR
//          Trace((stderr, "inflate.c too far\n"));
//          copy -= state.whave;
//          if (copy > state.length) { copy = state.length; }
//          if (copy > left) { copy = left; }
//          left -= copy;
//          state.length -= copy;
//          do {
//            output[put++] = 0;
//          } while (--copy);
//          if (state.length === 0) { state.mode = LEN; }
//          break;
//#endif
          }
          if (copy > state.wnext) {
            copy -= state.wnext;
            from = state.wsize - copy;
          }
          else {
            from = state.wnext - copy;
          }
          if (copy > state.length) { copy = state.length; }
          from_source = state.window;
        }
        else {                              /* copy from output */
          from_source = output;
          from = put - state.offset;
          copy = state.length;
        }
        if (copy > left) { copy = left; }
        left -= copy;
        state.length -= copy;
        do {
          output[put++] = from_source[from++];
        } while (--copy);
        if (state.length === 0) { state.mode = LEN$1; }
        break;
      case LIT$1:
        if (left === 0) { break inf_leave; }
        output[put++] = state.length;
        left--;
        state.mode = LEN$1;
        break;
      case CHECK$1:
        if (state.wrap) {
          //=== NEEDBITS(32);
          while (bits < 32) {
            if (have === 0) { break inf_leave; }
            have--;
            // Use '|' instead of '+' to make sure that result is signed
            hold |= input[next++] << bits;
            bits += 8;
          }
          //===//
          _out -= left;
          strm.total_out += _out;
          state.total += _out;
          if ((state.wrap & 4) && _out) {
            strm.adler = state.check =
                /*UPDATE_CHECK(state.check, put - _out, _out);*/
                (state.flags ? crc32_1$1(state.check, output, _out, put - _out) : adler32_1$1(state.check, output, _out, put - _out));

          }
          _out = left;
          // NB: crc32 stored as signed 32-bit int, zswap32 returns signed too
          if ((state.wrap & 4) && (state.flags ? hold : zswap32$1(hold)) !== state.check) {
            strm.msg = 'incorrect data check';
            state.mode = BAD$2;
            break;
          }
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
          //Tracev((stderr, "inflate:   check matches trailer\n"));
        }
        state.mode = LENGTH$1;
        /* falls through */
      case LENGTH$1:
        if (state.wrap && state.flags) {
          //=== NEEDBITS(32);
          while (bits < 32) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          if ((state.wrap & 4) && hold !== (state.total & 0xffffffff)) {
            strm.msg = 'incorrect length check';
            state.mode = BAD$2;
            break;
          }
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
          //Tracev((stderr, "inflate:   length matches trailer\n"));
        }
        state.mode = DONE$1;
        /* falls through */
      case DONE$1:
        ret = Z_STREAM_END$1$1;
        break inf_leave;
      case BAD$2:
        ret = Z_DATA_ERROR$1$1;
        break inf_leave;
      case MEM$1:
        return Z_MEM_ERROR$1$1;
      case SYNC$1:
        /* falls through */
      default:
        return Z_STREAM_ERROR$1$1;
    }
  }

  // inf_leave <- here is real place for "goto inf_leave", emulated via "break inf_leave"

  /*
     Return from inflate(), updating the total counts and the check value.
     If there was no progress during the inflate() call, return a buffer
     error.  Call updatewindow() to create and/or update the window state.
     Note: a memory error from inflate() is non-recoverable.
   */

  //--- RESTORE() ---
  strm.next_out = put;
  strm.avail_out = left;
  strm.next_in = next;
  strm.avail_in = have;
  state.hold = hold;
  state.bits = bits;
  //---

  if (state.wsize || (_out !== strm.avail_out && state.mode < BAD$2 &&
                      (state.mode < CHECK$1 || flush !== Z_FINISH$1$1))) {
    if (updatewindow$1(strm, strm.output, strm.next_out, _out - strm.avail_out)) ;
  }
  _in -= strm.avail_in;
  _out -= strm.avail_out;
  strm.total_in += _in;
  strm.total_out += _out;
  state.total += _out;
  if ((state.wrap & 4) && _out) {
    strm.adler = state.check = /*UPDATE_CHECK(state.check, strm.next_out - _out, _out);*/
      (state.flags ? crc32_1$1(state.check, output, _out, strm.next_out - _out) : adler32_1$1(state.check, output, _out, strm.next_out - _out));
  }
  strm.data_type = state.bits + (state.last ? 64 : 0) +
                    (state.mode === TYPE$2 ? 128 : 0) +
                    (state.mode === LEN_$1 || state.mode === COPY_$1 ? 256 : 0);
  if (((_in === 0 && _out === 0) || flush === Z_FINISH$1$1) && ret === Z_OK$1$1) {
    ret = Z_BUF_ERROR$2;
  }
  return ret;
};


const inflateEnd$1 = (strm) => {

  if (inflateStateCheck$1(strm)) {
    return Z_STREAM_ERROR$1$1;
  }

  let state = strm.state;
  if (state.window) {
    state.window = null;
  }
  strm.state = null;
  return Z_OK$1$1;
};


const inflateGetHeader$1 = (strm, head) => {

  /* check state */
  if (inflateStateCheck$1(strm)) { return Z_STREAM_ERROR$1$1; }
  const state = strm.state;
  if ((state.wrap & 2) === 0) { return Z_STREAM_ERROR$1$1; }

  /* save header structure */
  state.head = head;
  head.done = false;
  return Z_OK$1$1;
};


const inflateSetDictionary$1 = (strm, dictionary) => {
  const dictLength = dictionary.length;

  let state;
  let dictid;
  let ret;

  /* check state */
  if (inflateStateCheck$1(strm)) { return Z_STREAM_ERROR$1$1; }
  state = strm.state;

  if (state.wrap !== 0 && state.mode !== DICT$1) {
    return Z_STREAM_ERROR$1$1;
  }

  /* check for correct dictionary identifier */
  if (state.mode === DICT$1) {
    dictid = 1; /* adler32(0, null, 0)*/
    /* dictid = adler32(dictid, dictionary, dictLength); */
    dictid = adler32_1$1(dictid, dictionary, dictLength, 0);
    if (dictid !== state.check) {
      return Z_DATA_ERROR$1$1;
    }
  }
  /* copy dictionary to window using updatewindow(), which will amend the
   existing dictionary if appropriate */
  ret = updatewindow$1(strm, dictionary, dictLength, dictLength);
  if (ret) {
    state.mode = MEM$1;
    return Z_MEM_ERROR$1$1;
  }
  state.havedict = 1;
  // Tracev((stderr, "inflate:   dictionary set\n"));
  return Z_OK$1$1;
};


var inflateReset_1$1 = inflateReset$1;
var inflateReset2_1$1 = inflateReset2$1;
var inflateResetKeep_1$1 = inflateResetKeep$1;
var inflateInit_1$1 = inflateInit$1;
var inflateInit2_1$1 = inflateInit2$1;
var inflate_2$1$1 = inflate$2$1;
var inflateEnd_1$1 = inflateEnd$1;
var inflateGetHeader_1$1 = inflateGetHeader$1;
var inflateSetDictionary_1$1 = inflateSetDictionary$1;
var inflateInfo$1 = 'pako inflate (from Nodeca project)';

/* Not implemented
module.exports.inflateCodesUsed = inflateCodesUsed;
module.exports.inflateCopy = inflateCopy;
module.exports.inflateGetDictionary = inflateGetDictionary;
module.exports.inflateMark = inflateMark;
module.exports.inflatePrime = inflatePrime;
module.exports.inflateSync = inflateSync;
module.exports.inflateSyncPoint = inflateSyncPoint;
module.exports.inflateUndermine = inflateUndermine;
module.exports.inflateValidate = inflateValidate;
*/

var inflate_1$2$1 = {
	inflateReset: inflateReset_1$1,
	inflateReset2: inflateReset2_1$1,
	inflateResetKeep: inflateResetKeep_1$1,
	inflateInit: inflateInit_1$1,
	inflateInit2: inflateInit2_1$1,
	inflate: inflate_2$1$1,
	inflateEnd: inflateEnd_1$1,
	inflateGetHeader: inflateGetHeader_1$1,
	inflateSetDictionary: inflateSetDictionary_1$1,
	inflateInfo: inflateInfo$1
};

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

function GZheader$1() {
  /* true if compressed data believed to be text */
  this.text       = 0;
  /* modification time */
  this.time       = 0;
  /* extra flags (not used when writing a gzip file) */
  this.xflags     = 0;
  /* operating system */
  this.os         = 0;
  /* pointer to extra field or Z_NULL if none */
  this.extra      = null;
  /* extra field length (valid if extra != Z_NULL) */
  this.extra_len  = 0; // Actually, we don't need it in JS,
                       // but leave for few code modifications

  //
  // Setup limits is not necessary because in js we should not preallocate memory
  // for inflate use constant limit in 65536 bytes
  //

  /* space at extra (only when reading header) */
  // this.extra_max  = 0;
  /* pointer to zero-terminated file name or Z_NULL */
  this.name       = '';
  /* space at name (only when reading header) */
  // this.name_max   = 0;
  /* pointer to zero-terminated comment or Z_NULL */
  this.comment    = '';
  /* space at comment (only when reading header) */
  // this.comm_max   = 0;
  /* true if there was or will be a header crc */
  this.hcrc       = 0;
  /* true when done reading gzip header (not used when writing a gzip file) */
  this.done       = false;
}

var gzheader$1 = GZheader$1;

const toString$6 = Object.prototype.toString;

/* Public constants ==========================================================*/
/* ===========================================================================*/

const {
  Z_NO_FLUSH: Z_NO_FLUSH$3, Z_FINISH: Z_FINISH$4,
  Z_OK: Z_OK$4, Z_STREAM_END: Z_STREAM_END$4, Z_NEED_DICT: Z_NEED_DICT$2, Z_STREAM_ERROR: Z_STREAM_ERROR$3, Z_DATA_ERROR: Z_DATA_ERROR$3, Z_MEM_ERROR: Z_MEM_ERROR$2
} = constants$2$1;

/* ===========================================================================*/


/**
 * class Inflate
 *
 * Generic JS-style wrapper for zlib calls. If you don't need
 * streaming behaviour - use more simple functions: [[inflate]]
 * and [[inflateRaw]].
 **/

/* internal
 * inflate.chunks -> Array
 *
 * Chunks of output data, if [[Inflate#onData]] not overridden.
 **/

/**
 * Inflate.result -> Uint8Array|String
 *
 * Uncompressed result, generated by default [[Inflate#onData]]
 * and [[Inflate#onEnd]] handlers. Filled after you push last chunk
 * (call [[Inflate#push]] with `Z_FINISH` / `true` param).
 **/

/**
 * Inflate.err -> Number
 *
 * Error code after inflate finished. 0 (Z_OK) on success.
 * Should be checked if broken data possible.
 **/

/**
 * Inflate.msg -> String
 *
 * Error message, if [[Inflate.err]] != 0
 **/


/**
 * new Inflate(options)
 * - options (Object): zlib inflate options.
 *
 * Creates new inflator instance with specified params. Throws exception
 * on bad params. Supported options:
 *
 * - `windowBits`
 * - `dictionary`
 *
 * [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced)
 * for more information on these.
 *
 * Additional options, for internal needs:
 *
 * - `chunkSize` - size of generated data chunks (16K by default)
 * - `raw` (Boolean) - do raw inflate
 * - `to` (String) - if equal to 'string', then result will be converted
 *   from utf8 to utf16 (javascript) string. When string output requested,
 *   chunk length can differ from `chunkSize`, depending on content.
 *
 * By default, when no options set, autodetect deflate/gzip data format via
 * wrapper header.
 *
 * ##### Example:
 *
 * ```javascript
 * const pako = require('pako')
 * const chunk1 = new Uint8Array([1,2,3,4,5,6,7,8,9])
 * const chunk2 = new Uint8Array([10,11,12,13,14,15,16,17,18,19]);
 *
 * const inflate = new pako.Inflate({ level: 3});
 *
 * inflate.push(chunk1, false);
 * inflate.push(chunk2, true);  // true -> last chunk
 *
 * if (inflate.err) { throw new Error(inflate.err); }
 *
 * console.log(inflate.result);
 * ```
 **/
function Inflate$1$1(options) {
  this.options = common$1.assign({
    chunkSize: 1024 * 64,
    windowBits: 15,
    to: ''
  }, options || {});

  const opt = this.options;

  // Force window size for `raw` data, if not set directly,
  // because we have no header for autodetect.
  if (opt.raw && (opt.windowBits >= 0) && (opt.windowBits < 16)) {
    opt.windowBits = -opt.windowBits;
    if (opt.windowBits === 0) { opt.windowBits = -15; }
  }

  // If `windowBits` not defined (and mode not raw) - set autodetect flag for gzip/deflate
  if ((opt.windowBits >= 0) && (opt.windowBits < 16) &&
      !(options && options.windowBits)) {
    opt.windowBits += 32;
  }

  // Gzip header has no info about windows size, we can do autodetect only
  // for deflate. So, if window size not set, force it to max when gzip possible
  if ((opt.windowBits > 15) && (opt.windowBits < 48)) {
    // bit 3 (16) -> gzipped data
    // bit 4 (32) -> autodetect gzip/deflate
    if ((opt.windowBits & 15) === 0) {
      opt.windowBits |= 15;
    }
  }

  this.err    = 0;      // error code, if happens (0 = Z_OK)
  this.msg    = '';     // error message
  this.ended  = false;  // used to avoid multiple onEnd() calls
  this.chunks = [];     // chunks of compressed data

  this.strm   = new zstream$1();
  this.strm.avail_out = 0;

  let status  = inflate_1$2$1.inflateInit2(
    this.strm,
    opt.windowBits
  );

  if (status !== Z_OK$4) {
    throw new Error(messages$1[status]);
  }

  this.header = new gzheader$1();

  inflate_1$2$1.inflateGetHeader(this.strm, this.header);

  // Setup dictionary
  if (opt.dictionary) {
    // Convert data if needed
    if (typeof opt.dictionary === 'string') {
      opt.dictionary = strings$1.string2buf(opt.dictionary);
    } else if (toString$6.call(opt.dictionary) === '[object ArrayBuffer]') {
      opt.dictionary = new Uint8Array(opt.dictionary);
    }
    if (opt.raw) { //In raw mode we need to set the dictionary early
      status = inflate_1$2$1.inflateSetDictionary(this.strm, opt.dictionary);
      if (status !== Z_OK$4) {
        throw new Error(messages$1[status]);
      }
    }
  }
}

/**
 * Inflate#push(data[, flush_mode]) -> Boolean
 * - data (Uint8Array|ArrayBuffer): input data
 * - flush_mode (Number|Boolean): 0..6 for corresponding Z_NO_FLUSH..Z_TREE
 *   flush modes. See constants. Skipped or `false` means Z_NO_FLUSH,
 *   `true` means Z_FINISH.
 *
 * Sends input data to inflate pipe, generating [[Inflate#onData]] calls with
 * new output chunks. Returns `true` on success. If end of stream detected,
 * [[Inflate#onEnd]] will be called.
 *
 * `flush_mode` is not needed for normal operation, because end of stream
 * detected automatically. You may try to use it for advanced things, but
 * this functionality was not tested.
 *
 * On fail call [[Inflate#onEnd]] with error code and return false.
 *
 * ##### Example
 *
 * ```javascript
 * push(chunk, false); // push one of data chunks
 * ...
 * push(chunk, true);  // push last chunk
 * ```
 **/
Inflate$1$1.prototype.push = function (data, flush_mode) {
  const strm = this.strm;
  const chunkSize = this.options.chunkSize;
  const dictionary = this.options.dictionary;
  let status, _flush_mode, last_avail_out;

  if (this.ended) return false;

  if (flush_mode === ~~flush_mode) _flush_mode = flush_mode;
  else _flush_mode = flush_mode === true ? Z_FINISH$4 : Z_NO_FLUSH$3;

  // Convert data if needed
  if (toString$6.call(data) === '[object ArrayBuffer]') {
    strm.input = new Uint8Array(data);
  } else {
    strm.input = data;
  }

  strm.next_in = 0;
  strm.avail_in = strm.input.length;

  for (;;) {
    if (strm.avail_out === 0) {
      strm.output = new Uint8Array(chunkSize);
      strm.next_out = 0;
      strm.avail_out = chunkSize;
    }

    status = inflate_1$2$1.inflate(strm, _flush_mode);

    if (status === Z_NEED_DICT$2 && dictionary) {
      status = inflate_1$2$1.inflateSetDictionary(strm, dictionary);

      if (status === Z_OK$4) {
        status = inflate_1$2$1.inflate(strm, _flush_mode);
      } else if (status === Z_DATA_ERROR$3) {
        // Replace code with more verbose
        status = Z_NEED_DICT$2;
      }
    }

    // Skip snyc markers if more data follows and not raw mode
    while (strm.avail_in > 0 &&
           status === Z_STREAM_END$4 &&
           strm.state.wrap > 0 &&
           data[strm.next_in] !== 0)
    {
      inflate_1$2$1.inflateReset(strm);
      status = inflate_1$2$1.inflate(strm, _flush_mode);
    }

    switch (status) {
      case Z_STREAM_ERROR$3:
      case Z_DATA_ERROR$3:
      case Z_NEED_DICT$2:
      case Z_MEM_ERROR$2:
        this.onEnd(status);
        this.ended = true;
        return false;
    }

    // Remember real `avail_out` value, because we may patch out buffer content
    // to align utf8 strings boundaries.
    last_avail_out = strm.avail_out;

    if (strm.next_out) {
      if (strm.avail_out === 0 || status === Z_STREAM_END$4) {

        if (this.options.to === 'string') {

          let next_out_utf8 = strings$1.utf8border(strm.output, strm.next_out);

          let tail = strm.next_out - next_out_utf8;
          let utf8str = strings$1.buf2string(strm.output, next_out_utf8);

          // move tail & realign counters
          strm.next_out = tail;
          strm.avail_out = chunkSize - tail;
          if (tail) strm.output.set(strm.output.subarray(next_out_utf8, next_out_utf8 + tail), 0);

          this.onData(utf8str);

        } else {
          this.onData(strm.output.length === strm.next_out ? strm.output : strm.output.subarray(0, strm.next_out));
        }
      }
    }

    // Must repeat iteration if out buffer is full
    if (status === Z_OK$4 && last_avail_out === 0) continue;

    // Finalize if end of stream reached.
    if (status === Z_STREAM_END$4) {
      status = inflate_1$2$1.inflateEnd(this.strm);
      this.onEnd(status);
      this.ended = true;
      return true;
    }

    if (strm.avail_in === 0) break;
  }

  return true;
};


/**
 * Inflate#onData(chunk) -> Void
 * - chunk (Uint8Array|String): output data. When string output requested,
 *   each chunk will be string.
 *
 * By default, stores data blocks in `chunks[]` property and glue
 * those in `onEnd`. Override this handler, if you need another behaviour.
 **/
Inflate$1$1.prototype.onData = function (chunk) {
  this.chunks.push(chunk);
};


/**
 * Inflate#onEnd(status) -> Void
 * - status (Number): inflate status. 0 (Z_OK) on success,
 *   other if not.
 *
 * Called either after you tell inflate that the input stream is
 * complete (Z_FINISH). By default - join collected chunks,
 * free memory and fill `results` / `err` properties.
 **/
Inflate$1$1.prototype.onEnd = function (status) {
  // On success - join
  if (status === Z_OK$4) {
    if (this.options.to === 'string') {
      this.result = this.chunks.join('');
    } else {
      this.result = common$1.flattenChunks(this.chunks);
    }
  }
  this.chunks = [];
  this.err = status;
  this.msg = this.strm.msg;
};


/**
 * inflate(data[, options]) -> Uint8Array|String
 * - data (Uint8Array|ArrayBuffer): input data to decompress.
 * - options (Object): zlib inflate options.
 *
 * Decompress `data` with inflate/ungzip and `options`. Autodetect
 * format via wrapper header by default. That's why we don't provide
 * separate `ungzip` method.
 *
 * Supported options are:
 *
 * - windowBits
 *
 * [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced)
 * for more information.
 *
 * Sugar (options):
 *
 * - `raw` (Boolean) - say that we work with raw stream, if you don't wish to specify
 *   negative windowBits implicitly.
 * - `to` (String) - if equal to 'string', then result will be converted
 *   from utf8 to utf16 (javascript) string. When string output requested,
 *   chunk length can differ from `chunkSize`, depending on content.
 *
 *
 * ##### Example:
 *
 * ```javascript
 * const pako = require('pako');
 * const input = pako.deflate(new Uint8Array([1,2,3,4,5,6,7,8,9]));
 * let output;
 *
 * try {
 *   output = pako.inflate(input);
 * } catch (err) {
 *   console.log(err);
 * }
 * ```
 **/
function inflate$1$1(input, options) {
  const inflator = new Inflate$1$1(options);

  inflator.push(input);

  // That will never happens, if you don't cheat with options :)
  if (inflator.err) throw inflator.msg || messages$1[inflator.err];

  return inflator.result;
}


/**
 * inflateRaw(data[, options]) -> Uint8Array|String
 * - data (Uint8Array|ArrayBuffer): input data to decompress.
 * - options (Object): zlib inflate options.
 *
 * The same as [[inflate]], but creates raw data, without wrapper
 * (header and adler32 crc).
 **/
function inflateRaw$1$1(input, options) {
  options = options || {};
  options.raw = true;
  return inflate$1$1(input, options);
}


/**
 * ungzip(data[, options]) -> Uint8Array|String
 * - data (Uint8Array|ArrayBuffer): input data to decompress.
 * - options (Object): zlib inflate options.
 *
 * Just shortcut to [[inflate]], because it autodetects format
 * by header.content. Done for convenience.
 **/


var Inflate_1$1$1 = Inflate$1$1;
var inflate_2$2 = inflate$1$1;
var inflateRaw_1$1$1 = inflateRaw$1$1;
var ungzip$1$1 = inflate$1$1;
var constants$3 = constants$2$1;

var inflate_1$1$1 = {
	Inflate: Inflate_1$1$1,
	inflate: inflate_2$2,
	inflateRaw: inflateRaw_1$1$1,
	ungzip: ungzip$1$1,
	constants: constants$3
};

const { Deflate, deflate, deflateRaw, gzip } = deflate_1$1;

const { Inflate: Inflate$2, inflate: inflate$3, inflateRaw: inflateRaw$2, ungzip: ungzip$2 } = inflate_1$1$1;
var deflateRaw_1 = deflateRaw;
var inflate_1$3 = inflate$3;
var inflateRaw_1$2 = inflateRaw$2;
var ungzip_1$1 = ungzip$2;

const FEXTRA$1 = 4;  // gzip spec F.EXTRA flag

function isgzipped$1(data) {
    const b = ArrayBuffer.isView(data) ? data : new Uint8Array(data);
    return b[0] ===31 && b[1] === 139;
}

/**
 * Pako does not properly ungzip block compressed files if > 1 block is present.  Test for bgzip and use wrapper.
 */
function ungzip_blocks$1(data) {
    const ba = ArrayBuffer.isView(data) ? data : new Uint8Array(data);
    const b = ba[3] & FEXTRA$1;
    if (b !== 0 && ba[12] === 66 && ba[13] === 67) {
        return unbgzf$1(ba.buffer);
    } else {
        return ungzip_1$1(ba);
    }
}

// Uncompress data,  assumed to be series of bgzipped blocks
function unbgzf$1(data, lim) {

    const oBlockList = [];
    let ptr = 0;
    let totalSize = 0;

    lim = lim || data.byteLength - 18;

    while (ptr < lim) {
        try {
            const ba = ArrayBuffer.isView(data) ? data : new Uint8Array(data, ptr, 18);
            const xlen = (ba[11] << 8) | (ba[10]);
            const flg = ba[3];
            const fextra = flg & FEXTRA$1;
            const si1 = ba[12];
            const si2 = ba[13];
            const slen = (ba[15] << 8) | (ba[14]);
            const bsize = ((ba[17] << 8) | (ba[16])) + 1;
            const start = 12 + xlen + ptr;    // Start of CDATA
            const bytesLeft = data.byteLength - start;
            const cDataSize = bsize - xlen - 19;
            if (bytesLeft < cDataSize || cDataSize <= 0) break;

            const a = new Uint8Array(data, start, cDataSize);
            const unc = inflateRaw_1$2(a);

            // const inflate = new Zlib.RawInflate(a);
            // const unc = inflate.decompress();

            ptr += (cDataSize - 1) + 26; //inflate.ip + 26
            totalSize += unc.byteLength;
            oBlockList.push(unc);
        } catch (e) {
            console.error(e);
            break;
        }
    }

    // Concatenate decompressed blocks
    if (oBlockList.length === 1) {
        return oBlockList[0];
    } else {
        const out = new Uint8Array(totalSize);
        let cursor = 0;
        for (let i = 0; i < oBlockList.length; ++i) {
            var b = new Uint8Array(oBlockList[i]);
            arrayCopy$1(b, 0, out, cursor, b.length);
            cursor += b.length;
        }
        return out;
    }
}

function bgzBlockSize$1(data) {
    const ba = ArrayBuffer.isView(data) ? data : new Uint8Array(data);
    const bsize = (ba[17] << 8 | ba[16]) + 1;
    return bsize;
}

// From Thomas Down's zlib implementation

const testArray$1 = new Uint8Array(1);
const hasSubarray$1 = (typeof testArray$1.subarray === 'function');

function arrayCopy$1(src, srcOffset, dest, destOffset, count) {
    if (count === 0) {
        return;
    }
    if (!src) {
        throw "Undef src";
    } else if (!dest) {
        throw "Undef dest";
    }
    if (srcOffset === 0 && count === src.length) {
        arrayCopy_fast$1(src, dest, destOffset);
    } else if (hasSubarray$1) {
        arrayCopy_fast$1(src.subarray(srcOffset, srcOffset + count), dest, destOffset);
    } else if (src.BYTES_PER_ELEMENT === 1 && count > 100) {
        arrayCopy_fast$1(new Uint8Array(src.buffer, src.byteOffset + srcOffset, count), dest, destOffset);
    } else {
        arrayCopy_slow$1(src, srcOffset, dest, destOffset, count);
    }
}

function arrayCopy_slow$1(src, srcOffset, dest, destOffset, count) {
    for (let i = 0; i < count; ++i) {
        dest[destOffset + i] = src[srcOffset + i];
    }
}

function arrayCopy_fast$1(src, dest, destOffset) {
    dest.set(src, destOffset);
}


/**
 * Compress string and encode in a url safe form
 * @param s
 */
function compressString(str) {

    const bytes = new Uint8Array(str.length);
    for (var i = 0; i < str.length; i++) {
        bytes[i] = str.charCodeAt(i);
    }
    const compressedBytes = new deflateRaw_1(bytes);            // UInt8Arry
    const compressedString = String.fromCharCode.apply(null, compressedBytes);      // Convert to string
    let enc = btoa(compressedString);
    return enc.replace(/\+/g, '.').replace(/\//g, '_').replace(/=/g, '-');   // URL safe
}

/**
 * Uncompress the url-safe encoded compressed string, presumably created by compressString above
 *
 * @param enc
 * @returns {string}
 */
function uncompressString(enc) {

    enc = enc.replace(/\./g, '+').replace(/_/g, '/').replace(/-/g, '=');

    const compressedString = atob(enc);
    const compressedBytes = [];
    for (let i = 0; i < compressedString.length; i++) {
        compressedBytes.push(compressedString.charCodeAt(i));
    }
    //const bytes = new Zlib.RawInflate(compressedBytes).decompress();
    const bytes = inflateRaw_1$2(compressedBytes);

    let str = '';
    for (let b of bytes) {
        str += String.fromCharCode(b);
    }
    return str;
}


/**
 * @param dataURI
 * @returns {Array<number>|Uint8Array}
 */
function decodeDataURI$1(dataURI, gzip) {

    const split = dataURI.split(',');
    const info = split[0].split(':')[1];
    let dataString = split[1];

    if (info.indexOf('base64') >= 0) {
        dataString = atob(dataString);

        const bytes = new Uint8Array(dataString.length);
        for (let i = 0; i < dataString.length; i++) {
            bytes[i] = dataString.charCodeAt(i);
        }

        let plain;
        if (gzip || info.indexOf('gzip') > 0) {
            plain = ungzip_1$1(bytes);
        } else {
            plain = bytes;
        }
        return plain
    } else {
        return decodeURIComponent(dataString);      // URL encoded string -- not currently used or tested
    }
}

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2016-2017 The Regents of the University of California
 * Author: Jim Robinson
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
const IGVMath = {

    lerp: (v0, v1, t) => {
        return (1 - t) * v0 + t * v1;
    },

    mean: function (array) {

        var t = 0, n = 0,
            i;
        for (i = 0; i < array.length; i++) {
            if (!isNaN(array[i])) {
                t += array[i];
                n++;
            }
        }
        return n > 0 ? t / n : 0;
    },

    meanAndStdev: function (array) {

        var v, t = 0, t2 = 0, n = 0, i;

        for (i = 0; i < array.length; i++) {

            v = array[i];

            if (!isNaN(v)) {
                t += v;
                t2 += v * v;
                n++;
            }
        }
        return n > 0 ? {mean: t / n, stdev: Math.sqrt(t2 - t * t / n)} : {mean: 0, stdev: 0};
    },

    median: function (numbers) {
        // median of [3, 5, 4, 4, 1, 1, 2, 3] = 3
        var median = 0, numsLen = numbers.length;
        numbers.sort();

        if (
            numsLen % 2 === 0 // is even
        ) {
            // average of two middle numbers
            median = (numbers[numsLen / 2 - 1] + numbers[numsLen / 2]) / 2;
        } else { // is odd
            // middle number only
            median = numbers[(numsLen - 1) / 2];
        }

        return median;
    },

    // Fast percentile function for "p" near edges.  This needs profiled for p in middle (e.g. median)
    percentile: function (array, p) {

        if (array.length === 0) return undefined;

        var k = Math.floor(array.length * ((100 - p) / 100));
        if (k === 0) {
            array.sort(function (a, b) {
                return b - a
            });
            return array[k];
        } else {
            return selectElement(array, k);
        }

    },


    clamp: function (value, min, max) {
        return Math.min(Math.max(value, min), max);
    },

    log2: function (x) {
        return Math.log(x) / Math.LN2;
    }

};

function selectElement(array, k) {

    // Credit Steve Hanov http://stevehanov.ca/blog/index.php?id=122
    var heap = new BinaryHeap(),
        i;

    for (i = 0; i < array.length; i++) {

        var item = array[i];

        // If we have not yet found k items, or the current item is larger than
        // the smallest item on the heap, add current item
        if (heap.content.length < k || item > heap.content[0]) {
            // If the heap is full, remove the smallest element on the heap.
            if (heap.content.length === k) {
                heap.pop();
            }
            heap.push(item);
        }
    }

    return heap.content[0];
}


function BinaryHeap() {
    this.content = [];
}

BinaryHeap.prototype = {
    push: function (element) {
        // Add the new element to the end of the array.
        this.content.push(element);
        // Allow it to bubble up.
        this.bubbleUp(this.content.length - 1);
    },

    pop: function () {
        // Store the first element so we can return it later.
        var result = this.content[0];
        // Get the element at the end of the array.
        var end = this.content.pop();
        // If there are any elements left, put the end element at the
        // start, and let it sink down.
        if (this.content.length > 0) {
            this.content[0] = end;
            this.sinkDown(0);
        }
        return result;
    },

    remove: function (node) {
        var length = this.content.length;
        // To remove a value, we must search through the array to find
        // it.
        for (var i = 0; i < length; i++) {
            if (this.content[i] !== node) continue;
            // When it is found, the process seen in 'pop' is repeated
            // to fill up the hole.
            var end = this.content.pop();
            // If the element we popped was the one we needed to remove,
            // we're done.
            if (i === length - 1) break;
            // Otherwise, we replace the removed element with the popped
            // one, and allow it to float up or sink down as appropriate.
            this.content[i] = end;
            this.bubbleUp(i);
            this.sinkDown(i);
            break;
        }
    },

    size: function () {
        return this.content.length;
    },

    bubbleUp: function (n) {
        // Fetch the element that has to be moved.
        var element = this.content[n], score = element;
        // When at 0, an element can not go up any further.
        while (n > 0) {
            // Compute the parent element's index, and fetch it.
            var parentN = Math.floor((n + 1) / 2) - 1,
                parent = this.content[parentN];
            // If the parent has a lesser score, things are in order and we
            // are done.
            if (score >= parent)
                break;

            // Otherwise, swap the parent with the current element and
            // continue.
            this.content[parentN] = element;
            this.content[n] = parent;
            n = parentN;
        }
    },

    sinkDown: function (n) {
        // Look up the target element and its score.
        var length = this.content.length,
            element = this.content[n],
            elemScore = element;

        while (true) {
            // Compute the indices of the child elements.
            var child2N = (n + 1) * 2, child1N = child2N - 1;
            // This is used to store the new position of the element,
            // if any.
            var swap = null;
            // If the first child exists (is inside the array)...
            if (child1N < length) {
                // Look it up and compute its score.
                var child1 = this.content[child1N],
                    child1Score = child1;
                // If the score is less than our element's, we need to swap.
                if (child1Score < elemScore)
                    swap = child1N;
            }
            // Do the same checks for the other child.
            if (child2N < length) {
                var child2 = this.content[child2N],
                    child2Score = child2;
                if (child2Score < (swap == null ? elemScore : child1Score))
                    swap = child2N;
            }

            // No need to swap further, we are done.
            if (swap == null) break;

            // Otherwise, swap and continue.
            this.content[n] = this.content[swap];
            this.content[swap] = element;
            n = swap;
        }
    }
};

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

function _random(min, max) {
    return Math.random() * (max - min) + min
}

const IGVColor = {

    rgbListFromHSV: () => {

        let s = 1;
        let accumulation = [];
        for (let v = 1; v >= 0.5; v -= .1) {
            for (let h = 0; h < 1; h += 1 / 28) {
                const r = "rgb(" + IGVColor.hsvToRgb(h, s, v).join(",") + ")";
                accumulation.push(r);
            }
        }

        // add black
        accumulation.pop();
        accumulation.push(IGVColor.rgbColor(16, 16, 16));

        return accumulation
    },

    rgbToHex: function (rgb) {
        rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
        return (rgb && rgb.length === 4) ? "#" +
            ("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) +
            ("0" + parseInt(rgb[2], 10).toString(16)).slice(-2) +
            ("0" + parseInt(rgb[3], 10).toString(16)).slice(-2) : ''
    },

    hexToRgb: function (hex) {

        var cooked = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);

        if (null === cooked) {
            return undefined
        }

        return "rgb(" + parseInt(cooked[1], 16) + "," + parseInt(cooked[2], 16) + "," + parseInt(cooked[3], 16) + ")"
    },

    /**
     * Converts an HSV color value to RGB. Conversion formula
     * adapted from http://en.wikipedia.org/wiki/HSV_color_space.
     * Assumes h, s, and v are contained in the set [0, 1] and
     * returns r, g, and b in the set [0, 255].
     *
     * Credit: https://gist.githubusercontent.com/mjackson/5311256
     *
     * @param   h       The hue
     * @param   s       The saturation
     * @param   v       The value
     * @return  Array   The RGB representation
     */
    hsvToRgb: function (h, s, v) {
        var r, g, b;

        var i = Math.floor(h * 6);
        var f = h * 6 - i;
        var p = v * (1 - s);
        var q = v * (1 - f * s);
        var t = v * (1 - (1 - f) * s);

        switch (i % 6) {
            case 0:
                r = v, g = t, b = p;
                break
            case 1:
                r = q, g = v, b = p;
                break
            case 2:
                r = p, g = v, b = t;
                break
            case 3:
                r = p, g = q, b = v;
                break
            case 4:
                r = t, g = p, b = v;
                break
            case 5:
                r = v, g = p, b = q;
                break
        }

        return [Math.floor(r * 255), Math.floor(g * 255), Math.floor(b * 255)]
    },

    /**
     * Converts an HSL color value to RGB. Conversion formula
     * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
     * Assumes h, s, and l are contained in the set [0, 1] and
     * returns r, g, and b in the set [0, 255].
     *
     * Credit: https://gist.githubusercontent.com/mjackson/5311256
     *
     * @param   h       The hue
     * @param   s       The saturation
     * @param   l       The lightness
     * @return  Array   The RGB representation
     */
    hslToRgb: function (h, s, l) {
        var r, g, b;

        if (s === 0) {
            r = g = b = l; // achromatic
        } else {


            var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
            var p = 2 * l - q;

            r = IGVColor.hue2rgb(p, q, h + 1 / 3);
            g = IGVColor.hue2rgb(p, q, h);
            b = IGVColor.hue2rgb(p, q, h - 1 / 3);
        }

        return [r * 255, g * 255, b * 255]
    },

    hue2rgb: (p, q, t) => {
        if (t < 0) t += 1;
        if (t > 1) t -= 1;
        if (t < 1 / 6) return p + (q - p) * 6 * t
        if (t < 1 / 2) return q
        if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
        return p
    },

    rgbaColor: function (r, g, b, a) {

        r = IGVMath.clamp(r, 0, 255);
        g = IGVMath.clamp(g, 0, 255);
        b = IGVMath.clamp(b, 0, 255);
        a = IGVMath.clamp(a, 0.0, 1.0);

        return "rgba(" + r + "," + g + "," + b + "," + a + ")"
    },

    rgbColor: function (r, g, b) {

        r = IGVMath.clamp(r, 0, 255);
        g = IGVMath.clamp(g, 0, 255);
        b = IGVMath.clamp(b, 0, 255);

        return "rgb(" + r + "," + g + "," + b + ")"
    },

    greyScale: function (value) {

        var grey = IGVMath.clamp(value, 0, 255);

        return "rgb(" + grey + "," + grey + "," + grey + ")"
    },

    randomGrey: function (min, max) {

        min = IGVMath.clamp(min, 0, 255);
        max = IGVMath.clamp(max, 0, 255);

        var g = Math.round(_random(min, max)).toString(10);

        return "rgb(" + g + "," + g + "," + g + ")"
    },

    randomRGB: function (min, max) {

        min = IGVMath.clamp(min, 0, 255);
        max = IGVMath.clamp(max, 0, 255);

        var r = Math.round(_random(min, max)).toString(10);
        var g = Math.round(_random(min, max)).toString(10);
        var b = Math.round(_random(min, max)).toString(10);

        return "rgb(" + r + "," + g + "," + b + ")"
    },

    randomRGBConstantAlpha: function (min, max, alpha) {

        min = IGVMath.clamp(min, 0, 255);
        max = IGVMath.clamp(max, 0, 255);

        var r = Math.round(_random(min, max)).toString(10);
        var g = Math.round(_random(min, max)).toString(10);
        var b = Math.round(_random(min, max)).toString(10);

        return "rgba(" + r + "," + g + "," + b + "," + alpha + ")"
    },

    addAlpha: function (color, alpha) {

        if (color === "0" || color === ".") {
            color = "rgb(0,0,0)";
        } else {
            const c = this.colorNameToHex(color);
            if (c) {
                color = c;
            }
        }

        var isHex = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(color);

        if (color.startsWith("rgba")) {
            const idx = color.lastIndexOf(",");
            return color.substring(0, idx + 1) + alpha.toString() + ")"
        }

        if (isHex) {
            color = IGVColor.hexToRgb(color);
        }

        if (color.startsWith("rgb")) {
            return color.replace("rgb", "rgba").replace(")", ", " + alpha + ")")
        } else {
            return color
        }
    },

    rgbComponents: function (color) {

        if (color === "0" || color === ".") {
            return [0, 0, 0]
        }
        const isHex = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(color);
        if (isHex) {
            color = IGVColor.hexToRgb(color);
        } else {
            if (!color.startsWith("rgb")) {
                const hex = this.colorNameToHex(color);
                color = this.hexToRgb(hex);
            }
        }

        if (color.startsWith("rgb(")) {
            return color.substring(4, color.length - 1).split(",").map(s => Number.parseInt(s.trim()))
        } else if (color.startsWith("rgba(")) {
            return color.substring(5, color.length - 1).split(",").map((s, i) => {
                s = s.trim();
                return i === 3 ? Number.parseFloat(s) : Number.parseInt(s)
            })
        } else {
            throw Error("Unrecognized color string: color")
        }
    },

    /**
     *
     * @param dest  RGB components as an array
     * @param src  RGB components as an array
     * @param alpha   alpha transparancy in the range 0-1
     * @returns {}
     */
    getCompositeColor: function (dest, src, alpha) {

        var r = Math.floor(alpha * src[0] + (1 - alpha) * dest[0]),
            g = Math.floor(alpha * src[1] + (1 - alpha) * dest[1]),
            b = Math.floor(alpha * src[2] + (1 - alpha) * dest[2]);

        return "rgb(" + r + "," + g + "," + b + ")"

    },

    /**
     * Return a color represented by the string.  If the string is not a recognized color format return the
     * string itself.  This function exists for backward compatibility,  createColorStringSafe is preferredz
     * @param str
     */
    createColorString: function (str) {
        // Excel will quote color strings, strip all quotes
        const color = this.createColorStringSafe(str);
        return color ? color : str
    },

    /**
     * If str is a recognized color format return a string encoding the color
     * @param str
     */
    createColorStringSafe: function (str) {
        // Excel will quote color strings, strip all quotes
        str = stripQuotes$2(str);

        if (str.startsWith('rgb(') && str.endsWith(')')) {
            return str
        }
        if (str.startsWith('#') && str.length < 8) {
            return str
        }

        // See if string is a recognized web color
        const hex = this.colorNameToHex(str);
        if (hex) {
            return hex
        }

        // UCSC format (e.g.  0,0,255)
        const tokens = str.split(",");
        if (tokens.length === 3 && isColorComponent(tokens[0]) && isColorComponent(tokens[1]) && isColorComponent(tokens[2])) {
            return `rgb(${str})`
        }
    },

    darkenLighten: function (color, amt) {

        let src;
        let hexColor = this.colorNameToHex(color);
        if (hexColor) {
            src = IGVColor.hexToRgb(hexColor);
        } else {
            src = color.startsWith('rgb(') ? color : IGVColor.hexToRgb(color);
        }

        const components = src.replace(")", "").substring(4).split(",");

        const r = Math.max(0, Math.min(255, Number.parseInt(components[0].trim()) + amt));
        const g = Math.max(0, Math.min(255, Number.parseInt(components[1].trim()) + amt));
        const b = Math.max(0, Math.min(255, Number.parseInt(components[2].trim()) + amt));

        return 'rgb(' + r.toString() + ',' + g.toString() + ',' + b.toString() + ')'

    },

    /**
     * Convert html/css color name to hex value.  Adapted from https://gist.github.com/mxfh/4719348
     * @param colorName
     * @returns {*}
     */
    colorNameToHex: function (colorName) { // color list from http://stackoverflow.com/q/1573053/731179  with added gray/gray
        const definedColorNames = {
            "aliceblue": "#f0f8ff",
            "antiquewhite": "#faebd7",
            "aqua": "#00ffff",
            "aquamarine": "#7fffd4",
            "azure": "#f0ffff",
            "beige": "#f5f5dc",
            "bisque": "#ffe4c4",
            "black": "#000000",
            "blanchedalmond": "#ffebcd",
            "blue": "#0000ff",
            "blueviolet": "#8a2be2",
            "brown": "#a52a2a",
            "burlywood": "#deb887",
            "cadetblue": "#5f9ea0",
            "chartreuse": "#7fff00",
            "chocolate": "#d2691e",
            "coral": "#ff7f50",
            "cornflowerblue": "#6495ed",
            "cornsilk": "#fff8dc",
            "crimson": "#dc143c",
            "cyan": "#00ffff",
            "darkblue": "#00008b",
            "darkcyan": "#008b8b",
            "darkgoldenrod": "#b8860b",
            "darkgray": "#a9a9a9",
            "darkgreen": "#006400",
            "darkkhaki": "#bdb76b",
            "darkmagenta": "#8b008b",
            "darkolivegreen": "#556b2f",
            "darkorange": "#ff8c00",
            "darkorchid": "#9932cc",
            "darkred": "#8b0000",
            "darksalmon": "#e9967a",
            "darkseagreen": "#8fbc8f",
            "darkslateblue": "#483d8b",
            "darkslategray": "#2f4f4f",
            "darkturquoise": "#00ced1",
            "darkviolet": "#9400d3",
            "deeppink": "#ff1493",
            "deepskyblue": "#00bfff",
            "dimgray": "#696969",
            "dodgerblue": "#1e90ff",
            "firebrick": "#b22222",
            "floralwhite": "#fffaf0",
            "forestgreen": "#228b22",
            "fuchsia": "#ff00ff",
            "gainsboro": "#dcdcdc",
            "ghostwhite": "#f8f8ff",
            "gold": "#ffd700",
            "goldenrod": "#daa520",
            "gray": "#808080",
            "green": "#008000",
            "greenyellow": "#adff2f",
            "honeydew": "#f0fff0",
            "hotpink": "#ff69b4",
            "indianred ": "#cd5c5c",
            "indigo ": "#4b0082",
            "ivory": "#fffff0",
            "khaki": "#f0e68c",
            "lavender": "#e6e6fa",
            "lavenderblush": "#fff0f5",
            "lawngreen": "#7cfc00",
            "lemonchiffon": "#fffacd",
            "lightblue": "#add8e6",
            "lightcoral": "#f08080",
            "lightcyan": "#e0ffff",
            "lightgoldenrodyellow": "#fafad2",
            "lightgrey": "#d3d3d3",
            "lightgreen": "#90ee90",
            "lightpink": "#ffb6c1",
            "lightsalmon": "#ffa07a",
            "lightseagreen": "#20b2aa",
            "lightskyblue": "#87cefa",
            "lightslategray": "#778899",
            "lightsteelblue": "#b0c4de",
            "lightyellow": "#ffffe0",
            "lime": "#00ff00",
            "limegreen": "#32cd32",
            "linen": "#faf0e6",
            "magenta": "#ff00ff",
            "maroon": "#800000",
            "mediumaquamarine": "#66cdaa",
            "mediumblue": "#0000cd",
            "mediumorchid": "#ba55d3",
            "mediumpurple": "#9370d8",
            "mediumseagreen": "#3cb371",
            "mediumslateblue": "#7b68ee",
            "mediumspringgreen": "#00fa9a",
            "mediumturquoise": "#48d1cc",
            "mediumvioletred": "#c71585",
            "midnightblue": "#191970",
            "mintcream": "#f5fffa",
            "mistyrose": "#ffe4e1",
            "moccasin": "#ffe4b5",
            "navajowhite": "#ffdead",
            "navy": "#000080",
            "oldlace": "#fdf5e6",
            "olive": "#808000",
            "olivedrab": "#6b8e23",
            "orange": "#ffa500",
            "orangered": "#ff4500",
            "orchid": "#da70d6",
            "palegoldenrod": "#eee8aa",
            "palegreen": "#98fb98",
            "paleturquoise": "#afeeee",
            "palevioletred": "#d87093",
            "papayawhip": "#ffefd5",
            "peachpuff": "#ffdab9",
            "peru": "#cd853f",
            "pink": "#ffc0cb",
            "plum": "#dda0dd",
            "powderblue": "#b0e0e6",
            "purple": "#800080",
            "red": "#ff0000",
            "rosybrown": "#bc8f8f",
            "royalblue": "#4169e1",
            "saddlebrown": "#8b4513",
            "salmon": "#fa8072",
            "sandybrown": "#f4a460",
            "seagreen": "#2e8b57",
            "seashell": "#fff5ee",
            "sienna": "#a0522d",
            "silver": "#c0c0c0",
            "skyblue": "#87ceeb",
            "slateblue": "#6a5acd",
            "slategray": "#708090",
            "snow": "#fffafa",
            "springgreen": "#00ff7f",
            "steelblue": "#4682b4",
            "tan": "#d2b48c",
            "teal": "#008080",
            "thistle": "#d8bfd8",
            "tomato": "#ff6347",
            "turquoise": "#40e0d0",
            "violet": "#ee82ee",
            "wheat": "#f5deb3",
            "white": "#ffffff",
            "whitesmoke": "#f5f5f5",
            "yellow": "#ffff00",
            "yellowgreen": "#9acd32",
            "darkgrey": "#a9a9a9",
            "darkslategrey": "#2f4f4f",
            "dimgrey": "#696969",
            "grey": "#808080",
            "lightgray": "#d3d3d3",
            "lightslategrey": "#778899",
            "slategrey": "#708090"
        };
        return definedColorNames[colorName]
    }
};

function isColorComponent(str) {
    const num = Number.parseInt(str);
    return !Number.isNaN(num) && num >= 0 && num <= 255
}

// Support for oauth token based authorization
// This class supports explicit setting of an oauth token either globally or for specific hosts.
//
// The variable oauth.google.access_token, which becomes igv.oauth.google.access_token on ES5 conversion is
// supported for backward compatibility

const DEFAULT_HOST$1 = "googleapis";

class Oauth$1 {

    constructor() {
        this.oauthTokens = {};
    }


    setToken(token, host) {
        host = host || DEFAULT_HOST$1;
        this.oauthTokens[host] = token;
    }

    getToken(host) {
        host = host || DEFAULT_HOST$1;
        let token;
        for (let key of Object.keys(this.oauthTokens)) {
            const regex = wildcardToRegExp$1(key);
            if (regex.test(host)) {
                token = this.oauthTokens[key];
                break
            }
        }
        return token
    }

    removeToken(host) {
        host = host || DEFAULT_HOST$1;
        for (let key of Object.keys(this.oauthTokens)) {
            const regex = wildcardToRegExp$1(key);
            if (regex.test(host)) {
                this.oauthTokens[key] = undefined;
            }
        }
    }

    // Special object for google -- legacy support
    // google: {
    //     setToken: function (token) {
    //         oauth.setToken(token);
    //     }
    // }
}


/**
 * Creates a RegExp from the given string, converting asterisks to .* expressions,
 * and escaping all other characters.
 *
 * credit https://gist.github.com/donmccurdy/6d073ce2c6f3951312dfa45da14a420f
 */
function wildcardToRegExp$1(s) {
    return new RegExp('^' + s.split(/\*+/).map(regExpEscape$1).join('.*') + '$')
}

/**
 * RegExp-escapes all characters in the given string.
 *
 * credit https://gist.github.com/donmccurdy/6d073ce2c6f3951312dfa45da14a420f
 */
function regExpEscape$1(s) {
    return s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
}

function isGoogleURL$1(url) {
    return (url.includes("googleapis") && !url.includes("urlshortener")) ||
        isGoogleStorageURL$1(url) ||
        isGoogleDriveURL$1(url)
}

function isGoogleStorageURL$1(url) {
    return url.startsWith("gs://") ||
        url.startsWith("https://www.googleapis.com/storage") ||
        url.startsWith("https://storage.cloud.google.com") ||
        url.startsWith("https://storage.googleapis.com");
}

function isGoogleDriveURL$1(url) {
    return url.indexOf("drive.google.com") >= 0 || url.indexOf("www.googleapis.com/drive") > 0
}

/**
 * Translate gs:// urls to https
 * See https://cloud.google.com/storage/docs/json_api/v1
 * @param gsUrl
 * @returns {string|*}
 */
function translateGoogleCloudURL$1(gsUrl) {

    let {bucket, object} = parseBucketName$1(gsUrl);
    object = encode$1(object);

    const qIdx = gsUrl.indexOf('?');
    const paramString = (qIdx > 0) ? gsUrl.substring(qIdx) + "&alt=media" : "?alt=media";

    return `https://storage.googleapis.com/storage/v1/b/${bucket}/o/${object}${paramString}`
}

/**
 * Parse a google bucket and object name from a google storage URL.  Known forms include
 *
 * gs://BUCKET_NAME/OBJECT_NAME
 * https://storage.googleapis.com/BUCKET_NAME/OBJECT_NAME
 * https://storage.googleapis.com/storage/v1/b/BUCKET_NAME/o/OBJECT_NAME
 * https://www.googleapis.com/storage/v1/b/BUCKET_NAME/o/OBJECT_NAME"
 * https://storage.googleapis.com/download/storage/v1/b/BUCKET_NAME/o/OBJECT_NAME
 *
 * @param url
 */
function parseBucketName$1(url) {

    let bucket;
    let object;

    if (url.startsWith("gs://")) {
        const i = url.indexOf('/', 5);
        if (i >= 0) {
            bucket = url.substring(5, i);
            const qIdx = url.indexOf('?');
            object = (qIdx < 0) ? url.substring(i + 1) : url.substring(i + 1, qIdx);
        }

    } else if (url.startsWith("https://storage.googleapis.com") || url.startsWith("https://storage.cloud.google.com")) {
        const bucketIdx = url.indexOf("/v1/b/", 8);
        if (bucketIdx > 0) {
            const objIdx = url.indexOf("/o/", bucketIdx);
            if (objIdx > 0) {
                const queryIdx = url.indexOf("?", objIdx);
                bucket = url.substring(bucketIdx + 6, objIdx);
                object = queryIdx > 0 ? url.substring(objIdx + 3, queryIdx) : url.substring(objIdx + 3);
            }

        } else {
            const idx1 = url.indexOf("/", 8);
            const idx2 = url.indexOf("/", idx1+1);
            const idx3 = url.indexOf("?", idx2);
            if (idx2 > 0) {
                bucket = url.substring(idx1+1, idx2);
                object = idx3 < 0 ? url.substring(idx2+1) : url.substring(idx2+1, idx3);
            }
        }

    } else if (url.startsWith("https://www.googleapis.com/storage/v1/b")) {
        const bucketIdx = url.indexOf("/v1/b/", 8);
        const objIdx = url.indexOf("/o/", bucketIdx);
        if (objIdx > 0) {
            const queryIdx = url.indexOf("?", objIdx);
            bucket = url.substring(bucketIdx + 6, objIdx);
            object = queryIdx > 0 ? url.substring(objIdx + 3, queryIdx) : url.substring(objIdx + 3);
        }
    }

    if (bucket && object) {
        return {
            bucket, object
        }
    } else {
        throw Error(`Unrecognized Google Storage URI: ${url}`)
    }

}

/**
 * Percent a GCS object name.  See https://cloud.google.com/storage/docs/request-endpoints
 * Specific characters to encode:
 *   !, #, $, &, ', (, ), *, +, ,, /, :, ;, =, ?, @, [, ], and space characters.
 * @param obj
 */

function encode$1(objectName) {

    let result = '';
    objectName.split('').forEach(function(letter) {
        if(encodings$1$1.has(letter)) {
            result += encodings$1$1.get(letter);
        } else {
            result += letter;
        }
    });
    return result;
}

//	%23	%24	%25	%26	%27	%28	%29	%2A	%2B	%2C	%2F	%3A	%3B	%3D	%3F	%40	%5B	%5D
const encodings$1$1 = new Map();
encodings$1$1.set("!", "%21");
encodings$1$1.set("#", "%23");
encodings$1$1.set("$", "%24");
encodings$1$1.set("%", "%25");
encodings$1$1.set("&", "%26");
encodings$1$1.set("'", "%27");
encodings$1$1.set("(", "%28");
encodings$1$1.set(")", "%29");
encodings$1$1.set("*", "%2A");
encodings$1$1.set("+", "%2B");
encodings$1$1.set(",", "%2C");
encodings$1$1.set("/", "%2F");
encodings$1$1.set(":", "%3A");
encodings$1$1.set(";", "%3B");
encodings$1$1.set("=", "%3D");
encodings$1$1.set("?", "%3F");
encodings$1$1.set("@", "%40");
encodings$1$1.set("[", "%5B");
encodings$1$1.set("]", "%5D");
encodings$1$1.set(" ", "%20");

// Convenience functions for the gapi oAuth library.

async function init$2(config) {

    if (!(google.accounts.oauth2.initTokenClient)) {
        throw new Error("Google accounts token client not loaded (https://accounts.google.com/gsi/client)")
    }

    if (isInitialized$2()) {
        throw new Error("Google client is already initialized")
    }

    // Note: callback is added when accessToken is requested
    const codeClientConfig = {
        client_id: config.client_id || config.clientId,
        scope: config.scope || 'https://www.googleapis.com/auth/userinfo.profile',
        state: config.state || 'igv',
        error: (err) => {
            throw new Error(err.type)
        },
        hint: config.hint,     // Optional
        hosted_domain: config.hosted_domain  // Optional
    };

    const tokenClient = google.accounts.oauth2.initTokenClient(codeClientConfig);

    // Attach an object to keep igv state
    google.igv = {
        tokenClient: tokenClient,
        apiKey: config.apiKey
    };
}

function isInitialized$2() {
    return window.google && window.google.igv
}

/**
 * Return the current access token if the user is signed in, or undefined otherwise.  This function does not
 * attempt a signIn or request any specfic scopes.
 *
 * @returns access_token || undefined
 */
function getCurrentAccessToken$2() {
    return (isInitialized$2() && google.igv.tokenResponse && Date.now() < google.igv.tokenExpiresAt) ?
        google.igv.tokenResponse.access_token :
        undefined
}


let promise$1;
/**
 * Return a promise for an access token for the given scope.  If the user hasn't authorized the scope request it
 *
 * @param scope
 * @returns {Promise<unknown>}
 */
async function getAccessToken$2(scope) {

    if (!isInitialized$2()) {
        throw Error("Google oAuth has not been initialized")
    }

    if (google.igv.tokenResponse &&
        Date.now() < google.igv.tokenExpiresAt &&
        google.accounts.oauth2.hasGrantedAllScopes(google.igv.tokenResponse, scope)) {
        return google.igv.tokenResponse.access_token
    } else {
        const tokenClient = google.igv.tokenClient;
        if(!promise$1) {
            promise$1 = new Promise((resolve, reject) => {
                try {
                    // Settle this promise in the response callback for requestAccessToken()
                    tokenClient.callback = (tokenResponse) => {
                        if (tokenResponse.error !== undefined) {
                            reject(tokenResponse);
                        }
                        google.igv.tokenResponse = tokenResponse;
                        google.igv.tokenExpiresAt = Date.now() + tokenResponse.expires_in * 1000;
                        resolve(tokenResponse.access_token);
                    };
                    tokenClient.requestAccessToken({scope});
                } catch (err) {
                }
            });
        }
        return promise$1
    }
}

// gapi.auth2.getAuthInstance().isSignedIn.listen(status => {
//     const user = gapi.auth2.getAuthInstance().currentUser.get()
//     queryGoogleAuthenticationStatus(user, status)
// })

function getScopeForURL$1(url) {
    if (isGoogleDriveURL$1(url)) {
        return "https://www.googleapis.com/auth/drive.file"
    } else if (isGoogleStorageURL$1(url)) {
        return "https://www.googleapis.com/auth/devstorage.read_only"
    } else {
        return 'https://www.googleapis.com/auth/userinfo.profile'
    }
}

function getApiKey$1() {
    return google.igv.apiKey
}

/**
 * Return information about a specific google drive URL
 *
 * @param googleDriveURL
 * @returns {Promise<any>}
 */
async function getDriveFileInfo$1(googleDriveURL) {

    const id = getGoogleDriveFileID$1(googleDriveURL);
    let endPoint = "https://www.googleapis.com/drive/v3/files/" + id + "?supportsTeamDrives=true";
    const apiKey = getApiKey$1();
    if (apiKey) {
        endPoint += "&key=" + apiKey;
    }
    const response = await fetch(endPoint);
    let json = await response.json();
    if (json.error && json.error.code === 404) {
        let scope = "https://www.googleapis.com/auth/drive.readonly";
        const access_token = await getAccessToken$2(scope);
        if (access_token) {
            const response = await fetch(endPoint, {
                headers: {
                    'Authorization': `Bearer ${access_token}`
                }
            });
            json = await response.json();
            if (json.error) {
                throw Error(json.error);
            }
        } else {
            throw Error(json.error);
        }
    }
    return json;
}


function getDriveDownloadURL$1(link) {
    // Return a google drive download url for the sharable link
    //https://drive.google.com/open?id=0B-lleX9c2pZFbDJ4VVRxakJzVGM
    //https://drive.google.com/file/d/1_FC4kCeO8E3V4dJ1yIW7A0sn1yURKIX-/view?usp=sharing
    var id = getGoogleDriveFileID$1(link);
    return id ? "https://www.googleapis.com/drive/v3/files/" + id + "?alt=media&supportsTeamDrives=true" : link;
}

function getGoogleDriveFileID$1(link) {

    //https://drive.google.com/file/d/1_FC4kCeO8E3V4dJ1yIW7A0sn1yURKIX-/view?usp=sharing
    //https://www.googleapis.com/drive/v3/files/1w-tvo6p1SH4p1OaQSVxpkV_EJgGIstWF?alt=media&supportsTeamDrives=true"

    if (link.includes("/open?id=")) {
        const i1 = link.indexOf("/open?id=") + 9;
        const i2 = link.indexOf("&");
        if (i1 > 0 && i2 > i1) {
            return link.substring(i1, i2)
        } else if (i1 > 0) {
            return link.substring(i1);
        }

    } else if (link.includes("/file/d/")) {
        const i1 = link.indexOf("/file/d/") + 8;
        const i2 = link.lastIndexOf("/");
        return link.substring(i1, i2);

    } else if (link.startsWith("https://www.googleapis.com/drive")) {
        let i1 = link.indexOf("/files/");
        const i2 = link.indexOf("?");
        if (i1 > 0) {
            i1 += 7;
            return i2 > 0 ?
                link.substring(i1, i2) :
                link.substring(i1)
        }
    }

    throw Error("Unknown Google Drive url format: " + link);


}

// The MIT License (MIT)

/**
 * @constructor
 * @param {Object} options A set op options to pass to the throttle function
 *        @param {number} requestsPerSecond The amount of requests per second
 *                                          the library will limit to
 */
class Throttle$1 {
    constructor(options) {
        this.requestsPerSecond = options.requestsPerSecond || 10;
        this.lastStartTime = 0;
        this.queued = [];
    }

    /**
     * Adds a promise
     * @param {Function} async function to be executed
     * @param {Object} options A set of options.
     * @return {Promise} A promise
     */
    add(asyncFunction, options) {

        var self = this;
        return new Promise(function (resolve, reject) {
            self.queued.push({
                resolve: resolve,
                reject: reject,
                asyncFunction: asyncFunction,
            });
            self.dequeue();
        });
    }

    /**
     * Adds all the promises passed as parameters
     * @param {Function} promises An array of functions that return a promise
     * @param {Object} options A set of options.
     * @param {number} options.signal An AbortSignal object that can be used to abort the returned promise
     * @param {number} options.weight A "weight" of each operation resolving by array of promises
     * @return {Promise} A promise that succeeds when all the promises passed as options do
     */
    addAll(promises, options) {
        var addedPromises = promises.map(function (promise) {
            return this.add(promise, options);
        }.bind(this));

        return Promise.all(addedPromises);
    };

    /**
     * Dequeues a promise
     * @return {void}
     */
    dequeue() {
        if (this.queued.length > 0) {
            var now = new Date(),
                inc = (1000 / this.requestsPerSecond) + 1,
                elapsed = now - this.lastStartTime;

            if (elapsed >= inc) {
                this._execute();
            } else {
                // we have reached the limit, schedule a dequeue operation
                setTimeout(function () {
                    this.dequeue();
                }.bind(this), inc - elapsed);
            }
        }
    }

    /**
     * Executes the promise
     * @private
     * @return {void}
     */
    async _execute() {
        this.lastStartTime = new Date();
        var candidate = this.queued.shift();
        const f = candidate.asyncFunction;
        try {
            const r = await f();
            candidate.resolve(r);
        } catch (e) {
            candidate.reject(e);
        }

    }


}

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

class IGVXhr$1 {

    constructor() {
        this.apiKey = undefined;
        this.googleThrottle = new Throttle$1({
            requestsPerSecond: 8
        });
        this.RANGE_WARNING_GIVEN = false;
        this.oauth = new Oauth$1();
    }

    setApiKey(key) {
        this.apiKey = key;
    }

    async loadArrayBuffer(url, options) {
        options = options || {};
        if (!options.responseType) {
            options.responseType = "arraybuffer";
        }
        if (isFile$1(url)) {
            return this._loadFileSlice(url, options)
        } else {
            return this.load(url, options)
        }
    }

    /**
     * A wrapper around loadArrayBuffer that inflates gzipped data
     * @param url
     * @param options
     * @returns {Promise<Uint8Array>}
     */
     async loadByteArray(url, options) {
        const arraybuffer = await this.loadArrayBuffer(url, options);
        let plain;
        if (isgzipped$1(arraybuffer)) {
            plain = ungzip_blocks$1(arraybuffer);
        } else {
            plain = new Uint8Array(arraybuffer);
        }
        return plain
    }


    async loadJson(url, options) {
        options = options || {};
        const method = options.method || (options.sendData ? "POST" : "GET");
        if (method === "POST") {
            options.contentType = "application/json";
        }
        const result = await this.loadString(url, options);
        if (result) {
            return JSON.parse(result)
        } else {
            return result
        }
    }

    async loadString(path, options) {
        options = options || {};
        if (path instanceof File) {
            return this._loadStringFromFile(path, options)
        } else {
            return this._loadStringFromUrl(path, options)
        }
    }

    async load(url, options) {

        options = options || {};
        const urlType = typeof url;

        // Resolve functions, promises, and functions that return promises
        url = await (typeof url === 'function' ? url() : url);

        if (isFile$1(url)) {
            return this._loadFileSlice(url, options)
        } else if (typeof url.startsWith === 'function') {   // Test for string
            if (url.startsWith("data:")) {
                const buffer = decodeDataURI$1(url).buffer;
                if (options.range) {
                    const rangeEnd = options.range.size ? options.range.start + options.range.size : buffer.byteLength;
                    return buffer.slice(options.range.start, rangeEnd)
                } else {
                    return buffer
                }
            } else {
                if (url.startsWith("https://drive.google.com")) {
                    url = getDriveDownloadURL$1(url);
                }
                if (isGoogleDriveURL$1(url) || url.startsWith("https://www.dropbox.com")) {
                    return this.googleThrottle.add(async () => {
                        return this._loadURL(url, options)
                    })
                } else {
                    return this._loadURL(url, options)
                }
            }
        } else {
            throw Error(`url must be either a 'File', 'string', 'function', or 'Promise'.  Actual type: ${urlType}`)
        }
    }

    async _loadURL(url, options) {

        const self = this;
        const _url = url;   // The unmodified URL, needed in case of an oAuth retry

        url = mapUrl$1(url);

        options = options || {};

        let oauthToken = options.oauthToken || this.getOauthToken(url);
        if (oauthToken) {
            oauthToken = await (typeof oauthToken === 'function' ? oauthToken() : oauthToken);
        }

        return new Promise(function (resolve, reject) {

            // Various Google tansformations
            if (isGoogleURL$1(url) && !isGoogleStorageSigned$1(url)) {
                if (isGoogleStorageURL$1(url)) {
                    url = translateGoogleCloudURL$1(url);
                }
                url = addApiKey$1(url);

                if (isGoogleDriveURL$1(url)) {
                    addTeamDrive$1(url);
                }

                // If we have an access token try it, but don't force a signIn or request for scopes yet
                if (!oauthToken) {
                    oauthToken = getCurrentGoogleAccessToken$1();
                }
            }

            const headers = options.headers || {};
            if (oauthToken) {
                addOauthHeaders$1(headers, oauthToken);
            }
            const range = options.range;


            const xhr = new XMLHttpRequest();
            const sendData = options.sendData || options.body;
            const method = options.method || (sendData ? "POST" : "GET");
            const responseType = options.responseType;
            const contentType = options.contentType;
            const mimeType = options.mimeType;

            xhr.open(method, url);

            if (options.timeout) {
                xhr.timeout = options.timeout;
            }

            if (range) {
                let rangeEnd = "";
                if (range.size) {
                    rangeEnd = range.start + range.size - 1;
                }
                xhr.setRequestHeader("Range", "bytes=" + range.start + "-" + rangeEnd);
                //      xhr.setRequestHeader("Cache-Control", "no-cache");    <= This can cause CORS issues, disabled for now
            }
            if (contentType) {
                xhr.setRequestHeader("Content-Type", contentType);
            }
            if (mimeType) {
                xhr.overrideMimeType(mimeType);
            }
            if (responseType) {
                xhr.responseType = responseType;
            }
            if (headers) {
                for (let key of Object.keys(headers)) {
                    const value = headers[key];
                    xhr.setRequestHeader(key, value);
                }
            }

            // NOTE: using withCredentials with servers that return "*" for access-allowed-origin will fail
            if (options.withCredentials === true) {
                xhr.withCredentials = true;
            }

            xhr.onload = async function (event) {

                // when the url points to a local file, the status is 0
                if (xhr.status === 0 || (xhr.status >= 200 && xhr.status <= 300)) {
                    if ("HEAD" === options.method) {
                        // Support fetching specific headers.  Attempting to fetch all headers can be problematic with CORS
                        const headers = options.requestedHeaders || ['content-length'];
                        const headerMap = {};
                        for (let h of headers) {
                            headerMap[h] = xhr.getResponseHeader(h);
                        }
                        resolve(headerMap);
                    } else {
                        // Assume "GET" or "POST"
                        if (range && xhr.status !== 206 && range.start !== 0) {

                            // For small files a range starting at 0 can return the whole file => 200
                            // Provide just the slice we asked for, throw out the rest quietly
                            // If file is large warn user
                            if (xhr.response.length > 100000 && !self.RANGE_WARNING_GIVEN) {
                                alert(`Warning: Range header ignored for URL: ${url}.  This can have severe performance impacts.`);
                            }
                            resolve(xhr.response.slice(range.start, range.start + range.size));
                        } else {
                            resolve(xhr.response);
                        }
                    }
                } else if (xhr.status === 416) {
                    handleError(Error(`416 Unsatisfiable Range`));
                } else if (isInitialized$2() &&
                    ((xhr.status === 404 || xhr.status === 401 || xhr.status === 403) &&
                        isGoogleURL$1(url)) &&
                    !options.retries) {
                    tryGoogleAuth();

                } else {
                    if (xhr.status === 403) {
                        handleError("Access forbidden: " + url);
                    } else {
                        handleError(xhr.status);
                    }
                }
            };


            xhr.onerror = function (event) {
                if (isGoogleURL$1(url) && !options.retries) {
                    tryGoogleAuth();
                } else {
                    handleError("Error accessing resource: " + url + " Status: " + xhr.status);
                }
            };

            xhr.ontimeout = function (event) {
                handleError("Timed out");
            };

            xhr.onabort = function (event) {
                reject(event);
            };

            try {
                xhr.send(sendData);
            } catch (e) {
                if (isGoogleURL$1(url) && !options.retries) {
                    tryGoogleAuth();
                } else {
                    handleError(e);
                }
            }


            function handleError(error) {
                if (reject) {
                    reject(error);
                } else {
                    throw error
                }
            }

            async function tryGoogleAuth() {
                try {
                    const accessToken = await fetchGoogleAccessToken$1(_url);
                    options.retries = 1;
                    options.oauthToken = accessToken;
                    const response = await self.load(_url, options);
                    resolve(response);
                } catch (e) {
                    if (e.error) {
                        const msg = e.error.startsWith("popup_blocked") ?
                            "Google login popup blocked by browser." :
                            e.error;
                        alert(msg);
                    } else {
                        handleError(e);
                    }
                }
            }
        })

    }

    async _loadFileSlice(localfile, options) {

        let blob = (options && options.range) ?
            localfile.slice(options.range.start, options.range.start + options.range.size) :
            localfile;

        const arrayBuffer = await blob.arrayBuffer();

        if ("arraybuffer" === options.responseType) {
            return arrayBuffer
        } else {
            return arrayBufferToString$1(arrayBuffer)
        }
    }

    async _loadStringFromFile(localfile, options) {

        const blob = options.range ? localfile.slice(options.range.start, options.range.start + options.range.size) : localfile;
        const arrayBuffer = await blob.arrayBuffer();
        return arrayBufferToString$1(arrayBuffer)
    }

    async _loadStringFromUrl(url, options) {

        options = options || {};
        options.responseType = "arraybuffer";
        const data = await this.load(url, options);
        return arrayBufferToString$1(data)
    }

    /**
     * Explicity set an oAuth token for use with given host.  If host is undefined token is used for google api access*
     * @param token
     * @param host
     */
    setOauthToken(token, host) {
        this.oauth.setToken(token, host);
    }

    /**
     * Return an oauth token for the URL if we have one.  This method does not force sign-in, and the token may
     * or may not be valid.  Sign-in is triggered on request failure.
     * *
     * @param url
     * @returns {*}
     */
    getOauthToken(url) {

        // Google is the default provider, don't try to parse host for google URLs
        const host = isGoogleURL$1(url) ?
            undefined :
            parseUri$1(url).host;

        // First check the explicit settings (i.e. token set through the API)
        let token = this.oauth.getToken(host);
        if (token) {
            return token
        } else if (host === undefined) {
            // Now try Google oauth tokens previously obtained.  This will return undefined if google oauth is not
            // configured.
            const googleToken = getCurrentGoogleAccessToken$1();
            if (googleToken && googleToken.expires_at > Date.now()) {
                return googleToken.access_token
            }
        }
    }

    /**
     * This method should only be called when it is known the server supports HEAD requests.  It is used to recover
     * from 416 errors from out-of-spec WRT range request servers.  Notably Globus.
     * * *
     * @param url
     * @param options
     * @returns {Promise<unknown>}
     */
    async getContentLength(url, options) {
        options = options || {};
        options.method = 'HEAD';
        options.requestedHeaders = ['content-length'];
        const headerMap = await this._loadURL(url, options);
        const contentLengthString = headerMap['content-length'];
        return contentLengthString ? Number.parseInt(contentLengthString) : 0
    }

}

function isGoogleStorageSigned$1(url) {
    return url.indexOf("X-Goog-Signature") > -1
}


/**
 * Return a Google oAuth token, triggering a sign in if required.   This method should not be called until we know
 * a token is required, that is until we've tried the url and received a 401, 403, or 404.
 *
 * @param url
 * @returns the oauth token
 */
async function fetchGoogleAccessToken$1(url) {
    if (isInitialized$2()) {
        const scope = getScopeForURL$1(url);
        const access_token = await getAccessToken$2(scope);
        return access_token
    } else {
        throw Error(
            `Authorization is required, but Google oAuth has not been initalized. Contact your site administrator for assistance.`)
    }
}

/**
 * Return the current google access token, if one exists.  Do not triger signOn or request additional scopes.
 * @returns {undefined|access_token}
 */
function getCurrentGoogleAccessToken$1() {
    if (isInitialized$2()) {
        const access_token = getCurrentAccessToken$2();
        return access_token
    } else {
        return undefined
    }
}

function addOauthHeaders$1(headers, acToken) {
    if (acToken) {
        headers["Cache-Control"] = "no-cache";
        headers["Authorization"] = "Bearer " + acToken;
    }
    return headers
}


function addApiKey$1(url) {
    let apiKey = igvxhr$1.apiKey;
    if (!apiKey && typeof gapi !== "undefined") {
        apiKey = gapi.apiKey;
    }
    if (apiKey !== undefined && !url.includes("key=")) {
        const paramSeparator = url.includes("?") ? "&" : "?";
        url = url + paramSeparator + "key=" + apiKey;
    }
    return url
}

function addTeamDrive$1(url) {
    if (url.includes("supportsTeamDrive")) {
        return url
    } else {
        const paramSeparator = url.includes("?") ? "&" : "?";
        url = url + paramSeparator + "supportsTeamDrive=true";
    }
}

/**
 * Perform some well-known url mappings.
 * @param url
 */
function mapUrl$1(url) {

    if (url.startsWith("https://www.dropbox.com")) {
        return url.replace("//www.dropbox.com", "//dl.dropboxusercontent.com")
    } else if (url.startsWith("https://drive.google.com")) {
        return getDriveDownloadURL$1(url)
    } else if (url.includes("//www.broadinstitute.org/igvdata")) {
        return url.replace("//www.broadinstitute.org/igvdata", "//data.broadinstitute.org/igvdata")
    } else if (url.includes("//igvdata.broadinstitute.org")) {
        return url.replace("//igvdata.broadinstitute.org", "//s3.amazonaws.com/igv.broadinstitute.org")
    } else if (url.includes("//igv.genepattern.org")) {
        return url.replace("//igv.genepattern.org", "//igv-genepattern-org.s3.amazonaws.com")
    } else if (url.startsWith("ftp://ftp.ncbi.nlm.nih.gov/geo")) {
        return url.replace("ftp://", "https://")
    } else {
        return url
    }
}


function arrayBufferToString$1(arraybuffer) {

    let plain;
    if (isgzipped$1(arraybuffer)) {
        plain = ungzip_blocks$1(arraybuffer);
    } else {
        plain = new Uint8Array(arraybuffer);
    }

    if ('TextDecoder' in getGlobalObject$1()) {
        return new TextDecoder().decode(plain)
    } else {
        return decodeUTF8$1(plain)
    }
}

/**
 * Use when TextDecoder is not available (primarily IE).
 *
 * From: https://gist.github.com/Yaffle/5458286
 *
 * @param octets
 * @returns {string}
 */
function decodeUTF8$1(octets) {
    var string = "";
    var i = 0;
    while (i < octets.length) {
        var octet = octets[i];
        var bytesNeeded = 0;
        var codePoint = 0;
        if (octet <= 0x7F) {
            bytesNeeded = 0;
            codePoint = octet & 0xFF;
        } else if (octet <= 0xDF) {
            bytesNeeded = 1;
            codePoint = octet & 0x1F;
        } else if (octet <= 0xEF) {
            bytesNeeded = 2;
            codePoint = octet & 0x0F;
        } else if (octet <= 0xF4) {
            bytesNeeded = 3;
            codePoint = octet & 0x07;
        }
        if (octets.length - i - bytesNeeded > 0) {
            var k = 0;
            while (k < bytesNeeded) {
                octet = octets[i + k + 1];
                codePoint = (codePoint << 6) | (octet & 0x3F);
                k += 1;
            }
        } else {
            codePoint = 0xFFFD;
            bytesNeeded = octets.length - i;
        }
        string += String.fromCodePoint(codePoint);
        i += bytesNeeded + 1;
    }
    return string
}


function getGlobalObject$1() {
    if (typeof self !== 'undefined') {
        return self
    }
    if (typeof global !== 'undefined') {
        return global
    } else {
        return window
    }
}


const igvxhr$1 = new IGVXhr$1();

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/** An implementation of an interval tree, following the explanation.
 * from CLR.
 *
 * Public interface:
 *   Constructor  IntervalTree
 *   Insertion    insert
 *   Search       findOverlapping
 */

var BLACK$1 = 1;
var RED$1 = 2;

var NIL$1 = {};
NIL$1.color = BLACK$1;
NIL$1.parent = NIL$1;
NIL$1.left = NIL$1;
NIL$1.right = NIL$1;


class IntervalTree$1 {

    constructor() {
        this.root = NIL$1;
    }

    insert(start, end, value) {

        var interval = new Interval$1(start, end, value);
        var x = new Node$2(interval);
        this.treeInsert(x);
        x.color = RED$1;
        while (x !== this.root && x.parent.color === RED$1) {
            if (x.parent === x.parent.parent.left) {
                let y = x.parent.parent.right;
                if (y.color === RED$1) {
                    x.parent.color = BLACK$1;
                    y.color = BLACK$1;
                    x.parent.parent.color = RED$1;
                    x = x.parent.parent;
                } else {
                    if (x === x.parent.right) {
                        x = x.parent;
                        leftRotate$1.call(this, x);
                    }
                    x.parent.color = BLACK$1;
                    x.parent.parent.color = RED$1;
                    rightRotate$1.call(this, x.parent.parent);
                }
            } else {
                let y = x.parent.parent.left;
                if (y.color === RED$1) {
                    x.parent.color = BLACK$1;
                    y.color = BLACK$1;
                    x.parent.parent.color = RED$1;
                    x = x.parent.parent;
                } else {
                    if (x === x.parent.left) {
                        x = x.parent;
                        rightRotate$1.call(this, x);
                    }
                    x.parent.color = BLACK$1;
                    x.parent.parent.color = RED$1;
                    leftRotate$1.call(this, x.parent.parent);
                }
            }
        }
        this.root.color = BLACK$1;
    }

    /**
     *
     * @param start - query interval
     * @param end - query interval
     * @returns Array of all intervals overlapping the query region
     */
    findOverlapping(start, end) {


        var searchInterval = new Interval$1(start, end, 0);

        if (this.root === NIL$1) return [];

        var intervals = searchAll$1.call(this, searchInterval, this.root, []);

        if (intervals.length > 1) {
            intervals.sort(function (i1, i2) {
                return i1.low - i2.low;
            });
        }

        return intervals;
    }

    /**
     * Dump info on intervals to console.  For debugging.
     */
    logIntervals() {

        logNode(this.root);

        function logNode(node, indent) {

            if (node.left !== NIL$1) logNode(node.left);
            if (node.right !== NIL$1) logNode(node.right);
        }

    }

    mapIntervals(func) {

        applyInterval(this.root);

        function applyInterval(node) {

            func(node.interval);

            if (node.left !== NIL$1) applyInterval(node.left);
            if (node.right !== NIL$1) applyInterval(node.right);
        }
    }


    /**
     * Note:  Does not maintain RB constraints,  this is done post insert
     *
     * @param x  a Node
     */
    treeInsert(x) {
        var node = this.root;
        var y = NIL$1;
        while (node !== NIL$1) {
            y = node;
            if (x.interval.low <= node.interval.low) {
                node = node.left;
            } else {
                node = node.right;
            }
        }
        x.parent = y;

        if (y === NIL$1) {
            this.root = x;
            x.left = x.right = NIL$1;
        } else {
            if (x.interval.low <= y.interval.low) {
                y.left = x;
            } else {
                y.right = x;
            }
        }

        applyUpdate$1.call(this, x);
    }
}

function searchAll$1(interval, node, results) {

    if (node.interval.overlaps(interval)) {
        results.push(node.interval);
    }

    if (node.left !== NIL$1 && node.left.max >= interval.low) {
        searchAll$1.call(this, interval, node.left, results);
    }

    if (node.right !== NIL$1 && node.right.min <= interval.high) {
        searchAll$1.call(this, interval, node.right, results);
    }

    return results;
}

function leftRotate$1(x) {
    var y = x.right;
    x.right = y.left;
    if (y.left !== NIL$1) {
        y.left.parent = x;
    }
    y.parent = x.parent;
    if (x.parent === NIL$1) {
        this.root = y;
    } else {
        if (x.parent.left === x) {
            x.parent.left = y;
        } else {
            x.parent.right = y;
        }
    }
    y.left = x;
    x.parent = y;

    applyUpdate$1.call(this, x);
    // no need to apply update on y, since it'll y is an ancestor
    // of x, and will be touched by applyUpdate().
}


function rightRotate$1(x) {
    var y = x.left;
    x.left = y.right;
    if (y.right !== NIL$1) {
        y.right.parent = x;
    }
    y.parent = x.parent;
    if (x.parent === NIL$1) {
        this.root = y;
    } else {
        if (x.parent.right === x) {
            x.parent.right = y;
        } else {
            x.parent.left = y;
        }
    }
    y.right = x;
    x.parent = y;


    applyUpdate$1.call(this, x);
    // no need to apply update on y, since it'll y is an ancestor
    // of x, and will be touched by applyUpdate().
}


// Applies the statistic update on the node and its ancestors.
function applyUpdate$1(node) {
    while (node !== NIL$1) {
        var nodeMax = node.left.max > node.right.max ? node.left.max : node.right.max;
        var intervalHigh = node.interval.high;
        node.max = nodeMax > intervalHigh ? nodeMax : intervalHigh;

        var nodeMin = node.left.min < node.right.min ? node.left.min : node.right.min;
        var intervalLow = node.interval.low;
        node.min = nodeMin < intervalLow ? nodeMin : intervalLow;

        node = node.parent;
    }
}


class Interval$1 {
    constructor(low, high, value) {
        this.low = low;
        this.high = high;
        this.value = value;
    }

    equals(other) {
        if (!other) {
            return false;
        }
        if (this === other) {
            return true;
        }
        return (this.low === other.low &&
            this.high === other.high);

    }

    compareTo(other) {
        if (this.low < other.low)
            return -1;
        if (this.low > other.low)
            return 1;

        if (this.high < other.high)
            return -1;
        if (this.high > other.high)
            return 1;

        return 0;
    }

    /**
     * Returns true if this interval overlaps the other.
     */
    overlaps(other) {
            return (this.low <= other.high && other.low <= this.high);
    }
}

function Node$2(interval) {
    this.parent = NIL$1;
    this.left = NIL$1;
    this.right = NIL$1;
    this.interval = interval;
    this.color = RED$1;
}

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2016-2017 The Regents of the University of California
 * Author: Jim Robinson
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

const FeatureUtils = {

    packFeatures: function (features, maxRows, sorted) {

        var start;
        var end;

        if (!features) return;

        maxRows = maxRows || 10000;

        if (!sorted) {
            features.sort(function (a, b) {
                return a.start - b.start;
            });
        }


        if (features.length === 0) {
            return [];

        } else {

            var bucketList = [],
                allocatedCount = 0,
                lastAllocatedCount = 0,
                nextStart,
                row,
                index,
                bucket,
                feature,
                gap = 2,
                bucketStart;

            start = features[0].start;
            end = features[features.length - 1].start;

            bucketStart = Math.max(start, features[0].start);
            nextStart = bucketStart;

            features.forEach(function (alignment) {

                var buckListIndex = Math.max(0, alignment.start - bucketStart);
                if (bucketList[buckListIndex] === undefined) {
                    bucketList[buckListIndex] = [];
                }
                bucketList[buckListIndex].push(alignment);
            });


            row = 0;


            while (allocatedCount < features.length && row <= maxRows) {


                while (nextStart <= end) {

                    bucket = undefined;

                    while (!bucket && nextStart <= end) {

                        index = nextStart - bucketStart;
                        if (bucketList[index] === undefined) {
                            ++nextStart;                     // No buckets at this index
                        } else {
                            bucket = bucketList[index];
                        }

                    } // while (bucket)

                    if (!bucket) {
                        break;
                    }
                    feature = bucket.pop();
                    if (0 === bucket.length) {
                        bucketList[index] = undefined;
                    }

                    feature.row = row;

                    nextStart = feature.end + gap;
                    ++allocatedCount;

                } // while (nextStart)

                row++;
                nextStart = bucketStart;

                if (allocatedCount === lastAllocatedCount) break;   // Protect from infinite loops

                lastAllocatedCount = allocatedCount;

            } // while (allocatedCount)

        }
    },


    /**
     * Find features overlapping the given interval.  It is assumed that all features share the same chromosome.
     *
     * TODO -- significant overlap with FeatureCache, refactor to combine
     *
     * @param featureList
     * @param start
     * @param end
     */
    findOverlapping: function (featureList, start, end) {

        if (!featureList || featureList.length === 0) {
            return [];
        } else {
            const tree = buildIntervalTree$1(featureList);
            const intervals = tree.findOverlapping(start, end);

            if (intervals.length === 0) {
                return [];
            } else {
                // Trim the list of features in the intervals to those
                // overlapping the requested range.
                // Assumption: features are sorted by start position

                featureList = [];

                intervals.forEach(function (interval) {
                    const intervalFeatures = interval.value;
                    const len = intervalFeatures.length;
                    for (let i = 0; i < len; i++) {
                        const feature = intervalFeatures[i];
                        if (feature.start > end) break;
                        else if (feature.end > start) {
                            featureList.push(feature);
                        }
                    }
                });

                featureList.sort(function (a, b) {
                    return a.start - b.start;
                });

                return featureList;
            }
        }

    }
};


/**
 * Build an interval tree from the feature list for fast interval based queries.   We lump features in groups
 * of 10, or total size / 100,   to reduce size of the tree.
 *
 * @param featureList
 */
function buildIntervalTree$1(featureList) {

    const tree = new IntervalTree$1();
    const len = featureList.length;
    const chunkSize = Math.max(10, Math.round(len / 100));

    featureList.sort(function (f1, f2) {
        return (f1.start === f2.start ? 0 : (f1.start > f2.start ? 1 : -1));
    });

    for (let i = 0; i < len; i += chunkSize) {
        const e = Math.min(len, i + chunkSize);
        const subArray = featureList.slice(i, e);
        const iStart = subArray[0].start;
        let iEnd = iStart;
        subArray.forEach(function (feature) {
            iEnd = Math.max(iEnd, feature.end);
        });
        tree.insert(iStart, iEnd, subArray);
    }

    return tree;
}

function hexToRGB(hex) {
    // Ensure the hex value is in the proper format
    hex = hex.replace(/^#/, '');

    // If it's a shorthand hex color (like #f06), double each character
    if (hex.length === 3) {
        hex = hex.split('').map(char => char + char).join('');
    }

    if (hex.length !== 6) {
        throw new Error('Invalid HEX color.');
    }

    // Parse the r, g, b values
    let bigint = parseInt(hex, 16);
    let r = (bigint >> 16) & 255;
    let g = (bigint >> 8) & 255;
    let b = bigint & 255;

    return `rgb(${r}, ${g}, ${b})`;
}

const genericColorPickerPalette =
    {
        licorice: "#000000",
        steel: "#6e6e6e",
        magnesium: "#b8b8b8",
        mercury: "#e8e8e8",
        cayenne: "#891100",
        mocha: "#894800",
        aspargus: "#888501",
        fern: "#458401",
        teal: "#008688",
        salmon: "#ff726e",
        tangerine: "#ff8802",
        cantaloupe: "#ffce6e",
        lemon: "#fffa03",
        lime: "#83f902",
        honeydew: "#cefa6e",
        ice: "#68fdff",
        aqua: "#008cff",
        blueberry: "#002eff",
        midnight: "#001888",
        grape: "#8931ff",
        lavender: "#d278ff",
        orchid: "#6e76ff",
        strawberry: "#ff2987",
        magenta: "#ff39ff",
        carnation: "#ff7fd3"
    };

const appleCrayonRGBPalette =
    {
        cantaloupe: {r: 255, g: 206, b: 110},
        honeydew: {r: 206, g: 250, b: 110},
        spindrift: {r: 104, g: 251, b: 208},
        sky: {r: 106, g: 207, b: 255},
        lavender: {r: 210, g: 120, b: 255},
        carnation: {r: 255, g: 127, b: 211},
        licorice: {r: 0, g: 0, b: 0},
        snow: {r: 255, g: 255, b: 255},
        salmon: {r: 255, g: 114, b: 110},
        banana: {r: 255, g: 251, b: 109},
        flora: {r: 104, g: 249, b: 110},
        ice: {r: 104, g: 253, b: 255},
        orchid: {r: 110, g: 118, b: 255},
        bubblegum: {r: 255, g: 122, b: 255},
        lead: {r: 30, g: 30, b: 30},
        mercury: {r: 232, g: 232, b: 232},
        tangerine: {r: 255, g: 136, b: 2},
        lime: {r: 131, g: 249, b: 2},
        sea_foam: {r: 3, g: 249, b: 135},
        aqua: {r: 0, g: 140, b: 255},
        grape: {r: 137, g: 49, b: 255},
        strawberry: {r: 255, g: 41, b: 135},
        tungsten: {r: 58, g: 58, b: 58},
        silver: {r: 208, g: 208, b: 208},
        maraschino: {r: 255, g: 33, b: 1},
        lemon: {r: 255, g: 250, b: 3},
        spring: {r: 5, g: 248, b: 2},
        turquoise: {r: 0, g: 253, b: 255},
        blueberry: {r: 0, g: 46, b: 255},
        magenta: {r: 255, g: 57, b: 255},
        iron: {r: 84, g: 84, b: 83},
        magnesium: {r: 184, g: 184, b: 184},
        mocha: {r: 137, g: 72, b: 0},
        fern: {r: 69, g: 132, b: 1},
        moss: {r: 1, g: 132, b: 72},
        ocean: {r: 0, g: 74, b: 136},
        eggplant: {r: 73, g: 26, b: 136},
        maroon: {r: 137, g: 22, b: 72},
        steel: {r: 110, g: 110, b: 110},
        aluminum: {r: 160, g: 159, b: 160},
        cayenne: {r: 137, g: 17, b: 0},
        aspargus: {r: 136, g: 133, b: 1},
        clover: {r: 2, g: 132, b: 1},
        teal: {r: 0, g: 134, b: 136},
        midnight: {r: 0, g: 24, b: 136},
        plum: {r: 137, g: 30, b: 136},
        tin: {r: 135, g: 134, b: 135},
        nickel: {r: 136, g: 135, b: 135}
    };

function appleCrayonRGB(name) {
    const {r, g, b} = appleCrayonRGBPalette[name];
    return `rgb(${r},${g},${b})`
}

function appleCrayonRGBA(name, alpha) {
    const {r, g, b} = appleCrayonRGBPalette[name];
    return `rgba(${r},${g},${b},${alpha})`
}

const colorPalettes = {

    Set1:
        [
            "rgb(228,26,28)",
            "rgb(55,126,184)",
            "rgb(77,175,74)",
            "rgb(166,86,40)",
            "rgb(152,78,163)",
            "rgb(255,127,0)",
            "rgb(247,129,191)",
            "rgb(153,153,153)",
            "rgb(255,255,51)"
        ],

    Dark2:
        [
            "rgb(27,158,119)",
            "rgb(217,95,2)",
            "rgb(117,112,179)",
            "rgb(231,41,138)",
            "rgb(102,166,30)",
            "rgb(230,171,2)",
            "rgb(166,118,29)",
            "rgb(102,102,102)"
        ],

    Set2:
        [
            "rgb(102, 194,165)",
            "rgb(252,141,98)",
            "rgb(141,160,203)",
            "rgb(231,138,195)",
            "rgb(166,216,84)",
            "rgb(255,217,47)",
            "rgb(229,196,148)",
            "rgb(179,179,179)"
        ],

    Set3:
        [
            "rgb(141,211,199)",
            "rgb(255,255,179)",
            "rgb(190,186,218)",
            "rgb(251,128,114)",
            "rgb(128,177,211)",
            "rgb(253,180,98)",
            "rgb(179,222,105)",
            "rgb(252,205,229)",
            "rgb(217,217,217)",
            "rgb(188,128,189)",
            "rgb(204,235,197)",
            "rgb(255,237,111)"
        ],

    Pastel1:
        [
            "rgb(251,180,174)",
            "rgb(179,205,227)",
            "rgb(204,235,197)",
            "rgb(222,203,228)",
            "rgb(254,217,166)",
            "rgb(255,255,204)",
            "rgb(229,216,189)",
            "rgb(253,218,236)"
        ],

    Pastel2:
        [
            "rgb(173,226,207)",
            "rgb(253,205,172)",
            "rgb(203,213,232)",
            "rgb(244,202,228)",
            "rgb(230,245,201)",
            "rgb(255,242,174)",
            "rgb(243,225,206)"
        ],

    Accent:
        [
            "rgb(127,201,127)",
            "rgb(190,174,212)",
            "rgb(253,192,134)",
            "rgb(255,255,153)",
            "rgb(56,108,176)",
            "rgb(240,2,127)",
            "rgb(191,91,23)"
        ]
};

class PaletteColorTable {

    constructor(palette) {
        this.colors = colorPalettes[palette];
        if (!Array.isArray(this.colors)) this.colors = [];
        this.colorTable = new Map();
        this.nextIdx = 0;
        this.colorGenerator = new RandomColorGenerator();
    }

    getColor(key) {
        if (!this.colorTable.has(key)) {
            if (this.nextIdx < this.colors.length) {
                this.colorTable.set(key, this.colors[this.nextIdx]);
            } else {
                this.colorTable.set(key, this.colorGenerator.get());
            }
            this.nextIdx++;
        }
        return this.colorTable.get(key)
    }
}

class ColorTable {
    constructor(colors) {
        this.colorTable = colors || {};
        this.nextIdx = 0;
        this.colorGenerator = new RandomColorGenerator();
    }

    getColor(key) {
        if (!this.colorTable.hasOwnProperty(key)) {
            if (this.colorTable.hasOwnProperty("*")) {
                return this.colorTable["*"]
            }
            this.colorTable[key] = this.colorGenerator.get();
        }
        return this.colorTable[key]
    }
}

// Random color generator from https://github.com/sterlingwes/RandomColor/blob/master/rcolor.js
// Free to use & distribute under the MIT license
// Wes Johnson (@SterlingWes)
//
// inspired by http://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/
function RandomColorGenerator() {
    this.hue = Math.random();
    this.goldenRatio = 0.618033988749895;
    this.hexwidth = 2;
}

RandomColorGenerator.prototype.hsvToRgb = function (h, s, v) {
    var h_i = Math.floor(h * 6),
        f = h * 6 - h_i,
        p = v * (1 - s),
        q = v * (1 - f * s),
        t = v * (1 - (1 - f) * s),
        r = 255,
        g = 255,
        b = 255;
    switch (h_i) {
        case 0:
            r = v, g = t, b = p;
            break
        case 1:
            r = q, g = v, b = p;
            break
        case 2:
            r = p, g = v, b = t;
            break
        case 3:
            r = p, g = q, b = v;
            break
        case 4:
            r = t, g = p, b = v;
            break
        case 5:
            r = v, g = p, b = q;
            break
    }
    return [Math.floor(r * 256), Math.floor(g * 256), Math.floor(b * 256)]
};

RandomColorGenerator.prototype.padHex = function (str) {
    if (str.length > this.hexwidth) return str
    return new Array(this.hexwidth - str.length + 1).join('0') + str
};

RandomColorGenerator.prototype.get = function (saturation, value) {
    this.hue += this.goldenRatio;
    this.hue %= 1;
    if (typeof saturation !== "number") saturation = 0.5;
    if (typeof value !== "number") value = 0.95;
    var rgb = this.hsvToRgb(this.hue, saturation, value);

    return "#" + this.padHex(rgb[0].toString(16))
        + this.padHex(rgb[1].toString(16))
        + this.padHex(rgb[2].toString(16))

};

new RandomColorGenerator();

function rgbaColor(r, g, b, a) {
    r = IGVMath.clamp(r, 0, 255);
    g = IGVMath.clamp(g, 0, 255);
    b = IGVMath.clamp(b, 0, 255);
    a = IGVMath.clamp(a, 0.0, 1.0);
    return `rgba(${r}, ${g}, ${b}, ${a})`
}

function rgbColor(r, g, b) {
    r = IGVMath.clamp(r, 0, 255);
    g = IGVMath.clamp(g, 0, 255);
    b = IGVMath.clamp(b, 0, 255);
    return `rgb(${r}, ${g}, ${b})`
}

function randomRGB$1(min, max) {

    min = IGVMath.clamp(min, 0, 255);
    max = IGVMath.clamp(max, 0, 255);

    const r = Math.round(Math.random() * (max - min) + min).toString(10);
    const g = Math.round(Math.random() * (max - min) + min).toString(10);
    const b = Math.round(Math.random() * (max - min) + min).toString(10);
    return `rgb(${r},${g},${b})`

}

function rgbaStringTokens(rgbaString) {

    if (rgbaString.startsWith('rgba(')) {

        const [ignore, pass0 ] = rgbaString.split('(');

        const [ rgba ] = pass0.split(')');

        return rgba.split(',').map((string, index) => index < 3 ? parseInt(string) : parseFloat(string))

    } else {
        return undefined
    }
}

function rgbStringTokens(rgbString) {

    if (rgbString.startsWith('rgb(')) {

        const [ignore, pass0 ] = rgbString.split('(');

        const [ rgb ] = pass0.split(')');

        return rgb.split(',').map(string => parseInt(string))

    } else {
        return undefined
    }
}

const fudge = 0.005;
function rgbStringHeatMapLerp(_a, _b, interpolant) {

    if (interpolant < fudge) {
        return _a
    } else if (interpolant > 1.0 - fudge) {
        return _b
    } else {
        let rA, gA, bA;
        let rB, gB, bB;
        if (interpolant < 0.5) {

            interpolant /= .5;

            [ rA, gA, bA ] = rgbStringTokens(_a);
            [ rB, gB, bB ] = rgbStringTokens(appleCrayonRGB('snow'));
        } else {

            interpolant = (interpolant - .5) / .5;

            [ rA, gA, bA ] = rgbStringTokens(appleCrayonRGB('snow'));
            [ rB, gB, bB ] = rgbStringTokens(_b);
        }

        const [ r, g, b ] =
            [
                Math.floor(IGVMath.lerp(rA, rB, interpolant)),
                Math.floor(IGVMath.lerp(gA, gB, interpolant)),
                Math.floor(IGVMath.lerp(bA, bB, interpolant))
            ];

        return rgbColor(r, g, b)

    }


}

/**
 * Test if the given value is a string or number.  Not using typeof as it fails on boxed primitives.
 *
 * @param value
 * @returns boolean
 */

function isSimpleType(value) {
    const simpleTypes = new Set(["boolean", "number", "string", "symbol"]);
    const valueType = typeof value;
    return (value !== undefined && (simpleTypes.has(valueType) || value.substring || value.toFixed))
}

function buildOptions(config, options) {

    var defaultOptions = {
        oauthToken: config.oauthToken,
        headers: config.headers,
        withCredentials: config.withCredentials,
        filename: config.filename
    };

    return Object.assign(defaultOptions, options)
}

/**
 * isMobile test from http://detectmobilebrowsers.com
 * TODO -- improve UI design so this isn't neccessary
 * @returns {boolean}
 */

// igv.isMobile = function () {
//
//     const a = (navigator.userAgent || navigator.vendor || window.opera);
//     return (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) ||
//         /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4)))
//
// }

const doAutoscale = function (features) {
    var min, max;

    if (features && features.length > 0) {
        min = Number.MAX_VALUE;
        max = -Number.MAX_VALUE;

        for(let f of features) {
            if (!Number.isNaN(f.value)) {
                min = Math.min(min, f.value);
                max = Math.max(max, f.value);
            }
        }

        // Insure we have a zero baseline
        if (max > 0) min = Math.min(0, min);
        if (max < 0) max = 0;
    } else {
        // No features -- default
        min = 0;
        max = 100;
    }

    return {min: min, max: max}
};

const validateGenomicExtent = function (chromosomeLengthBP, genomicExtent, minimumBP) {

    let ss = genomicExtent.start;
    let ee = genomicExtent.end;

    if (undefined === ee) {

        ss -= minimumBP / 2;
        ee = ss + minimumBP;

        if (ee > chromosomeLengthBP) {
            ee = chromosomeLengthBP;
            ss = ee - minimumBP;
        } else if (ss < 0) {
            ss = 0;
            ee = minimumBP;
        }

    } else if (ee - ss < minimumBP) {

        const center = (ee + ss) / 2;

        if (center - minimumBP / 2 < 0) {
            ss = 0;
            ee = ss + minimumBP;
        } else if (center + minimumBP / 2 > chromosomeLengthBP) {
            ee = chromosomeLengthBP;
            ss = ee - minimumBP;
        } else {
            ss = center - minimumBP / 2;
            ee = ss + minimumBP;
        }
    }

    genomicExtent.start = Math.ceil(ss);
    genomicExtent.end = Math.floor(ee);
};

/*!
 * is-number <https://github.com/jonschlinkert/is-number>
 *
 * Copyright (c) 2014-present, Jon Schlinkert.
 * Released under the MIT License.
 */

const isNumber = function (num) {
    if (typeof num === 'number') {
        return num - num === 0
    }
    if (typeof num === 'string' && num.trim() !== '') {
        return Number.isFinite(+num)
    }
    return false
};

async function getFilename$1(url) {
    if (isString$3(url) && url.startsWith("https://drive.google.com")) {
        // This will fail if Google API key is not defined
        if (getApiKey$1() === undefined) {
            throw Error("Google drive is referenced, but API key is not defined.  An API key is required for Google Drive access")
        }
        const json = await getDriveFileInfo$1(url);
        return json.originalFileName || json.name
    } else {
        return getFilename$2(url)
    }
}

function prettyBasePairNumber(raw) {

    var denom,
        units,
        value,
        floored;

    if (raw > 1e7) {
        denom = 1e6;
        units = " mb";
    } else if (raw > 1e4) {

        denom = 1e3;
        units = " kb";

        value = raw / denom;
        floored = Math.floor(value);
        return numberFormatter$1(floored) + units
    } else {
        return numberFormatter$1(raw) + " bp"
    }

    value = raw / denom;
    floored = Math.floor(value);

    return floored.toString() + units
}


function isDataURL(obj) {
    return (isString$3(obj) && obj.startsWith("data:"))
}

function createColumn(columnContainer, className) {
    const column = div$2({class: className});
    columnContainer.appendChild(column);
}


function insertElementBefore(element, referenceNode) {
    referenceNode.parentNode.insertBefore(element, referenceNode);
}

function insertElementAfter(element, referenceNode) {
    referenceNode.parentNode.insertBefore(element, referenceNode.nextSibling);
}

/**
 * Test to see if page is loaded in a secure context, that is by https or is localhost.
 */
function isSecureContext() {
    return window.location.protocol === "https:" || window.location.hostname === "localhost"
}

function getElementVerticalDimension(element) {

    const style = window.getComputedStyle(element);

    const marginTop = parseInt(style.marginTop);
    const marginBottom = parseInt(style.marginBottom);

    const { top, bottom, height } = element.getBoundingClientRect();
    return {
        top: Math.floor(top) - marginTop,
        bottom: Math.floor(bottom) + marginBottom,
        height: Math.floor(height) + marginTop + marginBottom
    };
}

class Popover {

    constructor(parent, isDraggable, title, closeHandler) {

        this.parent = parent;

        this.popover = div$2({ class: "igv-ui-popover" });
        parent.appendChild(this.popover);

        this.popoverHeader = div$2();
        this.popover.appendChild(this.popoverHeader);

        const titleElement = div$2();
        this.popoverHeader.appendChild(titleElement);
        if (title) {
            titleElement.textContent = title;
        }

        // attach close handler
        const el = div$2();
        this.popoverHeader.appendChild(el);
        el.appendChild(createIcon$1('times'));
        el.addEventListener('click', e => {
            e.stopPropagation();
            e.preventDefault();
            closeHandler ? closeHandler() : this.dismiss();
        });

        if (true === isDraggable) {
            makeDraggable$1(this.popover, this.popoverHeader, { minX:0, minY:0 });
        }

        this.popoverContent = div$2();
        this.popover.appendChild(this.popoverContent);

        this.popover.style.display = 'none';


    }

    configure(menuItems) {

        if (0 === menuItems.length) {
            return
        }

        const menuElements = createMenuElements$1(menuItems, this.popover);

        for (const { element } of menuElements) {
            this.popoverContent.appendChild(element);
        }

    }

    present(event) {

        this.popover.style.display = 'block';

        const parent = this.popover.parentNode;
        const { x, y, width } = translateMouseCoordinates(event, parent);
        this.popover.style.top  = `${ y }px`;

        const { width: w } = this.popover.getBoundingClientRect();

        const xmax = x + w;
        const delta = xmax - width;

        this.popover.style.left = `${ xmax > width ? (x - delta) : x }px`;
        this.popoverContent.style.maxWidth = `${ Math.min(w, width) }px`;
    }

    presentContentWithEvent(e, content) {

        this.popover.style.display = 'block';

        this.popoverContent.innerHTML = content;

        present$1(e, this.popover, this.popoverContent);

    }

    presentMenu(e, menuItems) {

        if (0 === menuItems.length) {
            return
        }

        this.popover.style.display = 'block';

        const menuElements = createMenuElements$1(menuItems, this.popover);
        for (let item of menuElements) {
            this.popoverContent.appendChild(item.element);
        }

        present$1(e, this.popover, this.popoverContent);
    }

    dismiss() {
        this.popover.style.display = 'none';
    }

    hide() {
        this.popover.style.display = 'none';
        this.dispose();
    }

    dispose() {

        if (this.popover) {
            this.popover.parentNode.removeChild(this.popover);
        }

        const keys = Object.keys(this);
        for (let key of keys) {
            this[ key ] = undefined;
        }
    }

}

function present$1(e, popover, popoverContent) {

    const { x, y, width } = translateMouseCoordinates(e, popover.parentNode);
    popover.style.top  = `${ y }px`;

    const { width: w } = popover.getBoundingClientRect();

    const xmax = x + w;
    const delta = xmax - width;

    popover.style.left = `${ xmax > width ? (x - delta) : x }px`;
    popoverContent.style.maxWidth = `${ Math.min(w, width) }px`;


}

function createMenuElements$1(itemList, popover) {

    const list  = itemList.map(function (item, i) {
        let element;

        if (typeof item === 'string') {
            element = div$2();
            element.innerHTML = item;
        } else if (typeof item === 'Node') {
            element = item;
        } else {
            if (typeof item.init === 'function') {
                item.init();
            }

            if ("checkbox" === item.type) {
                element = createCheckbox$1("Show all bases", item.value);
            }

            else {
                element = div$2();
                if (typeof item.label === 'string') {
                    element.innerHTML = item.label;
                }
            }

            if (item.click && "color" !== item.type) {
                element.addEventListener('click', handleClick);
                element.addEventListener('touchend', handleClick);
                element.addEventListener('mouseup', function (e) {
                    e.preventDefault();
                    e.stopPropagation();
                });

                // eslint-disable-next-line no-inner-declarations
                function handleClick(e) {
                    item.click();
                    hide$1(popover);
                    e.preventDefault();
                    e.stopPropagation();
                }
            }
        }


        return { element, init: item.init };
    });

    return list;
}

/*! @license DOMPurify 3.2.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.6/LICENSE */

const {
  entries,
  setPrototypeOf,
  isFrozen,
  getPrototypeOf,
  getOwnPropertyDescriptor
} = Object;
let {
  freeze,
  seal,
  create: create$2
} = Object; // eslint-disable-line import/no-mutable-exports
let {
  apply,
  construct
} = typeof Reflect !== 'undefined' && Reflect;
if (!freeze) {
  freeze = function freeze(x) {
    return x;
  };
}
if (!seal) {
  seal = function seal(x) {
    return x;
  };
}
if (!apply) {
  apply = function apply(fun, thisValue, args) {
    return fun.apply(thisValue, args);
  };
}
if (!construct) {
  construct = function construct(Func, args) {
    return new Func(...args);
  };
}
const arrayForEach = unapply(Array.prototype.forEach);
const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);
const arrayPop = unapply(Array.prototype.pop);
const arrayPush = unapply(Array.prototype.push);
const arraySplice = unapply(Array.prototype.splice);
const stringToLowerCase = unapply(String.prototype.toLowerCase);
const stringToString = unapply(String.prototype.toString);
const stringMatch = unapply(String.prototype.match);
const stringReplace = unapply(String.prototype.replace);
const stringIndexOf = unapply(String.prototype.indexOf);
const stringTrim = unapply(String.prototype.trim);
const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);
const regExpTest = unapply(RegExp.prototype.test);
const typeErrorCreate = unconstruct(TypeError);
/**
 * Creates a new function that calls the given function with a specified thisArg and arguments.
 *
 * @param func - The function to be wrapped and called.
 * @returns A new function that calls the given function with a specified thisArg and arguments.
 */
function unapply(func) {
  return function (thisArg) {
    if (thisArg instanceof RegExp) {
      thisArg.lastIndex = 0;
    }
    for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
      args[_key - 1] = arguments[_key];
    }
    return apply(func, thisArg, args);
  };
}
/**
 * Creates a new function that constructs an instance of the given constructor function with the provided arguments.
 *
 * @param func - The constructor function to be wrapped and called.
 * @returns A new function that constructs an instance of the given constructor function with the provided arguments.
 */
function unconstruct(func) {
  return function () {
    for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
      args[_key2] = arguments[_key2];
    }
    return construct(func, args);
  };
}
/**
 * Add properties to a lookup table
 *
 * @param set - The set to which elements will be added.
 * @param array - The array containing elements to be added to the set.
 * @param transformCaseFunc - An optional function to transform the case of each element before adding to the set.
 * @returns The modified set with added elements.
 */
function addToSet(set, array) {
  let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase;
  if (setPrototypeOf) {
    // Make 'in' and truthy checks like Boolean(set.constructor)
    // independent of any properties defined on Object.prototype.
    // Prevent prototype setters from intercepting set as a this value.
    setPrototypeOf(set, null);
  }
  let l = array.length;
  while (l--) {
    let element = array[l];
    if (typeof element === 'string') {
      const lcElement = transformCaseFunc(element);
      if (lcElement !== element) {
        // Config presets (e.g. tags.js, attrs.js) are immutable.
        if (!isFrozen(array)) {
          array[l] = lcElement;
        }
        element = lcElement;
      }
    }
    set[element] = true;
  }
  return set;
}
/**
 * Clean up an array to harden against CSPP
 *
 * @param array - The array to be cleaned.
 * @returns The cleaned version of the array
 */
function cleanArray(array) {
  for (let index = 0; index < array.length; index++) {
    const isPropertyExist = objectHasOwnProperty(array, index);
    if (!isPropertyExist) {
      array[index] = null;
    }
  }
  return array;
}
/**
 * Shallow clone an object
 *
 * @param object - The object to be cloned.
 * @returns A new object that copies the original.
 */
function clone(object) {
  const newObject = create$2(null);
  for (const [property, value] of entries(object)) {
    const isPropertyExist = objectHasOwnProperty(object, property);
    if (isPropertyExist) {
      if (Array.isArray(value)) {
        newObject[property] = cleanArray(value);
      } else if (value && typeof value === 'object' && value.constructor === Object) {
        newObject[property] = clone(value);
      } else {
        newObject[property] = value;
      }
    }
  }
  return newObject;
}
/**
 * This method automatically checks if the prop is function or getter and behaves accordingly.
 *
 * @param object - The object to look up the getter function in its prototype chain.
 * @param prop - The property name for which to find the getter function.
 * @returns The getter function found in the prototype chain or a fallback function.
 */
function lookupGetter(object, prop) {
  while (object !== null) {
    const desc = getOwnPropertyDescriptor(object, prop);
    if (desc) {
      if (desc.get) {
        return unapply(desc.get);
      }
      if (typeof desc.value === 'function') {
        return unapply(desc.value);
      }
    }
    object = getPrototypeOf(object);
  }
  function fallbackValue() {
    return null;
  }
  return fallbackValue;
}

const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'select', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);
const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']);
const svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']);
// List of SVG elements that are disallowed by default.
// We still need to know them so that we can do namespace
// checks properly in case one wants to add them to
// allow-list.
const svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']);
const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'mprescripts']);
// Similarly to SVG, we want to know all MathML elements,
// even those that we disallow by default.
const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
const text = freeze(['#text']);

const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns', 'slot']);
const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);
const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);

// eslint-disable-next-line unicorn/better-regex
const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex
const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape
const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
);
const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i);
const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex
);
const DOCTYPE_NAME = seal(/^html$/i);
const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);

var EXPRESSIONS = /*#__PURE__*/Object.freeze({
  __proto__: null,
  ARIA_ATTR: ARIA_ATTR,
  ATTR_WHITESPACE: ATTR_WHITESPACE,
  CUSTOM_ELEMENT: CUSTOM_ELEMENT,
  DATA_ATTR: DATA_ATTR,
  DOCTYPE_NAME: DOCTYPE_NAME,
  ERB_EXPR: ERB_EXPR,
  IS_ALLOWED_URI: IS_ALLOWED_URI,
  IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,
  MUSTACHE_EXPR: MUSTACHE_EXPR,
  TMPLIT_EXPR: TMPLIT_EXPR
});

/* eslint-disable @typescript-eslint/indent */
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
const NODE_TYPE = {
  element: 1,
  attribute: 2,
  text: 3,
  cdataSection: 4,
  entityReference: 5,
  // Deprecated
  entityNode: 6,
  // Deprecated
  progressingInstruction: 7,
  comment: 8,
  document: 9,
  documentType: 10,
  documentFragment: 11,
  notation: 12 // Deprecated
};
const getGlobal = function getGlobal() {
  return typeof window === 'undefined' ? null : window;
};
/**
 * Creates a no-op policy for internal use only.
 * Don't export this function outside this module!
 * @param trustedTypes The policy factory.
 * @param purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).
 * @return The policy created (or null, if Trusted Types
 * are not supported or creating the policy failed).
 */
const _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) {
  if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') {
    return null;
  }
  // Allow the callers to control the unique policy name
  // by adding a data-tt-policy-suffix to the script element with the DOMPurify.
  // Policy creation with duplicate names throws in Trusted Types.
  let suffix = null;
  const ATTR_NAME = 'data-tt-policy-suffix';
  if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {
    suffix = purifyHostElement.getAttribute(ATTR_NAME);
  }
  const policyName = 'dompurify' + (suffix ? '#' + suffix : '');
  try {
    return trustedTypes.createPolicy(policyName, {
      createHTML(html) {
        return html;
      },
      createScriptURL(scriptUrl) {
        return scriptUrl;
      }
    });
  } catch (_) {
    // Policy creation failed (most likely another DOMPurify script has
    // already run). Skip creating the policy, as this will only cause errors
    // if TT are enforced.
    console.warn('TrustedTypes policy ' + policyName + ' could not be created.');
    return null;
  }
};
const _createHooksMap = function _createHooksMap() {
  return {
    afterSanitizeAttributes: [],
    afterSanitizeElements: [],
    afterSanitizeShadowDOM: [],
    beforeSanitizeAttributes: [],
    beforeSanitizeElements: [],
    beforeSanitizeShadowDOM: [],
    uponSanitizeAttribute: [],
    uponSanitizeElement: [],
    uponSanitizeShadowNode: []
  };
};
function createDOMPurify() {
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
  const DOMPurify = root => createDOMPurify(root);
  DOMPurify.version = '3.2.6';
  DOMPurify.removed = [];
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
    // Not running in a browser, provide a factory function
    // so that you can pass your own Window
    DOMPurify.isSupported = false;
    return DOMPurify;
  }
  let {
    document
  } = window;
  const originalDocument = document;
  const currentScript = originalDocument.currentScript;
  const {
    DocumentFragment,
    HTMLTemplateElement,
    Node,
    Element,
    NodeFilter,
    NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,
    HTMLFormElement,
    DOMParser,
    trustedTypes
  } = window;
  const ElementPrototype = Element.prototype;
  const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
  const remove = lookupGetter(ElementPrototype, 'remove');
  const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
  const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
  const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
  // As per issue #47, the web-components registry is inherited by a
  // new document created via createHTMLDocument. As per the spec
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
  // a new empty registry is used when creating a template contents owner
  // document, so we use that as our parent document to ensure nothing
  // is inherited.
  if (typeof HTMLTemplateElement === 'function') {
    const template = document.createElement('template');
    if (template.content && template.content.ownerDocument) {
      document = template.content.ownerDocument;
    }
  }
  let trustedTypesPolicy;
  let emptyHTML = '';
  const {
    implementation,
    createNodeIterator,
    createDocumentFragment,
    getElementsByTagName
  } = document;
  const {
    importNode
  } = originalDocument;
  let hooks = _createHooksMap();
  /**
   * Expose whether this browser supports running the full DOMPurify.
   */
  DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;
  const {
    MUSTACHE_EXPR,
    ERB_EXPR,
    TMPLIT_EXPR,
    DATA_ATTR,
    ARIA_ATTR,
    IS_SCRIPT_OR_DATA,
    ATTR_WHITESPACE,
    CUSTOM_ELEMENT
  } = EXPRESSIONS;
  let {
    IS_ALLOWED_URI: IS_ALLOWED_URI$1
  } = EXPRESSIONS;
  /**
   * We consider the elements and attributes below to be safe. Ideally
   * don't add any new ones but feel free to remove unwanted ones.
   */
  /* allowed element names */
  let ALLOWED_TAGS = null;
  const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]);
  /* Allowed attribute names */
  let ALLOWED_ATTR = null;
  const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]);
  /*
   * Configure how DOMPurify should handle custom elements and their attributes as well as customized built-in elements.
   * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements)
   * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list)
   * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`.
   */
  let CUSTOM_ELEMENT_HANDLING = Object.seal(create$2(null, {
    tagNameCheck: {
      writable: true,
      configurable: false,
      enumerable: true,
      value: null
    },
    attributeNameCheck: {
      writable: true,
      configurable: false,
      enumerable: true,
      value: null
    },
    allowCustomizedBuiltInElements: {
      writable: true,
      configurable: false,
      enumerable: true,
      value: false
    }
  }));
  /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */
  let FORBID_TAGS = null;
  /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */
  let FORBID_ATTR = null;
  /* Decide if ARIA attributes are okay */
  let ALLOW_ARIA_ATTR = true;
  /* Decide if custom data attributes are okay */
  let ALLOW_DATA_ATTR = true;
  /* Decide if unknown protocols are okay */
  let ALLOW_UNKNOWN_PROTOCOLS = false;
  /* Decide if self-closing tags in attributes are allowed.
   * Usually removed due to a mXSS issue in jQuery 3.0 */
  let ALLOW_SELF_CLOSE_IN_ATTR = true;
  /* Output should be safe for common template engines.
   * This means, DOMPurify removes data attributes, mustaches and ERB
   */
  let SAFE_FOR_TEMPLATES = false;
  /* Output should be safe even for XML used within HTML and alike.
   * This means, DOMPurify removes comments when containing risky content.
   */
  let SAFE_FOR_XML = true;
  /* Decide if document with <html>... should be returned */
  let WHOLE_DOCUMENT = false;
  /* Track whether config is already set on this instance of DOMPurify. */
  let SET_CONFIG = false;
  /* Decide if all elements (e.g. style, script) must be children of
   * document.body. By default, browsers might move them to document.head */
  let FORCE_BODY = false;
  /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html
   * string (or a TrustedHTML object if Trusted Types are supported).
   * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead
   */
  let RETURN_DOM = false;
  /* Decide if a DOM `DocumentFragment` should be returned, instead of a html
   * string  (or a TrustedHTML object if Trusted Types are supported) */
  let RETURN_DOM_FRAGMENT = false;
  /* Try to return a Trusted Type object instead of a string, return a string in
   * case Trusted Types are not supported  */
  let RETURN_TRUSTED_TYPE = false;
  /* Output should be free from DOM clobbering attacks?
   * This sanitizes markups named with colliding, clobberable built-in DOM APIs.
   */
  let SANITIZE_DOM = true;
  /* Achieve full DOM Clobbering protection by isolating the namespace of named
   * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules.
   *
   * HTML/DOM spec rules that enable DOM Clobbering:
   *   - Named Access on Window (§7.3.3)
   *   - DOM Tree Accessors (§3.1.5)
   *   - Form Element Parent-Child Relations (§4.10.3)
   *   - Iframe srcdoc / Nested WindowProxies (§4.8.5)
   *   - HTMLCollection (§4.2.10.2)
   *
   * Namespace isolation is implemented by prefixing `id` and `name` attributes
   * with a constant string, i.e., `user-content-`
   */
  let SANITIZE_NAMED_PROPS = false;
  const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-';
  /* Keep element content when removing element? */
  let KEEP_CONTENT = true;
  /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead
   * of importing it into a new Document and returning a sanitized copy */
  let IN_PLACE = false;
  /* Allow usage of profiles like html, svg and mathMl */
  let USE_PROFILES = {};
  /* Tags to ignore content of when KEEP_CONTENT is true */
  let FORBID_CONTENTS = null;
  const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);
  /* Tags that are safe for data: URIs */
  let DATA_URI_TAGS = null;
  const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);
  /* Attributes safe for values like "javascript:" */
  let URI_SAFE_ATTRIBUTES = null;
  const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']);
  const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
  const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
  const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
  /* Document namespace */
  let NAMESPACE = HTML_NAMESPACE;
  let IS_EMPTY_INPUT = false;
  /* Allowed XHTML+XML namespaces */
  let ALLOWED_NAMESPACES = null;
  const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);
  let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);
  let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']);
  // Certain elements are allowed in both SVG and HTML
  // namespace. We need to specify them explicitly
  // so that they don't get erroneously deleted from
  // HTML namespace.
  const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']);
  /* Parsing of strict XHTML documents */
  let PARSER_MEDIA_TYPE = null;
  const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html'];
  const DEFAULT_PARSER_MEDIA_TYPE = 'text/html';
  let transformCaseFunc = null;
  /* Keep a reference to config to pass to hooks */
  let CONFIG = null;
  /* Ideally, do not touch anything below this line */
  /* ______________________________________________ */
  const formElement = document.createElement('form');
  const isRegexOrFunction = function isRegexOrFunction(testValue) {
    return testValue instanceof RegExp || testValue instanceof Function;
  };
  /**
   * _parseConfig
   *
   * @param cfg optional config literal
   */
  // eslint-disable-next-line complexity
  const _parseConfig = function _parseConfig() {
    let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
    if (CONFIG && CONFIG === cfg) {
      return;
    }
    /* Shield configuration object from tampering */
    if (!cfg || typeof cfg !== 'object') {
      cfg = {};
    }
    /* Shield configuration object from prototype pollution */
    cfg = clone(cfg);
    PARSER_MEDIA_TYPE =
    // eslint-disable-next-line unicorn/prefer-includes
    SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE;
    // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
    transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;
    /* Set configuration parameters */
    ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
    ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
    ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
    URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES;
    DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS;
    FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
    FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({});
    FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({});
    USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false;
    ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
    ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
    ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
    ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true
    SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false
    SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false; // Default true
    WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false
    RETURN_DOM = cfg.RETURN_DOM || false; // Default false
    RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false
    RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false
    FORCE_BODY = cfg.FORCE_BODY || false; // Default false
    SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true
    SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false
    KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
    IN_PLACE = cfg.IN_PLACE || false; // Default false
    IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;
    NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;
    MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS;
    HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS;
    CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};
    if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {
      CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;
    }
    if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {
      CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;
    }
    if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') {
      CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;
    }
    if (SAFE_FOR_TEMPLATES) {
      ALLOW_DATA_ATTR = false;
    }
    if (RETURN_DOM_FRAGMENT) {
      RETURN_DOM = true;
    }
    /* Parse profile info */
    if (USE_PROFILES) {
      ALLOWED_TAGS = addToSet({}, text);
      ALLOWED_ATTR = [];
      if (USE_PROFILES.html === true) {
        addToSet(ALLOWED_TAGS, html$1);
        addToSet(ALLOWED_ATTR, html);
      }
      if (USE_PROFILES.svg === true) {
        addToSet(ALLOWED_TAGS, svg$1);
        addToSet(ALLOWED_ATTR, svg);
        addToSet(ALLOWED_ATTR, xml);
      }
      if (USE_PROFILES.svgFilters === true) {
        addToSet(ALLOWED_TAGS, svgFilters);
        addToSet(ALLOWED_ATTR, svg);
        addToSet(ALLOWED_ATTR, xml);
      }
      if (USE_PROFILES.mathMl === true) {
        addToSet(ALLOWED_TAGS, mathMl$1);
        addToSet(ALLOWED_ATTR, mathMl);
        addToSet(ALLOWED_ATTR, xml);
      }
    }
    /* Merge configuration parameters */
    if (cfg.ADD_TAGS) {
      if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
        ALLOWED_TAGS = clone(ALLOWED_TAGS);
      }
      addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
    }
    if (cfg.ADD_ATTR) {
      if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
        ALLOWED_ATTR = clone(ALLOWED_ATTR);
      }
      addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
    }
    if (cfg.ADD_URI_SAFE_ATTR) {
      addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
    }
    if (cfg.FORBID_CONTENTS) {
      if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
        FORBID_CONTENTS = clone(FORBID_CONTENTS);
      }
      addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
    }
    /* Add #text in case KEEP_CONTENT is set to true */
    if (KEEP_CONTENT) {
      ALLOWED_TAGS['#text'] = true;
    }
    /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */
    if (WHOLE_DOCUMENT) {
      addToSet(ALLOWED_TAGS, ['html', 'head', 'body']);
    }
    /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */
    if (ALLOWED_TAGS.table) {
      addToSet(ALLOWED_TAGS, ['tbody']);
      delete FORBID_TAGS.tbody;
    }
    if (cfg.TRUSTED_TYPES_POLICY) {
      if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {
        throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');
      }
      if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {
        throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');
      }
      // Overwrite existing TrustedTypes policy.
      trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
      // Sign local variables required by `sanitize`.
      emptyHTML = trustedTypesPolicy.createHTML('');
    } else {
      // Uninitialized policy, attempt to initialize the internal dompurify policy.
      if (trustedTypesPolicy === undefined) {
        trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
      }
      // If creating the internal policy succeeded sign internal variables.
      if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {
        emptyHTML = trustedTypesPolicy.createHTML('');
      }
    }
    // Prevent further manipulation of configuration.
    // Not available in IE8, Safari 5, etc.
    if (freeze) {
      freeze(cfg);
    }
    CONFIG = cfg;
  };
  /* Keep track of all possible SVG and MathML tags
   * so that we can perform the namespace checks
   * correctly. */
  const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
  const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
  /**
   * @param element a DOM element whose namespace is being checked
   * @returns Return false if the element has a
   *  namespace that a spec-compliant parser would never
   *  return. Return true otherwise.
   */
  const _checkValidNamespace = function _checkValidNamespace(element) {
    let parent = getParentNode(element);
    // In JSDOM, if we're inside shadow DOM, then parentNode
    // can be null. We just simulate parent in this case.
    if (!parent || !parent.tagName) {
      parent = {
        namespaceURI: NAMESPACE,
        tagName: 'template'
      };
    }
    const tagName = stringToLowerCase(element.tagName);
    const parentTagName = stringToLowerCase(parent.tagName);
    if (!ALLOWED_NAMESPACES[element.namespaceURI]) {
      return false;
    }
    if (element.namespaceURI === SVG_NAMESPACE) {
      // The only way to switch from HTML namespace to SVG
      // is via <svg>. If it happens via any other tag, then
      // it should be killed.
      if (parent.namespaceURI === HTML_NAMESPACE) {
        return tagName === 'svg';
      }
      // The only way to switch from MathML to SVG is via`
      // svg if parent is either <annotation-xml> or MathML
      // text integration points.
      if (parent.namespaceURI === MATHML_NAMESPACE) {
        return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
      }
      // We only allow elements that are defined in SVG
      // spec. All others are disallowed in SVG namespace.
      return Boolean(ALL_SVG_TAGS[tagName]);
    }
    if (element.namespaceURI === MATHML_NAMESPACE) {
      // The only way to switch from HTML namespace to MathML
      // is via <math>. If it happens via any other tag, then
      // it should be killed.
      if (parent.namespaceURI === HTML_NAMESPACE) {
        return tagName === 'math';
      }
      // The only way to switch from SVG to MathML is via
      // <math> and HTML integration points
      if (parent.namespaceURI === SVG_NAMESPACE) {
        return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
      }
      // We only allow elements that are defined in MathML
      // spec. All others are disallowed in MathML namespace.
      return Boolean(ALL_MATHML_TAGS[tagName]);
    }
    if (element.namespaceURI === HTML_NAMESPACE) {
      // The only way to switch from SVG to HTML is via
      // HTML integration points, and from MathML to HTML
      // is via MathML text integration points
      if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
        return false;
      }
      if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
        return false;
      }
      // We disallow tags that are specific for MathML
      // or SVG and should never appear in HTML namespace
      return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
    }
    // For XHTML and XML documents that support custom namespaces
    if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {
      return true;
    }
    // The code should never reach this place (this means
    // that the element somehow got namespace that is not
    // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES).
    // Return false just in case.
    return false;
  };
  /**
   * _forceRemove
   *
   * @param node a DOM node
   */
  const _forceRemove = function _forceRemove(node) {
    arrayPush(DOMPurify.removed, {
      element: node
    });
    try {
      // eslint-disable-next-line unicorn/prefer-dom-node-remove
      getParentNode(node).removeChild(node);
    } catch (_) {
      remove(node);
    }
  };
  /**
   * _removeAttribute
   *
   * @param name an Attribute name
   * @param element a DOM node
   */
  const _removeAttribute = function _removeAttribute(name, element) {
    try {
      arrayPush(DOMPurify.removed, {
        attribute: element.getAttributeNode(name),
        from: element
      });
    } catch (_) {
      arrayPush(DOMPurify.removed, {
        attribute: null,
        from: element
      });
    }
    element.removeAttribute(name);
    // We void attribute values for unremovable "is" attributes
    if (name === 'is') {
      if (RETURN_DOM || RETURN_DOM_FRAGMENT) {
        try {
          _forceRemove(element);
        } catch (_) {}
      } else {
        try {
          element.setAttribute(name, '');
        } catch (_) {}
      }
    }
  };
  /**
   * _initDocument
   *
   * @param dirty - a string of dirty markup
   * @return a DOM, filled with the dirty markup
   */
  const _initDocument = function _initDocument(dirty) {
    /* Create a HTML document */
    let doc = null;
    let leadingWhitespace = null;
    if (FORCE_BODY) {
      dirty = '<remove></remove>' + dirty;
    } else {
      /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */
      const matches = stringMatch(dirty, /^[\r\n\t ]+/);
      leadingWhitespace = matches && matches[0];
    }
    if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) {
      // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)
      dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>';
    }
    const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
    /*
     * Use the DOMParser API by default, fallback later if needs be
     * DOMParser not work for svg when has multiple root element.
     */
    if (NAMESPACE === HTML_NAMESPACE) {
      try {
        doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);
      } catch (_) {}
    }
    /* Use createHTMLDocument in case DOMParser is not available */
    if (!doc || !doc.documentElement) {
      doc = implementation.createDocument(NAMESPACE, 'template', null);
      try {
        doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload;
      } catch (_) {
        // Syntax error if dirtyPayload is invalid xml
      }
    }
    const body = doc.body || doc.documentElement;
    if (dirty && leadingWhitespace) {
      body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null);
    }
    /* Work on whole document or just its body */
    if (NAMESPACE === HTML_NAMESPACE) {
      return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0];
    }
    return WHOLE_DOCUMENT ? doc.documentElement : body;
  };
  /**
   * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document.
   *
   * @param root The root element or node to start traversing on.
   * @return The created NodeIterator
   */
  const _createNodeIterator = function _createNodeIterator(root) {
    return createNodeIterator.call(root.ownerDocument || root, root,
    // eslint-disable-next-line no-bitwise
    NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
  };
  /**
   * _isClobbered
   *
   * @param element element to check for clobbering attacks
   * @return true if clobbered, false if safe
   */
  const _isClobbered = function _isClobbered(element) {
    return element instanceof HTMLFormElement && (typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' || !(element.attributes instanceof NamedNodeMap) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function');
  };
  /**
   * Checks whether the given object is a DOM node.
   *
   * @param value object to check whether it's a DOM node
   * @return true is object is a DOM node
   */
  const _isNode = function _isNode(value) {
    return typeof Node === 'function' && value instanceof Node;
  };
  function _executeHooks(hooks, currentNode, data) {
    arrayForEach(hooks, hook => {
      hook.call(DOMPurify, currentNode, data, CONFIG);
    });
  }
  /**
   * _sanitizeElements
   *
   * @protect nodeName
   * @protect textContent
   * @protect removeChild
   * @param currentNode to check for permission to exist
   * @return true if node was killed, false if left alive
   */
  const _sanitizeElements = function _sanitizeElements(currentNode) {
    let content = null;
    /* Execute a hook if present */
    _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
    /* Check if element is clobbered or can clobber */
    if (_isClobbered(currentNode)) {
      _forceRemove(currentNode);
      return true;
    }
    /* Now let's check the element's type and name */
    const tagName = transformCaseFunc(currentNode.nodeName);
    /* Execute a hook if present */
    _executeHooks(hooks.uponSanitizeElement, currentNode, {
      tagName,
      allowedTags: ALLOWED_TAGS
    });
    /* Detect mXSS attempts abusing namespace confusion */
    if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) {
      _forceRemove(currentNode);
      return true;
    }
    /* Remove any occurrence of processing instructions */
    if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {
      _forceRemove(currentNode);
      return true;
    }
    /* Remove any kind of possibly harmful comments */
    if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) {
      _forceRemove(currentNode);
      return true;
    }
    /* Remove element if anything forbids its presence */
    if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
      /* Check if we have a custom element to handle */
      if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
        if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
          return false;
        }
        if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
          return false;
        }
      }
      /* Keep content except for bad-listed elements */
      if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
        const parentNode = getParentNode(currentNode) || currentNode.parentNode;
        const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
        if (childNodes && parentNode) {
          const childCount = childNodes.length;
          for (let i = childCount - 1; i >= 0; --i) {
            const childClone = cloneNode(childNodes[i], true);
            childClone.__removalCount = (currentNode.__removalCount || 0) + 1;
            parentNode.insertBefore(childClone, getNextSibling(currentNode));
          }
        }
      }
      _forceRemove(currentNode);
      return true;
    }
    /* Check whether element has a valid namespace */
    if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
      _forceRemove(currentNode);
      return true;
    }
    /* Make sure that older browsers don't get fallback-tag mXSS */
    if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) {
      _forceRemove(currentNode);
      return true;
    }
    /* Sanitize element content to be template-safe */
    if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
      /* Get the element's text content */
      content = currentNode.textContent;
      arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
        content = stringReplace(content, expr, ' ');
      });
      if (currentNode.textContent !== content) {
        arrayPush(DOMPurify.removed, {
          element: currentNode.cloneNode()
        });
        currentNode.textContent = content;
      }
    }
    /* Execute a hook if present */
    _executeHooks(hooks.afterSanitizeElements, currentNode, null);
    return false;
  };
  /**
   * _isValidAttribute
   *
   * @param lcTag Lowercase tag name of containing element.
   * @param lcName Lowercase attribute name.
   * @param value Attribute value.
   * @return Returns true if `value` is valid, otherwise false.
   */
  // eslint-disable-next-line complexity
  const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {
    /* Make sure attribute cannot clobber */
    if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
      return false;
    }
    /* Allow valid data-* attributes: At least one character after "-"
        (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
        XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
        We don't need to check the value; it's always URI safe. */
    if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {
      if (
      // First condition does a very basic check if a) it's basically a valid custom element tagname AND
      // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
      // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck
      _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)) ||
      // Alternative, second condition checks if it's an `is`-attribute, AND
      // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
      lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))) ; else {
        return false;
      }
      /* Check value is safe. First, is attr inert? If so, is safe */
    } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) {
      return false;
    } else ;
    return true;
  };
  /**
   * _isBasicCustomElement
   * checks if at least one dash is included in tagName, and it's not the first char
   * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name
   *
   * @param tagName name of the tag of the node to sanitize
   * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
   */
  const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
    return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT);
  };
  /**
   * _sanitizeAttributes
   *
   * @protect attributes
   * @protect nodeName
   * @protect removeAttribute
   * @protect setAttribute
   *
   * @param currentNode to sanitize
   */
  const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
    /* Execute a hook if present */
    _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
    const {
      attributes
    } = currentNode;
    /* Check if we have attributes; if not we might have a text node */
    if (!attributes || _isClobbered(currentNode)) {
      return;
    }
    const hookEvent = {
      attrName: '',
      attrValue: '',
      keepAttr: true,
      allowedAttributes: ALLOWED_ATTR,
      forceKeepAttr: undefined
    };
    let l = attributes.length;
    /* Go backwards over all attributes; safely remove bad ones */
    while (l--) {
      const attr = attributes[l];
      const {
        name,
        namespaceURI,
        value: attrValue
      } = attr;
      const lcName = transformCaseFunc(name);
      const initValue = attrValue;
      let value = name === 'value' ? initValue : stringTrim(initValue);
      /* Execute a hook if present */
      hookEvent.attrName = lcName;
      hookEvent.attrValue = value;
      hookEvent.keepAttr = true;
      hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set
      _executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent);
      value = hookEvent.attrValue;
      /* Full DOM Clobbering protection via namespace isolation,
       * Prefix id and name attributes with `user-content-`
       */
      if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {
        // Remove the attribute with this value
        _removeAttribute(name, currentNode);
        // Prefix the value and later re-create the attribute with the sanitized value
        value = SANITIZE_NAMED_PROPS_PREFIX + value;
      }
      /* Work around a security issue with comments inside attributes */
      if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|title)/i, value)) {
        _removeAttribute(name, currentNode);
        continue;
      }
      /* Did the hooks approve of the attribute? */
      if (hookEvent.forceKeepAttr) {
        continue;
      }
      /* Did the hooks approve of the attribute? */
      if (!hookEvent.keepAttr) {
        _removeAttribute(name, currentNode);
        continue;
      }
      /* Work around a security issue in jQuery 3.0 */
      if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
        _removeAttribute(name, currentNode);
        continue;
      }
      /* Sanitize attribute content to be template-safe */
      if (SAFE_FOR_TEMPLATES) {
        arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
          value = stringReplace(value, expr, ' ');
        });
      }
      /* Is `value` valid for this attribute? */
      const lcTag = transformCaseFunc(currentNode.nodeName);
      if (!_isValidAttribute(lcTag, lcName, value)) {
        _removeAttribute(name, currentNode);
        continue;
      }
      /* Handle attributes that require Trusted Types */
      if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') {
        if (namespaceURI) ; else {
          switch (trustedTypes.getAttributeType(lcTag, lcName)) {
            case 'TrustedHTML':
              {
                value = trustedTypesPolicy.createHTML(value);
                break;
              }
            case 'TrustedScriptURL':
              {
                value = trustedTypesPolicy.createScriptURL(value);
                break;
              }
          }
        }
      }
      /* Handle invalid data-* attribute set by try-catching it */
      if (value !== initValue) {
        try {
          if (namespaceURI) {
            currentNode.setAttributeNS(namespaceURI, name, value);
          } else {
            /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
            currentNode.setAttribute(name, value);
          }
          if (_isClobbered(currentNode)) {
            _forceRemove(currentNode);
          } else {
            arrayPop(DOMPurify.removed);
          }
        } catch (_) {
          _removeAttribute(name, currentNode);
        }
      }
    }
    /* Execute a hook if present */
    _executeHooks(hooks.afterSanitizeAttributes, currentNode, null);
  };
  /**
   * _sanitizeShadowDOM
   *
   * @param fragment to iterate over recursively
   */
  const _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) {
    let shadowNode = null;
    const shadowIterator = _createNodeIterator(fragment);
    /* Execute a hook if present */
    _executeHooks(hooks.beforeSanitizeShadowDOM, fragment, null);
    while (shadowNode = shadowIterator.nextNode()) {
      /* Execute a hook if present */
      _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
      /* Sanitize tags and elements */
      _sanitizeElements(shadowNode);
      /* Check attributes next */
      _sanitizeAttributes(shadowNode);
      /* Deep shadow DOM detected */
      if (shadowNode.content instanceof DocumentFragment) {
        _sanitizeShadowDOM(shadowNode.content);
      }
    }
    /* Execute a hook if present */
    _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
  };
  // eslint-disable-next-line complexity
  DOMPurify.sanitize = function (dirty) {
    let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
    let body = null;
    let importedNode = null;
    let currentNode = null;
    let returnNode = null;
    /* Make sure we have a string to sanitize.
      DO NOT return early, as this will return the wrong type if
      the user has requested a DOM object rather than a string */
    IS_EMPTY_INPUT = !dirty;
    if (IS_EMPTY_INPUT) {
      dirty = '<!-->';
    }
    /* Stringify, in case dirty is an object */
    if (typeof dirty !== 'string' && !_isNode(dirty)) {
      if (typeof dirty.toString === 'function') {
        dirty = dirty.toString();
        if (typeof dirty !== 'string') {
          throw typeErrorCreate('dirty is not a string, aborting');
        }
      } else {
        throw typeErrorCreate('toString is not a function');
      }
    }
    /* Return dirty HTML if DOMPurify cannot run */
    if (!DOMPurify.isSupported) {
      return dirty;
    }
    /* Assign config vars */
    if (!SET_CONFIG) {
      _parseConfig(cfg);
    }
    /* Clean up removed elements */
    DOMPurify.removed = [];
    /* Check if dirty is correctly typed for IN_PLACE */
    if (typeof dirty === 'string') {
      IN_PLACE = false;
    }
    if (IN_PLACE) {
      /* Do some early pre-sanitization to avoid unsafe root nodes */
      if (dirty.nodeName) {
        const tagName = transformCaseFunc(dirty.nodeName);
        if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
          throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
        }
      }
    } else if (dirty instanceof Node) {
      /* If dirty is a DOM element, append to an empty document to avoid
         elements being stripped by the parser */
      body = _initDocument('<!---->');
      importedNode = body.ownerDocument.importNode(dirty, true);
      if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === 'BODY') {
        /* Node is already a body, use as is */
        body = importedNode;
      } else if (importedNode.nodeName === 'HTML') {
        body = importedNode;
      } else {
        // eslint-disable-next-line unicorn/prefer-dom-node-append
        body.appendChild(importedNode);
      }
    } else {
      /* Exit directly if we have nothing to do */
      if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
      // eslint-disable-next-line unicorn/prefer-includes
      dirty.indexOf('<') === -1) {
        return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
      }
      /* Initialize the document to work on */
      body = _initDocument(dirty);
      /* Check we have a DOM node from the data */
      if (!body) {
        return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : '';
      }
    }
    /* Remove first element node (ours) if FORCE_BODY is set */
    if (body && FORCE_BODY) {
      _forceRemove(body.firstChild);
    }
    /* Get node iterator */
    const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);
    /* Now start iterating over the created document */
    while (currentNode = nodeIterator.nextNode()) {
      /* Sanitize tags and elements */
      _sanitizeElements(currentNode);
      /* Check attributes next */
      _sanitizeAttributes(currentNode);
      /* Shadow DOM detected, sanitize it */
      if (currentNode.content instanceof DocumentFragment) {
        _sanitizeShadowDOM(currentNode.content);
      }
    }
    /* If we sanitized `dirty` in-place, return it. */
    if (IN_PLACE) {
      return dirty;
    }
    /* Return sanitized string or DOM */
    if (RETURN_DOM) {
      if (RETURN_DOM_FRAGMENT) {
        returnNode = createDocumentFragment.call(body.ownerDocument);
        while (body.firstChild) {
          // eslint-disable-next-line unicorn/prefer-dom-node-append
          returnNode.appendChild(body.firstChild);
        }
      } else {
        returnNode = body;
      }
      if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) {
        /*
          AdoptNode() is not used because internal state is not reset
          (e.g. the past names map of a HTMLFormElement), this is safe
          in theory but we would rather not risk another attack vector.
          The state that is cloned by importNode() is explicitly defined
          by the specs.
        */
        returnNode = importNode.call(originalDocument, returnNode, true);
      }
      return returnNode;
    }
    let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;
    /* Serialize doctype if allowed */
    if (WHOLE_DOCUMENT && ALLOWED_TAGS['!doctype'] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) {
      serializedHTML = '<!DOCTYPE ' + body.ownerDocument.doctype.name + '>\n' + serializedHTML;
    }
    /* Sanitize final string template-safe */
    if (SAFE_FOR_TEMPLATES) {
      arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
        serializedHTML = stringReplace(serializedHTML, expr, ' ');
      });
    }
    return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
  };
  DOMPurify.setConfig = function () {
    let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
    _parseConfig(cfg);
    SET_CONFIG = true;
  };
  DOMPurify.clearConfig = function () {
    CONFIG = null;
    SET_CONFIG = false;
  };
  DOMPurify.isValidAttribute = function (tag, attr, value) {
    /* Initialize shared config vars if necessary. */
    if (!CONFIG) {
      _parseConfig({});
    }
    const lcTag = transformCaseFunc(tag);
    const lcName = transformCaseFunc(attr);
    return _isValidAttribute(lcTag, lcName, value);
  };
  DOMPurify.addHook = function (entryPoint, hookFunction) {
    if (typeof hookFunction !== 'function') {
      return;
    }
    arrayPush(hooks[entryPoint], hookFunction);
  };
  DOMPurify.removeHook = function (entryPoint, hookFunction) {
    if (hookFunction !== undefined) {
      const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
      return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0];
    }
    return arrayPop(hooks[entryPoint]);
  };
  DOMPurify.removeHooks = function (entryPoint) {
    hooks[entryPoint] = [];
  };
  DOMPurify.removeAllHooks = function () {
    hooks = _createHooksMap();
  };
  return DOMPurify;
}
var purify = createDOMPurify();

const httpMessages$1 =
    {
        "401": "Access unauthorized",
        "403": "Access forbidden",
        "404": "Not found"
    };

class AlertDialog$1 {
    /**
     * Initialize a new alert dialog
     * @param parent
     * @param alertProps - Optional - properties such as scroll to error
     */
    constructor(parent, alertProps) {
        this.alertProps = Object.assign({
            /** When an alert is presented - focus occur */
            shouldFocus: true,
            /** When focus occur - scroll into that element in the view */
            preventScroll: false
        }, alertProps);

        // container
        this.container = document.createElement('div');
        this.container.className = "igv-ui-alert-dialog-container";
        parent.appendChild(this.container);
        this.container.setAttribute('tabIndex', '-1');

        // header
        const header = document.createElement('div');
        this.container.appendChild(header);

        this.errorHeadline = document.createElement('div');
        header.appendChild(this.errorHeadline);
        this.errorHeadline.textContent = '';

        // body container
        let bodyContainer = document.createElement('div');
        bodyContainer.className = 'igv-ui-alert-dialog-body';
        this.container.appendChild(bodyContainer);

        // body copy
        this.body = document.createElement('div');
        this.body.className = 'igv-ui-alert-dialog-body-copy';
        bodyContainer.appendChild(this.body);

        // ok container
        let ok_container = document.createElement('div');
        this.container.appendChild(ok_container);

        // ok
        this.ok = document.createElement('div');
        ok_container.appendChild(this.ok);
        this.ok.textContent = 'OK';

        const okHandler = () => {

            if (typeof this.callback === 'function') {
                this.callback("OK");
                this.callback = undefined;
            }
            this.body.innerHTML = '';
            this.container.style.display = 'none';
        };

        this.ok.addEventListener('click', event => {
            event.stopPropagation();
            okHandler();
        });

        this.container.addEventListener('keypress', event => {
            event.stopPropagation();
            if ('Enter' === event.key) {
                okHandler();
            }
        });

        makeDraggable$1(this.container, header);

        this.container.style.display = 'none';
    }

    present(alert, callback) {

        this.errorHeadline.textContent = alert.message ? 'ERROR' : '';
        let string = alert.message || alert;

        if (httpMessages$1.hasOwnProperty(string)) {
            string = httpMessages$1[string];
        }

        this.body.innerHTML = purify.sanitize(string);

        this.callback = callback;
        this.container.style.display = 'flex';
        if (this.alertProps.shouldFocus) {
            this.container.focus({ preventScroll: this.alertProps.preventScroll });
        }
    }
}

// Collection of helper functions for canvas rendering.  The "ctx" paramter in these functions is a canvas 2d context.


const IGVGraphics = {

    configureHighDPICanvas: function (ctx, w, h) {

        const scaleFactor = window.devicePixelRatio;
        // const scaleFactor = 1

        ctx.canvas.style.width = (`${w}px`);
        ctx.canvas.width = Math.floor(scaleFactor * w);

        ctx.canvas.style.height = (`${h}px`);
        ctx.canvas.height = Math.floor(scaleFactor * h);

        ctx.scale(scaleFactor, scaleFactor);

    },

    setProperties: function (ctx, properties) {

        for (var key in properties) {
            if (properties.hasOwnProperty(key)) {
                var value = properties[key];
                ctx[key] = value;
            }
        }
    },

    strokeLine: function (ctx, x1, y1, x2, y2, properties) {

        x1 = Math.floor(x1) + 0.5;
        y1 = Math.floor(y1) + 0.5;
        x2 = Math.floor(x2) + 0.5;
        y2 = Math.floor(y2) + 0.5;

        if (properties) {
            ctx.save();
            IGVGraphics.setProperties(ctx, properties);
        }

        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.stroke();

        if (properties) ctx.restore();
    },

    fillRect: function (ctx, x, y, w, h, properties) {

        if (properties) {
            ctx.save();
            IGVGraphics.setProperties(ctx, properties);
        }

        ctx.fillRect(x, y, w, h);

        if (properties) ctx.restore();
    },

    fillPolygon: function (ctx, x, y, properties) {
        if (properties) {
            ctx.save();
            IGVGraphics.setProperties(ctx, properties);
        }
        doPath(ctx, x, y);
        ctx.fill();
        if (properties) ctx.restore();
    },

    strokePolygon: function (ctx, x, y, properties) {
        if (properties) {
            ctx.save();
            IGVGraphics.setProperties(ctx, properties);
        }
        doPath(ctx, x, y);
        ctx.stroke();
        if (properties) ctx.restore();
    },

    fillText: function (ctx, text, x, y, properties, transforms) {

        if (properties || transforms) {
            ctx.save();
        }

        if (properties) {
            IGVGraphics.setProperties(ctx, properties);
        }

        if (transforms) {
            // Slow path with context saving and extra translate
            ctx.translate(x, y);

            for (var transform in transforms) {
                var value = transforms[transform];

                // TODO: Add error checking for robustness
                if (transform === 'translate') {
                    ctx.translate(value['x'], value['y']);
                }
                if (transform === 'rotate') {
                    ctx.rotate(value['angle'] * Math.PI / 180);
                }
            }

            ctx.fillText(text, 0, 0);
        } else {
            ctx.fillText(text, x, y);
        }

        if (properties || transforms) ctx.restore();
    },

    strokeText: function (ctx, text, x, y, properties, transforms) {


        if (properties || transforms) {
            ctx.save();
        }

        if (properties) {
            IGVGraphics.setProperties(ctx, properties);
        }

        if (transforms) {
            ctx.translate(x, y);

            for (var transform in transforms) {
                var value = transforms[transform];

                // TODO: Add error checking for robustness
                if (transform === 'translate') {
                    ctx.translate(value['x'], value['y']);
                }
                if (transform === 'rotate') {
                    ctx.rotate(value['angle'] * Math.PI / 180);
                }
            }

            ctx.strokeText(text, 0, 0);
        } else {
            ctx.strokeText(text, x, y);
        }

        if (properties || transforms) ctx.restore();
    },

    strokeCircle: function (ctx, x, y, radius, properties) {
        if (properties) {
            ctx.save();
            IGVGraphics.setProperties(ctx, properties);
        }
        ctx.beginPath();
        ctx.arc(x, y, radius, 0, 2 * Math.PI);
        ctx.stroke();
        if (properties) ctx.restore();
    },

    fillCircle: function (ctx, x, y, radius, properties) {
        if (properties) {
            ctx.save();
            IGVGraphics.setProperties(ctx, properties);
        }
        ctx.beginPath();
        ctx.arc(x, y, radius, 0, 2 * Math.PI);
        ctx.fill();
        if (properties) ctx.restore();
    },

    drawArrowhead: function (ctx, x, y, size, lineWidth) {

        ctx.save();
        if (!size) {
            size = 5;
        }
        if (lineWidth) {
            ctx.lineWidth = lineWidth;
        }
        ctx.beginPath();
        ctx.moveTo(x, y - size / 2);
        ctx.lineTo(x, y + size / 2);
        ctx.lineTo(x + size, y);
        ctx.lineTo(x, y - size / 2);
        ctx.closePath();
        ctx.fill();
        ctx.restore();
    },

    dashedLine: function (ctx, x1, y1, x2, y2, dashLen, properties = {}) {
        if (dashLen === undefined) dashLen = 2;
        ctx.setLineDash([dashLen, dashLen]);
        IGVGraphics.strokeLine(ctx, x1, y1, x2, y2, properties);
        ctx.setLineDash([]);
    },

    roundRect: function (ctx, x, y, width, height, radius, fill, stroke) {

        if (typeof stroke == "undefined") {
            stroke = true;
        }
        if (typeof radius === "undefined") {
            radius = 5;
        }
        ctx.beginPath();
        ctx.moveTo(x + radius, y);
        ctx.lineTo(x + width - radius, y);
        ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
        ctx.lineTo(x + width, y + height - radius);
        ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
        ctx.lineTo(x + radius, y + height);
        ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
        ctx.lineTo(x, y + radius);
        ctx.quadraticCurveTo(x, y, x + radius, y);
        ctx.closePath();
        if (stroke) {
            ctx.stroke();
        }
        if (fill) {
            ctx.fill();
        }
    },
    polygon: function (ctx, x, y, fill, stroke) {

        if (typeof stroke == "undefined") {
            stroke = true;
        }

        ctx.beginPath();
        var len = x.length;
        ctx.moveTo(x[0], y[0]);
        for (var i = 1; i < len; i++) {
            ctx.lineTo(x[i], y[i]);
            // this.moveTo(x[i], y[i]);
        }

        ctx.closePath();
        if (stroke) {
            ctx.stroke();
        }
        if (fill) {
            ctx.fill();
        }
    },

    drawRandomColorVerticalLines: ctx => {
        for (let x = 0; x < ctx.canvas.width; x++) {
            IGVGraphics.fillRect(ctx, x, 0, 1, ctx.canvas.height, { fillStyle: randomRGB$1(100, 250) });
        }
    },

    labelTransformWithContext: (ctx, exe) => {
        ctx.translate(exe, 0);
        ctx.scale(-1, 1);
        ctx.translate(-exe, 0);
    }

};

function doPath(ctx, x, y) {
    var i, len = x.length;
    ctx.beginPath();
    ctx.moveTo(x[0], y[0]);
    for (i = 1; i < len; i++) {
        ctx.lineTo(x[i], y[i]);
    }
    ctx.closePath();
}

const pairs =
    [
        ['A', 'T'],
        ['G', 'C'],
        ['Y', 'R'],
        ['W', 'S'],
        ['K', 'M'],
        ['D', 'H'],
        ['B', 'V']
    ];

const complements = new Map();
for (let p of pairs) {
    const p1 = p[0];
    const p2 = p[1];
    complements.set(p1, p2);
    complements.set(p2, p1);
    complements.set(p1.toLowerCase(), p2.toLowerCase());
    complements.set(p2.toLowerCase(), p1.toLowerCase());
}

function complementBase(base) {
    return complements.has(base) ? complements.get(base) : base
}

function complementSequence(sequence) {
    let comp = '';
    for (let base of sequence) {
        comp += complements.has(base) ? complements.get(base) : base;
    }
    return comp
}

function reverseComplementSequence(sequence) {

    let comp = '';
    let idx = sequence.length;
    while (idx-- > 0) {
        const base = sequence[idx];
        comp += complements.has(base) ? complements.get(base) : base;
    }
    return comp
}

/**
 * Object for chromosome meta-data
 */


class Chromosome {
    constructor(name, order, bpLength, altNames) {
        this.name = name;
        this.order = order;
        this.bpLength = bpLength;
        this.altNames = altNames;
    }

    getAltName(key) {
        return this.altNames && this.altNames.has(key) ? this.altNames.get(key) : this.name
    }
}

class GenomicInterval {

    constructor(chr, start, end, features) {
        this.chr = chr;
        this.start = start;
        this.end = end;
        this.features = features;
    }

    contains(chr, start, end) {
        return this.chr === chr &&
            this.start <= start &&
            this.end >= end
    }

    containsRange(range) {
        return this.chr === range.chr &&
            this.start <= range.start &&
            this.end >= range.end
    }

    get locusString() {
        return `${this.chr}:${this.start + 1}-${this.end}`
    }
}

class SequenceInterval extends GenomicInterval {

    constructor(chr, start, end, features) {
        super(chr, start, end, features);
    }

    getSequence(start, end) {
        if (start < this.start || end > this.end) {
            return null
        }
        const offset = start - this.start;
        const n = end - start;
        const seq = this.features ? this.features.substring(offset, offset + n) : null;
        return seq
    }

    hasSequence(start, end) {
        return start >= this.start && end <= this.end
    }

}

const splitLines$2 = splitLines$3;

const reservedProperties$1 = new Set(['fastaURL', 'indexURL', 'cytobandURL', 'indexed']);

class NonIndexedFasta {

    #chromosomeNames
    chromosomes = new Map()
    sequences = new Map()

    constructor(reference) {

        this.fastaURL = reference.fastaURL;
        this.withCredentials = reference.withCredentials;


        // Build a track-like config object from the referenceObject
        const config = {};
        for (let key in reference) {
            if (reference.hasOwnProperty(key) && !reservedProperties$1.has(key)) {
                config[key] = reference[key];
            }
        }
        this.config = config;
    }


    async init() {
        return this.loadAll()
    }

    getSequenceRecord(chr) {
        return this.chromosomes.get(chr)
    }

    get chromosomeNames() {
        if (!this.#chromosomeNames) {
            this.#chromosomeNames = Array.from(this.chromosomes.keys());
        }
        return this.#chromosomeNames
    }

    getFirstChromosomeName() {
        return this.chromosomeNames[0]
    }

    async getSequence(chr, start, end) {

        if (this.sequences.size === 0) {
            await this.loadAll();
        }

        if (!this.sequences.has(chr)) {
            return undefined
        }

        let seqSlice = this.sequences.get(chr).find(ss => ss.contains(start, end));
        if (!seqSlice) {
            seqSlice = this.sequences.get(chr).find(ss => ss.overlaps(start, end));
            if (!seqSlice) {
                return undefined
            }
        }

        start -= seqSlice.offset;
        end -= seqSlice.offset;

        let prefix = "";
        if (start < 0) {
            for (let i = start; i < Math.min(end, 0); i++) {
                prefix += "*";
            }
        }

        if (end <= 0) {
            return prefix
        }

        const seq = seqSlice.sequence;
        const seqEnd = Math.min(end, seq.length);
        return prefix + seq.substring(start, seqEnd)
    }

    async loadAll() {


        const pushChromosome = (current, order) => {
            const length = current.length || (current.offset + current.seq.length);
            if (!chrNameSet.has(current.chr)) {
                this.sequences.set(current.chr, []);
                this.chromosomes.set(current.chr, new Chromosome(current.chr, order, length));
                chrNameSet.add(current.chr);
            } else {
                const c = this.chromosomes.get(current.chr);
                c.bpLength = Math.max(c.bpLength, length);
            }
            this.sequences.get(current.chr).push(new SequenceSlice(current.offset, current.seq));
        };

        let data;
        if (isDataURL(this.fastaURL)) {
            let bytes = decodeDataURI$1(this.fastaURL);
            data = "";
            for (let b of bytes) {
                data += String.fromCharCode(b);
            }
        } else {
            data = await igvxhr$1.load(this.fastaURL, buildOptions(this.config));
        }

        const chrNameSet = new Set();
        const lines = splitLines$2(data);
        let order = 0;
        let current = {};
        for (let nextLine of lines) {
            if (nextLine.startsWith("#") || nextLine.length === 0) ; else if (nextLine.startsWith(">")) {
                // Start the next sequence
                if (current.seq && current.seq.length > 0) {
                    pushChromosome(current, order++);
                }
                current.seq = "";

                const parts = nextLine.substr(1).split(/\s+/);


                // Check for @len= token, which is a non-standard extension supporting igv-reports.
                if (nextLine.includes("@len=")) {
                    const nameParts = parts[0].split(':');
                    current.chr = nameParts[0];
                    if (nameParts.length > 1 && nameParts[1].indexOf('-') > 0) {

                        const locusParts = nameParts[1].split('-');
                        if (locusParts.length === 2 &&
                            /^[0-9]+$/.test(locusParts[0]) &&
                            /^[0-9]+$/.test(locusParts[1])) ;
                        const from = Number.parseInt(locusParts[0]);
                        Number.parseInt(locusParts[1]);
                        current.offset = from - 1;

                        // Check for chromosome length token
                        if (parts.length > 1 && parts[1].startsWith("@len=")) {
                            try {
                                current.length = parseInt(parts[1].trim().substring(5));
                            } catch (e) {
                                current.length = undefined;
                                console.error(`Error parsing sequence length for ${nextLine}`);
                            }
                        } else {
                            current.length = undefined;
                        }
                    }
                } else {
                    // No special tokens, a standard FASTA header
                    current.chr = parts[0];
                    current.offset = 0;
                }

            } else {
                // Not a header or comment, so it must be sequence data
                current.seq += nextLine;
            }
        }

        // Handle the last sequence
        if (current.seq && current.seq.length > 0) {
            pushChromosome(current, order);
        }

    }

    /**
     * Return the first cached interval containing the specified region, or undefined if no interval is found.
     *
     * @param chr
     * @param start
     * @param end
     * @returns a SequenceInterval or undefined
     */
    getSequenceInterval(chr, start, end) {

        const slices = this.sequences.get(chr);
        if (!slices) return undefined

        for (let sequenceSlice of slices) {
            const seq = sequenceSlice.sequence;
            const seqStart = sequenceSlice.offset;
            const seqEnd = seqStart + seq.length;

            if (seqStart <= start && seqEnd >= end) {
                return new SequenceInterval(chr, seqStart, seqEnd, seq)
            }
        }
        return undefined

    }
}


class SequenceSlice {

    constructor(offset, sequence) {
        this.offset = offset;
        this.sequence = sequence;
    }

    contains(start, end) {
        return this.offset <= start && this.end >= end
    }

    overlaps(start, end) {
        return this.offset < end && this.end > start
    }

    get end() {
        return this.offset + this.sequence.length
    }

}

// Indexed fasta files

const splitLines$1 = splitLines$3;

const reservedProperties = new Set(['fastaURL', 'indexURL', 'compressedIndexURL', 'cytobandURL', 'indexed']);

class FastaSequence {

    #chromosomeNames
    chromosomes = new Map()

    constructor(reference) {

        this.file = reference.fastaURL;
        this.indexFile = reference.indexURL;
        this.compressedIndexFile = reference.compressedIndexURL;
        this.withCredentials = reference.withCredentials;


        // Build a track-like config object from the referenceObject
        const config = {};
        for (let key in reference) {
            if (reference.hasOwnProperty(key) && !reservedProperties.has(key)) {
                config[key] = reference[key];
            }
        }
        this.config = config;
    }

    getSequenceRecord(chr) {
        return this.chromosomes.get(chr)
    }

    async init() {
        return this.getIndex()
    }

    get chromosomeNames() {
        if(!this.#chromosomeNames) {
            this.#chromosomeNames = Array.from(this.chromosomes.keys());
        }
        return this.#chromosomeNames
    }

    getFirstChromosomeName() {
        return this.chromosomeNames[0]
    }

    async getIndex() {

        if (this.index) {
            return this.index
        } else {
            const data = await igvxhr$1.load(this.indexFile, buildOptions(this.config));
            const lines = splitLines$1(data);
            const len = lines.length;
            let lineNo = 0;
            let order = 0;
            this.index = {};

            while (lineNo < len) {
                const tokens = lines[lineNo++].split("\t");
                const nTokens = tokens.length;

                if (nTokens === 5) {
                    // Parse the index line.
                    const chr = tokens[0];
                    const size = parseInt(tokens[1]);
                    const position = parseInt(tokens[2]);
                    const basesPerLine = parseInt(tokens[3]);
                    const bytesPerLine = parseInt(tokens[4]);

                    const indexEntry = {
                        size: size,
                        position: position,
                        basesPerLine: basesPerLine,
                        bytesPerLine: bytesPerLine
                    };

                    this.index[chr] = indexEntry;
                    this.chromosomes.set(chr, new Chromosome(chr, order++, size));
                }
            }
            return this.index
        }
    }


    //Code is losely based on https://github.com/GMOD/bgzf-filehandle
    //Reworked however in orde to work with the igvxhr interface for loading files
    //Additionally, replaced calls to the Long.js interface with standard JS calls for ArrayBuffers and the associated views
    //
    //The compressed index is an array of blocks, with each block being a pair: compressed-position & uncompressed-position (both in bytes)
    async getCompressedIndex() {
        const GZI_NUM_BYTES_OFFSET = 8;
        const GZI_NUM_BYTES_BLOCK = 8;
        if (this.compressedIndex) {
            return this.compressedIndex
        }
        if (!this.compressedIndexFile) {
            this.compressedIndex = [];
            return this.compressedIndex
        }
        //In contrast to the 'normal' reference (for which the index is chromosome based), this index is block-based
        //As such there is not need to make it a hash. An array is sufficient.
        this.compressedIndex = [];
        const gziData = await igvxhr$1.loadArrayBuffer(this.compressedIndexFile, buildOptions(this.config));
        const givenFileSize = gziData.byteLength;
        if (givenFileSize < GZI_NUM_BYTES_OFFSET) {
            return this.compressedIndex
        }
        //First 8 bytes are a little endian unsigned bigint (64bit), indicating the number of blocks in the index.
        const numBlocksBuffer = gziData.slice(0, GZI_NUM_BYTES_OFFSET);
        const numBlocks = Number((new DataView(numBlocksBuffer)).getBigUint64(0, true));
        //The remainder of the gzi content are pairs of little endian unsigned bigint (64bit) numbers.
        //The first of the pair is the compressed position of a block
        //The second of the pair is the uncompressed position of a block

        //Sanity check:
        //Is the size of the array-buffer (of the entire file) correct with regards to the number of blocks detailled by the first 8 bytes of the file?
        //Total file-size should be:
        // 8 + 2*(num_entries*8) bytes, with the first 8 bytes indicating the number of entries
        const expectedFileSize = GZI_NUM_BYTES_OFFSET + numBlocks * 2 * GZI_NUM_BYTES_BLOCK;
        if (givenFileSize != expectedFileSize) {
            return this.compressedIndex
        }

        //Push the first block to the index: the first block always has positions 0 for both the compressed and uncompressed file
        this.compressedIndex.push([0, 0]);

        //Further process all the blocks of the GZI index, and keep them in memory
        for (let blockNumber = 0; blockNumber < numBlocks; blockNumber++) {
            const bufferBlockStart = GZI_NUM_BYTES_OFFSET + blockNumber * 2 * GZI_NUM_BYTES_BLOCK;
            const bufferBlockEnd = GZI_NUM_BYTES_OFFSET + blockNumber * 2 * GZI_NUM_BYTES_BLOCK + 2 * GZI_NUM_BYTES_BLOCK;
            const bufferBlock = gziData.slice(bufferBlockStart, bufferBlockEnd);
            const viewBlock = new DataView(bufferBlock);
            const compressedPosition = Number(viewBlock.getBigUint64(0, true));  //First 8 bytes
            const uncompressedPosition = Number(viewBlock.getBigUint64(GZI_NUM_BYTES_BLOCK, true)); //Last 8 bytes
            this.compressedIndex.push([compressedPosition, uncompressedPosition]);
        }
        return this.compressedIndex
    }

    //The Fasta-index gives a byte-position of the chromosomal sequences within the FASTA file.
    //These locations need to be remapped to the locations within the zipped reference genome, using the GZI index
    //This function provides this functionality by 
    //1) taking the indicated start/stop byte locations within the UNCOMPRESSED FASTA file
    //2) remapping these byte locations to the correct blocks (and associated positions) within the COMPRESSED FASTA file
    //Subsequently, the calling method can then extract the correct blocks from the compressed FASTA files and uncompressed the data
    async getRelevantCompressedBlockNumbers(queryPositionStart, queryPositionEnd) {
        const UNCOMPRESSED_POSITION = 1;
        //Fallback for impossible values
        if (queryPositionStart < 0 || queryPositionEnd < 0 || queryPositionEnd < queryPositionStart) {
            return []
        }
        //Ensure compressed index is loaded
        await this.getCompressedIndex();
        let result = [];
        //Now search for the correct block-numbers (going from 0 to length(compressed-index)) which overlap with the provided byte-positions
        const lowestBlockNumber = 0;
        const highestBlockNumber = this.compressedIndex.length - 1;
        //Failsafe if for some reason the compressed index wasn't loaded or doesn't contain any data
        if (this.compressedIndex.length == 0) {
            return []
        }
        //Failsafe: if the queryPositionStart is greater than the uncompressed-position of the final block,
        //then this final block is the only possible result
        if (queryPositionStart > (this.compressedIndex)[highestBlockNumber][UNCOMPRESSED_POSITION]) {
            return [highestBlockNumber]
        }

        //Rather than doing a linear search over all blocks, a binary search is done for speed considerations
        //We are searching for the highest block number for which its position is smaller than the query start position
        //Afterwards we will simply expand the blocks until the entire query range is covered
        let searchLow = lowestBlockNumber;
        let searchHigh = highestBlockNumber;
        let searchPosition = Math.floor(this.compressedIndex.length / 2);
        let maxIterations = this.compressedIndex.length + 1;
        let solutionFound = false;
        //instead of doing a while(true), this for-loop prevents eternal loops in case of issues
        for (let iteration = 0; iteration < maxIterations; iteration++) {
            const searchUncompressedPosition = (this.compressedIndex)[searchPosition][UNCOMPRESSED_POSITION];
            const nextSearchUncompressedPosition = (searchPosition < (this.compressedIndex.length - 1)) ? (this.compressedIndex)[searchPosition + 1][UNCOMPRESSED_POSITION] : Infinity;
            //The query position lies within the current search block
            if (searchUncompressedPosition <= queryPositionStart && nextSearchUncompressedPosition > queryPositionStart) {
                solutionFound = true;
                break //searchPosition is the correct block number index
            }
            //Current block lies before the query position
            else if (searchUncompressedPosition < queryPositionStart) {
                searchLow = searchPosition + 1;
            }
            //Current block lies after the query position
            else {
                searchHigh = searchPosition - 1;
            }
            searchPosition = Math.ceil((searchHigh - searchLow) / 2) + searchLow;
        }
        //If for some reason the binary search did not reveal a correct block index, then we return the empty result
        if (!solutionFound) {
            return []
        }

        //Now extend the result by adding additional blocks until the entire query range is covered
        result.push(searchPosition);
        for (let blockIndex = searchPosition + 1; blockIndex < this.compressedIndex.length; blockIndex++) {
            result.push(blockIndex);
            const blockUncompressedPosition = (this.compressedIndex)[blockIndex][UNCOMPRESSED_POSITION];
            if (blockUncompressedPosition >= queryPositionEnd) {
                break
            }
        }

        //It is possible that the query end position lies AFTER the start of the final block
        //If this is the case, we add a 'fake' negative index which will be interpreted by the loadAndUncompressBlocks method as an indicator
        //to read until the end of the file 
        const finalRelevantBlock = result[result.length - 1];
        const finalIndexBlock = this.compressedIndex.length - 1;
        if (finalRelevantBlock === finalIndexBlock && (this.compressedIndex)[finalRelevantBlock][UNCOMPRESSED_POSITION] < queryPositionEnd) {
            result.push(-1);
        }

        return result
    }


    //Load the content from the blockIndices.
    //This is done on a per-block basis
    //Content of the first block will be trimmed in order to match the expected offset
    async loadAndUncompressBlocks(blockIndices, startByte) {
        const COMPRESSED_POSITION = 0;
        const UNCOMPRESSED_POSITION = 1;
        //Normally the compressed index should already exist, we're just makeing sure here
        await this.getCompressedIndex();

        if (blockIndices.length == 0) {
            return ""
        }

        //Storing data in seperate array with indices in order to assert order due to async behaviour of loops
        let resultCache = Array(blockIndices.length - 1);
        for (let i = 0; i < blockIndices.length - 1; i++) {
            const currentBlockNumber = blockIndices[i];
            const currentBlockInfo = (this.compressedIndex)[currentBlockNumber];
            const currentBlockCompressedPosition = currentBlockInfo[COMPRESSED_POSITION];

            const nextBlockNumber = blockIndices[i + 1];
            let compressedBytes = [];
            if (nextBlockNumber != -1) {  //default : read current entire block only
                const nextBlockInfo = (this.compressedIndex)[nextBlockNumber];
                const nextBlockCompressedPosition = nextBlockInfo[COMPRESSED_POSITION];
                const compressedLength = nextBlockCompressedPosition - currentBlockCompressedPosition;
                compressedBytes = await igvxhr$1.loadArrayBuffer(this.file, buildOptions(this.config, {
                    range: {
                        start: currentBlockCompressedPosition,
                        size: compressedLength
                    }
                }));
            } else {   // special case for query within final block: read until the end of the file
                compressedBytes = await igvxhr$1.loadArrayBuffer(this.file, buildOptions(this.config, {
                    range: {
                        start: currentBlockCompressedPosition
                    }
                }));
            }
            //now unzip the compressed bytes, and store them in the resultCache
            const uncompressedBytes = await unbgzf$1(compressedBytes);
            resultCache[i] = uncompressedBytes;
        }

        //Iterate over the result cache, create sequences from the data, and create a full sequence string from the data
        let result = "";
        for (let i = 0; i < resultCache.length; i++) {
            for (let j = 0; j < resultCache[i].length; j++) {
                const c = String.fromCharCode(resultCache[i][j]);
                result = result + c;
            }
        }

        //postprocess this data: because entire blocks are read we need to remove the first N bases of the first used block, 
        //which are not included in the original query positions
        const firstBlockInfo = (this.compressedIndex)[blockIndices[0]];
        const offset = startByte - firstBlockInfo[UNCOMPRESSED_POSITION];
        result = result.substring(offset);

        return result
    }


    async readSequence(chr, qstart, qend) {

        await this.getIndex();
        await this.getCompressedIndex(); //This will work even if no compressed index file is set

        const idxEntry = this.index[chr];
        if (!idxEntry) {
            // Tag interval with null so we don't try again
            // this.interval = new GenomicInterval(chr, qstart, qend, null)
            this.index[chr] = null;
            return null
        }

        const start = Math.max(0, qstart);    // qstart should never be < 0
        const end = Math.min(idxEntry.size, qend);
        const bytesPerLine = idxEntry.bytesPerLine;
        const basesPerLine = idxEntry.basesPerLine;
        const position = idxEntry.position;
        const nEndBytes = bytesPerLine - basesPerLine;
        const startLine = Math.floor(start / basesPerLine);
        const endLine = Math.floor(end / basesPerLine);
        const base0 = startLine * basesPerLine;   // Base at beginning of start line
        const offset = start - base0;
        const startByte = position + startLine * bytesPerLine + offset;
        const base1 = endLine * basesPerLine;
        const offset1 = end - base1;
        const endByte = position + endLine * bytesPerLine + offset1 - 1;
        const byteCount = endByte - startByte + 1;

        if (byteCount <= 0) {
            console.error("No sequence for " + chr + ":" + qstart + "-" + qend);
            return null
        }

        //If the compressed index file is set, then we are dealing with a compressed genome sequence
        //The selection of startByte/endByte is done for the non-compressed genome sequence.
        //These need to be 'converted' to the correct byte positions in the compressed genome sequence,
        //by making use of the compressed index (GZI file)
        let allBytes;
        if (!this.compressedIndexFile) {
            allBytes = await igvxhr$1.load(this.file, buildOptions(this.config, {
                range: {
                    start: startByte,
                    size: byteCount
                }
            }));
        } else {
            let relevantBlockIndices = await this.getRelevantCompressedBlockNumbers(startByte, endByte);
            if (relevantBlockIndices.length === 0) {
                return null
            }
            allBytes = await this.loadAndUncompressBlocks(relevantBlockIndices, startByte);
        }

        if (!allBytes) {
            return null
        }

        let nBases,
            seqBytes = "",
            srcPos = 0,
            allBytesLength = allBytes.length;

        if (offset > 0) {
            nBases = Math.min(end - start, basesPerLine - offset);
            seqBytes += allBytes.substr(srcPos, nBases);
            srcPos += (nBases + nEndBytes);
        }

        while (srcPos < allBytesLength) {
            nBases = Math.min(basesPerLine, allBytesLength - srcPos);
            seqBytes += allBytes.substr(srcPos, nBases);
            srcPos += (nBases + nEndBytes);
        }

        return seqBytes

    }
}

const splitLines = splitLines$3;

/**
 * Represents a reference object created from a ChromSizes file.  This is unusual, primarily for testing.
 */
class ChromSizes {

    #chromosomeNames
    chromosomes = new Map()

    constructor(url) {
        this.url = url;
    }

    async init() {
        return this.loadAll()
    }

    getSequenceRecord(chr) {
        return this.chromosomes.get(chr)
    }

    get chromosomeNames() {
        if(!this.#chromosomeNames) {
            this.#chromosomeNames = Array.from(this.chromosomes.keys());
        }
    }

    async getSequence(chr, start, end) {
        return null // TODO -- return array of "N"s?
    }

    async loadAll() {

        let data;
        if (isDataURL(this.url)) {
            let bytes = decodeDataURI$1(this.fastaURL);
            data = "";
            for (let b of bytes) {
                data += String.fromCharCode(b);
            }
        } else {
            data = await igvxhr$1.load(this.url, {});
        }

        const lines = splitLines(data);
        let order = 0;
        for (let nextLine of lines) {
            const tokens = nextLine.split('\t');
            if(tokens.length > 1) {
                const chrLength = Number.parseInt(tokens[1]);
                const chromosome = new Chromosome(tokens[0], order++, chrLength);
                this.chromosomes.set(tokens[0], chromosome);
            }
        }
    }

}

async function loadChromSizes(url) {

    const chromosomeSizes = new Map();

    let data;
    if (isDataURL(url)) {
        let bytes = decodeDataURI$1(url);
        data = "";
        for (let b of bytes) {
            data += String.fromCharCode(b);
        }
    } else {
        data = await igvxhr$1.load(url, {});
    }

    const lines = splitLines(data);
    let order = 0;
    for (let nextLine of lines) {
        const tokens = nextLine.split('\t');
        if(tokens.length > 1) {
            const chrLength = Number.parseInt(tokens[1]);
            chromosomeSizes.set(tokens[0], new Chromosome(tokens[0], order++, chrLength));
        }
    }
    return chromosomeSizes
}

class BinaryParser$1 {
    constructor(dataView, littleEndian = true) {

        this.littleEndian = littleEndian;
        this.position = 0;
        this.view = dataView;
        this.length = dataView.byteLength;
    }

    /**
     * Print the first "n" bytes to the console.  Used for debugging.
     * @param n
     */
    dumpBytes (n = 100) {
        const pos = this.position;
        const bytes = [];
        for(let i=0; i<= n; i++) {
            bytes.push(this.getByte());
        }
        this.setPosition(pos);
    }

    setPosition(position) {
        this.position = position;
    }

    available() {
        return this.length - this.position
    }

    remLength() {
        return this.length - this.position
    }

    hasNext() {
        return this.position < this.length - 1
    }

    getByte() {
        const retValue = this.view.getUint8(this.position, this.littleEndian);
        this.position++;
        return retValue
    }

    getShort() {
        const retValue = this.view.getInt16(this.position, this.littleEndian);
        this.position += 2;
        return retValue
    }

    getUShort() {
        const retValue = this.view.getUint16(this.position, this.littleEndian);
        this.position += 2;
        return retValue
    }


    getInt() {
        const retValue = this.view.getInt32(this.position, this.littleEndian);
        this.position += 4;
        return retValue
    }


    getUInt() {
        const retValue = this.view.getUint32(this.position, this.littleEndian);
        this.position += 4;
        return retValue
    }

    getLong() {

        // DataView doesn't support long. So we'll try manually
        var b = [];
        b[0] = this.view.getUint8(this.position);
        b[1] = this.view.getUint8(this.position + 1);
        b[2] = this.view.getUint8(this.position + 2);
        b[3] = this.view.getUint8(this.position + 3);
        b[4] = this.view.getUint8(this.position + 4);
        b[5] = this.view.getUint8(this.position + 5);
        b[6] = this.view.getUint8(this.position + 6);
        b[7] = this.view.getUint8(this.position + 7);

        let value = 0;
        if (this.littleEndian) {
            for (let i = b.length - 1; i >= 0; i--) {
                value = (value * 256) + b[i];
            }
        } else {
            for (let i = 0; i < b.length; i++) {
                value = (value * 256) + b[i];
            }
        }
        this.position += 8;
        return value
    }

    getString(len) {

        let s = "";
        let c;
        while ((c = this.view.getUint8(this.position++)) !== 0) {
            s += String.fromCharCode(c);
            if (len && s.length === len) break
        }
        return s
    }

    getFixedLengthString(len) {

        let s = "";
        for (let i = 0; i < len; i++) {
            const c = this.view.getUint8(this.position++);
            if (c > 0) {
                s += String.fromCharCode(c);
            }
        }
        return s
    }

    getFloat() {

        var retValue = this.view.getFloat32(this.position, this.littleEndian);
        this.position += 4;
        return retValue


    }

    getDouble() {

        var retValue = this.view.getFloat64(this.position, this.littleEndian);
        this.position += 8;
        return retValue
    }

    skip(n) {
        this.position += n;
        return this.position
    }


    /**
     * Return a BGZip (bam and tabix) virtual pointer
     * TODO -- why isn't 8th byte used ?
     * TODO -- does endian matter here ?
     * @returns {*}
     */
    getVPointer() {

        var position = this.position,
            offset = (this.view.getUint8(position + 1) << 8) | (this.view.getUint8(position)),
            byte6 = ((this.view.getUint8(position + 6) & 0xff) * 0x100000000),
            byte5 = ((this.view.getUint8(position + 5) & 0xff) * 0x1000000),
            byte4 = ((this.view.getUint8(position + 4) & 0xff) * 0x10000),
            byte3 = ((this.view.getUint8(position + 3) & 0xff) * 0x100),
            byte2 = ((this.view.getUint8(position + 2) & 0xff)),
            block = byte6 + byte5 + byte4 + byte3 + byte2;
        this.position += 8;

        return new VPointer$1(block, offset)
    }
}

class VPointer$1 {
    constructor(block, offset) {
        this.block = block;
        this.offset = offset;
    }

    isLessThan(vp) {
        return this.block < vp.block ||
            (this.block === vp.block && this.offset < vp.offset)
    }

    isGreaterThan(vp) {
        return this.block > vp.block ||
            (this.block === vp.block && this.offset > vp.offset)
    }

    isEqualTo(vp) {
        return this.block === vp.block && this.offset === vp.offset
    }

    print() {
        return "" + this.block + ":" + this.offset
    }
}

/**
 * A UCSC BigBed B+ tree, used to support searching the "extra indexes".
 *
 * Nodes are loaded on demand during search, avoiding the need to read the entire tree into
 * memory.  Tree nodes can be scattered across the file, making loading the entire tree unfeasible in reasonable time.
 */
class BPTree {

    static magic = 2026540177
    littleEndian = true
    type = 'BPTree'          // Either BPTree or BPChromTree
    nodeCache = new Map()

    static async loadBpTree(path, config, startOffset, type, loader) {
        const bpTree = new BPTree(path, config, startOffset, type, loader);
        return bpTree.init()
    }

    constructor(path, config, startOffset, type, loader) {
        this.path = path;
        this.config = config;
        this.startOffset = startOffset;
        if(type) {
            this.type = type;
        }
        this.loader = loader || igvxhr$1;
    }

    async init() {
        const binaryParser = await this.#getParserFor(this.startOffset, 32);
        let magic = binaryParser.getInt();
        if(magic !== BPTree.magic) {
            binaryParser.setPosition(0);
            this.littleEndian = !this.littleEndian;
            binaryParser.littleEndian = this.littleEndian;
            magic = binaryParser.getInt();
            if(magic !== BPTree.magic) {
                throw Error(`Bad magic number ${magic}`)
            }
        }

        const blockSize = binaryParser.getInt();
        const keySize = binaryParser.getInt();
        const valSize = binaryParser.getInt();
        const itemCount = binaryParser.getLong();
        const reserved = binaryParser.getLong();
        const nodeOffset = this.startOffset + 32;
        this.header = {magic, blockSize, keySize, valSize, itemCount, reserved, nodeOffset};
        return this
    }

    getItemCount() {
        if(!this.header) {
            throw Error("Header not initialized")
        }
        return this.header.itemCount
    }

    async search(term) {

        if(!this.header) {
            await this.init();
        }

        const walkTreeNode = async (offset) => {

            const node = await this.readTreeNode(offset);

            if (node.type === 1) {
                // Leaf node
                for (let item of node.items) {
                    if (term === item.key) {
                        return item.value
                    }
                }
            } else {
                // Non leaf node

                // Read and discard the first key.
                let childOffset = node.items[0].offset;

                for (let i = 1; i < node.items.length; i++) {
                    const key = node.items[i].key;
                    if (term.localeCompare(key) < 0) {
                        break
                    }
                    childOffset = node.items[i].offset;
                }

                return walkTreeNode(childOffset)
            }
        };

        // Kick things off
        return walkTreeNode(this.header.nodeOffset)
    }

    async readTreeNode (offset)  {

        if (this.nodeCache.has(offset)) {
            return this.nodeCache.get(offset)
        } else {
            let binaryParser = await this.#getParserFor(offset, 4);
            const type = binaryParser.getByte();
            binaryParser.getByte();
            const count = binaryParser.getUShort();
            const items = [];

            const {keySize, valSize} = this.header;

            if (type === 1) {
                // Leaf node
                const size = count * (keySize + valSize);
                binaryParser = await this.#getParserFor(offset + 4, size);
                for (let i = 0; i < count; i++) {
                    const key = binaryParser.getFixedLengthString(keySize);
                    let value;
                    if(this.type === 'BPChromTree') {
                        const id = binaryParser.getInt();
                        const size = binaryParser.getInt();
                        value = {id, size};
                    } else {
                        const offset = binaryParser.getLong();
                        if (valSize === 16) {
                            const length = binaryParser.getLong();
                            value = {offset, length};
                        } else {
                            value = {offset};
                        }
                    }
                    items.push({key, value});
                }
            } else {
                // Non leaf node
                const size = count * (keySize + 8);
                binaryParser = await this.#getParserFor(offset + 4, size);

                for (let i = 0; i < count; i++) {
                    const key = binaryParser.getFixedLengthString(keySize);
                    const offset = binaryParser.getLong();
                    items.push({key, offset});
                }
            }

            const node = {type, count, items};
            this.nodeCache.set(offset, node);
            return node
        }
    }

    async #getParserFor(start, size) {
        try {
            const data = await this.loader.loadArrayBuffer(this.path, buildOptions(this.config, {range: {start, size}}));
            return new BinaryParser$1(new DataView(data), this.littleEndian)
        } catch (e) {
            console.error(e);
        }
    }

}

/**
 * Note: Some portions of this code adapated from the GMOD two-bit.js project, @Copyright (c) 2017 Robert Buels
 * * https://github.com/GMOD/twobit-js/blob/master/src/twoBitFile.ts*
 */

const twoBit = ['T', 'C', 'A', 'G'];
const byteTo4Bases = [];
for (let i = 0; i < 256; i++) {
    byteTo4Bases.push(
        twoBit[(i >> 6) & 3] +
        twoBit[(i >> 4) & 3] +
        twoBit[(i >> 2) & 3] +
        twoBit[i & 3],
    );
}
const maskedByteTo4Bases = byteTo4Bases.map(bases => bases.toLowerCase());

class TwobitSequence {

    littleEndian
    metaIndex = new Map()
    chromosomeNames

    constructor(config) {
        this.url = config.twoBitURL || config.fastaURL;
        this.config = config;
        if(config.twoBitBptURL) {
            this.bptURL = config.twoBitBptURL;
        }
     }

    async init() {
        if(this.bptURL) {
            this.index = await BPTree.loadBpTree(this.bptURL, this.config, 0);
        } else {
            const idx = await this._readIndex();
            this.index = {
                search: async (name) =>  {
                    return idx.has(name) ? {offset: idx.get(name)} : undefined
                }
            };
        }
    }

    async readSequence(seqName, regionStart, regionEnd) {

        if (!this.index) {
            await this.init();
        }

        const record = await this.getSequenceRecord(seqName);
        if (!record) {
            return null
        }

        if (regionStart < 0) {
            throw new TypeError('regionStart cannot be less than 0')
        }
        // end defaults to the end of the sequence
        if (regionEnd === undefined || regionEnd > record.dnaSize) {
            regionEnd = record.dnaSize;
        }

        const nBlocks = this._getOverlappingBlocks(
            regionStart,
            regionEnd,
            record.nBlocks
        );
        const maskBlocks = this._getOverlappingBlocks(
            regionStart,
            regionEnd,
            record.maskBlocks
        );

        const baseBytesOffset = Math.floor(regionStart / 4);
        const start = record.packedPos + baseBytesOffset;
        const size = Math.floor(regionEnd / 4) - baseBytesOffset + 1;

        const baseBytesArrayBuffer = await igvxhr$1.loadArrayBuffer(this.url, {range: {start, size}});
        const baseBytes = new Uint8Array(baseBytesArrayBuffer);

        let sequenceBases = '';
        for (let genomicPosition = regionStart; genomicPosition < regionEnd; genomicPosition += 1) {

            // function checks if  we are currently masked

            while (maskBlocks.length && maskBlocks[0].end <= genomicPosition) {
                maskBlocks.shift();
            }
            const baseIsMasked = maskBlocks[0] && maskBlocks[0].start <= genomicPosition && maskBlocks[0].end > genomicPosition;


            // process the N block if we have one.  Masked "N" ("n")  is not supported
            if (nBlocks[0] && genomicPosition >= nBlocks[0].start && genomicPosition < nBlocks[0].end) {
                const currentNBlock = nBlocks.shift();
                while (genomicPosition < currentNBlock.end && genomicPosition < regionEnd) {
                    sequenceBases += 'N';
                    genomicPosition++;
                }
                genomicPosition--;
            } else {
                const bytePosition = Math.floor(genomicPosition / 4) - baseBytesOffset;
                const subPosition = genomicPosition % 4;
                const byte = baseBytes[bytePosition];

                sequenceBases += baseIsMasked
                    ? maskedByteTo4Bases[byte][subPosition]
                    : byteTo4Bases[byte][subPosition];

            }
        }
        return sequenceBases
    }

    /**
     * Read the internal index of the 2bit file.  This is a list of sequence names and their offsets in the file.
     *
     * @returns {Promise<Map<any, any>>}
     * @private
     */
    async _readIndex() {

        const index = new Map();
        this.chromosomeNames = [];

        const loadRange = {start: 0, size: 64};
        let arrayBuffer = await igvxhr$1.loadArrayBuffer(this.url, {range: loadRange});
        let dataView = new DataView(arrayBuffer);

        let ptr = 0;
        const magicLE = dataView.getUint32(ptr, true);
        const magicBE = dataView.getUint32(ptr, false);
        ptr += 4;

        const magic = 0x1A412743;
        if (magicLE === magic) {
            this.littleEndian = true;
        } else if (magicBE === magic) {
            this.littleEndian = false;
        } else {
            throw Error(`Bad magic number ${magic}`)
        }

        this.version = dataView.getUint32(ptr, this.littleEndian);
        ptr += 4;

        this.sequenceCount = dataView.getUint32(ptr, this.littleEndian);
        ptr += 4;

        this.reserved = dataView.getUint32(ptr, this.littleEndian);
        ptr += 4;

        // Loop through sequences loading name and file offset.  We don't know the precise size in bytes in advance.
        let estSize;
        let binaryBuffer;

        let estNameLength = 20;
        for (let i = 0; i < this.sequenceCount; i++) {

            if (!binaryBuffer || binaryBuffer.available() < 1) {
                estSize = (this.sequenceCount - i) * estNameLength;
                binaryBuffer = await this._loadBinaryBuffer(ptr, estSize);
            }
            const len = binaryBuffer.getByte();
            ptr += 1;

            if (binaryBuffer.available() < len + 5) {
                estSize = (this.sequenceCount - i) * estNameLength + 100;
                binaryBuffer = await this._loadBinaryBuffer(ptr, estSize);
            }
            const name = binaryBuffer.getString(len);
            const offset = binaryBuffer.getUInt();
            ptr += len + 4;
            index.set(name, offset);

            estNameLength = Math.floor(estNameLength * (i / (i + 1)) + name.length / (i + 1));

            this.chromosomeNames.push(name);
        }
        return index
    }

    /**
     * Fetch the sequence metadata for the given seq name *
     *
     * @param seqName
     * @returns {Promise<void>}
     */
    async getSequenceRecord(seqName) {

        if (!this.metaIndex.has(seqName)) {

            if (!this.index) {
                throw Error("TwobitSequence object must be initialized before accessing sequence")
            }

            let result = await this.index.search(seqName);
            if (!result) {
                return
            }
            let offset = result.offset;

            // Read size of dna data & # of "N" blocks
            let size = 8;
            let binaryBuffer = await this._loadBinaryBuffer(offset, size);
            const dnaSize = binaryBuffer.getUInt();
            const nBlockCount = binaryBuffer.getUInt();
            offset += size;

            // Read "N" blocks and # of mask blocks
            size = nBlockCount * (4 + 4) + 4;
            binaryBuffer = await this._loadBinaryBuffer(offset, size);
            const nBlockStarts = [];
            for (let i = 0; i < nBlockCount; i++) {
                nBlockStarts.push(binaryBuffer.getUInt());
            }
            const nBlockSizes = [];
            for (let i = 0; i < nBlockCount; i++) {
                nBlockSizes.push(binaryBuffer.getUInt());
            }
            const maskBlockCount = binaryBuffer.getUInt();
            offset += size;

            // Read "mask" blocks
            size = maskBlockCount * (4 + 4) + 4;
            binaryBuffer = await this._loadBinaryBuffer(offset, size);
            const maskBlockStarts = [];
            for (let i = 0; i < maskBlockCount; i++) {
                maskBlockStarts.push(binaryBuffer.getUInt());
            }
            const maskBlockSizes = [];
            for (let i = 0; i < maskBlockCount; i++) {
                maskBlockSizes.push(binaryBuffer.getUInt());
            }

            //Transform "N" and "mask" block data into something more useful
            const nBlocks = [];
            for (let i = 0; i < nBlockCount; i++) {
                nBlocks.push(new Block$1(nBlockStarts[i], nBlockSizes[i]));
            }
            const maskBlocks = [];
            for (let i = 0; i < maskBlockCount; i++) {
                maskBlocks.push(new Block$1(maskBlockStarts[i], maskBlockSizes[i]));
            }

            const reserved = binaryBuffer.getUInt();
            if (reserved != 0) {
                throw Error("Bad 2-bit file")
            }
            offset += size;
            const packedPos = offset;

            const meta = {
                dnaSize,
                nBlocks,
                maskBlocks,
                packedPos,
                bpLength: dnaSize
            };
            this.metaIndex.set(seqName, meta);


        }
        return this.metaIndex.get(seqName)
    }

    /**
     * Return blocks overlapping the genome region [start, end]
     *
     * TODO -- optimize this, currently it uses linear search
     * * *
     * @param start
     * @param end
     * @param blocks
     * @returns {*[]}
     * @private
     */
    _getOverlappingBlocks(start, end, blocks) {

        const overlappingBlocks = [];
        for (let block of blocks) {
            if (block.start > end) {
                break
            } else if (block.end < start) {
                continue
            } else {
                overlappingBlocks.push(block);
            }
        }
        return overlappingBlocks
    }

    async _loadBinaryBuffer(start, size) {
        const arrayBuffer = await igvxhr$1.loadArrayBuffer(this.url, {range: {start, size}});
        return new BinaryParser$1(new DataView(arrayBuffer), this.littleEndian)
    }
}

class Block$1 {

    constructor(start, size) {
        this.start = start;
        this.size = size;
    }

    get end() {
        return this.start + this.size

    }
}

/**
 * Wrapper for a sequence loader that provides caching
 */

class CachedSequence {

    static #minQuerySize = 1e5
    #currentQuery
    #cachedIntervals = []
    #maxIntervals = 10   // TODO - this should be >= the number of viewports for multi-locus view

    constructor(sequenceReader, browser) {
        this.sequenceReader = sequenceReader;
        this.browser = browser;
    }

    get chromosomes() {
        return this.sequenceReader.chromosomes
    }

    async getSequenceRecord(chr) {
        return this.sequenceReader.getSequenceRecord(chr)
    }

    async getSequence(chr, start, end) {

        let interval = this.#cachedIntervals.find(i => i.contains(chr, start, end));
        if (!interval) {
            interval =  await this.#queryForSequence(chr, start, end);
            this.#trimCache(interval);
            this.#cachedIntervals.push(interval);
        }

        if (interval) {
            const offset = start - interval.start;
            const n = end - start;
            const seq = interval.features ? interval.features.substring(offset, offset + n) : null;
            return seq
        } else {
            return undefined
        }
    }

    #trimCache(interval) {
        // Filter out redundant (subsumed) cached intervals
        this.#cachedIntervals = this.#cachedIntervals.filter(i => !interval.contains(i));
        if (this.#cachedIntervals.length === this.#maxIntervals) {
            this.#cachedIntervals.shift();
        }

        // Filter out out-of-view cached intervals.  Don't try this if there are too many frames, inefficient
        if (this.browser && this.browser.referenceFrameList.length < 100) {
            this.#cachedIntervals = this.#cachedIntervals.filter(i => {
                const b = undefined !== this.browser.referenceFrameList.find(frame => frame.overlaps(i));
                return b;
            });
        }
    }

    /**
     * Return the first cached interval containing the specified region, or undefined if no interval is found.
     *
     * @param chr
     * @param start
     * @param end
     * @returns a SequenceInterval or undefined
     */
    getSequenceInterval(chr, start, end) {
        return this.#cachedIntervals.find(i => i.contains(chr, start, end))
    }

    /**
     * Query for a sequence.  Returns a promise that is resolved when the asynchronous call to read sequence returns.
     *
     * @param chr
     * @param start
     * @param end
     * @returns {Promise<sequence>}
     */
    async #queryForSequence(chr, start, end) {
        // Expand query, to minimum of 100kb
        let qstart = start;
        let qend = end;
        if ((end - start) < CachedSequence.#minQuerySize) {
            const w = (end - start);
            const center = Math.round(start + w / 2);
            qstart = Math.max(0, center - CachedSequence.#minQuerySize/2);
            qend = qstart + CachedSequence.#minQuerySize;
        }
        const interval = new SequenceInterval(chr, qstart, qend);

        if (this.#currentQuery && this.#currentQuery[0].contains(chr, start, end)) {
            return this.#currentQuery[1]
        } else {
            const queryPromise = new Promise(async (resolve, reject) => {
                interval.features = await this.sequenceReader.readSequence(chr, qstart, qend);
                resolve(interval);
            });
            this.#currentQuery = [interval, queryPromise];
            return queryPromise
        }
    }


    async init() {
        return this.sequenceReader.init()
    }

    get chromosomeNames() {
        return this.sequenceReader.chromosomeNames
    }

    getFirstChromosomeName() {
        return typeof this.sequenceReader.getFirstChromosomeName === 'function' ? this.sequenceReader.getFirstChromosomeName() : undefined
    }

    #isIntervalInView(interval) {
        this.browser.referenceFrameList;
    }
}

/**
 * Create a sequence object.  The referenced object can include multiple sequence references, in particular
 * fasta and 2bit URLs.  This is for backward compatibility, the 2bit URL has preference.
 *
 * @param reference
 * @returns {Promise<CachedSequence|ChromSizes|NonIndexedFasta>}
 */
async function loadSequence(reference, browser) {

    let fasta;
    if ("chromsizes" === reference.format) {
        fasta = new ChromSizes(reference.fastaURL || reference.url);
    } else if ("2bit" === reference.format || reference.twoBitURL) {
        fasta = new CachedSequence(new TwobitSequence(reference), browser);
    } else if (isDataURL(reference.fastaURL) || !reference.indexURL) {
        fasta = new NonIndexedFasta(reference);
    } else if("gbk" === reference.format || reference.gbkURL) ;

    else {
        fasta = new CachedSequence(new FastaSequence(reference), browser);
    }
    await fasta.init();
    return fasta
}

const defaultNucleotideColors = {
    "A": "rgb(  0, 200,   0)",
    "C": "rgb(  0,0,200)",
    "T": "rgb(255,0,0)",
    "G": "rgb(209,113,  5)",
    "N": "rgb(80,80,80)"
};

/** An implementation of an interval tree, following the explanation.
 * from CLR.
 *
 * Public interface:
 *   Constructor  IntervalTree
 *   Insertion    insert
 *   Search       findOverlapping
 */

var BLACK$2 = 1;
var RED = 2;

var NIL$2 = {};
NIL$2.color = BLACK$2;
NIL$2.parent = NIL$2;
NIL$2.left = NIL$2;
NIL$2.right = NIL$2;


class IntervalTree {

    constructor() {
        this.root = NIL$2;
    }

    insert(start, end, value) {

        var interval = new Interval(start, end, value);
        var x = new Node$1(interval);
        this.treeInsert(x);
        x.color = RED;
        while (x !== this.root && x.parent.color === RED) {
            if (x.parent === x.parent.parent.left) {
                let y = x.parent.parent.right;
                if (y.color === RED) {
                    x.parent.color = BLACK$2;
                    y.color = BLACK$2;
                    x.parent.parent.color = RED;
                    x = x.parent.parent;
                } else {
                    if (x === x.parent.right) {
                        x = x.parent;
                        leftRotate.call(this, x);
                    }
                    x.parent.color = BLACK$2;
                    x.parent.parent.color = RED;
                    rightRotate.call(this, x.parent.parent);
                }
            } else {
                let y = x.parent.parent.left;
                if (y.color === RED) {
                    x.parent.color = BLACK$2;
                    y.color = BLACK$2;
                    x.parent.parent.color = RED;
                    x = x.parent.parent;
                } else {
                    if (x === x.parent.left) {
                        x = x.parent;
                        rightRotate.call(this, x);
                    }
                    x.parent.color = BLACK$2;
                    x.parent.parent.color = RED;
                    leftRotate.call(this, x.parent.parent);
                }
            }
        }
        this.root.color = BLACK$2;
    }

    /**
     *
     * @param start - query interval
     * @param end - query interval
     * @returns Array of all intervals overlapping the query region
     */
    findOverlapping(start, end) {


        var searchInterval = new Interval(start, end, 0);

        if (this.root === NIL$2) return [];

        var intervals = searchAll.call(this, searchInterval, this.root, []);

        if (intervals.length > 1) {
            intervals.sort(function (i1, i2) {
                return i1.low - i2.low;
            });
        }

        return intervals;
    }

    /**
     * Dump info on intervals to console.  For debugging.
     */
    logIntervals() {

        logNode(this.root);

        function logNode(node, indent) {

            if (node.left !== NIL$2) logNode(node.left);
            if (node.right !== NIL$2) logNode(node.right);
        }

    }

    mapIntervals(func) {

        applyInterval(this.root);

        function applyInterval(node) {

            func(node.interval);

            if (node.left !== NIL$2) applyInterval(node.left);
            if (node.right !== NIL$2) applyInterval(node.right);
        }
    }


    /**
     * Note:  Does not maintain RB constraints,  this is done post insert
     *
     * @param x  a Node
     */
    treeInsert(x) {
        var node = this.root;
        var y = NIL$2;
        while (node !== NIL$2) {
            y = node;
            if (x.interval.low <= node.interval.low) {
                node = node.left;
            } else {
                node = node.right;
            }
        }
        x.parent = y;

        if (y === NIL$2) {
            this.root = x;
            x.left = x.right = NIL$2;
        } else {
            if (x.interval.low <= y.interval.low) {
                y.left = x;
            } else {
                y.right = x;
            }
        }

        applyUpdate.call(this, x);
    }
}

function searchAll(interval, node, results) {

    if (node.interval.overlaps(interval)) {
        results.push(node.interval);
    }

    if (node.left !== NIL$2 && node.left.max >= interval.low) {
        searchAll.call(this, interval, node.left, results);
    }

    if (node.right !== NIL$2 && node.right.min <= interval.high) {
        searchAll.call(this, interval, node.right, results);
    }

    return results;
}

function leftRotate(x) {
    var y = x.right;
    x.right = y.left;
    if (y.left !== NIL$2) {
        y.left.parent = x;
    }
    y.parent = x.parent;
    if (x.parent === NIL$2) {
        this.root = y;
    } else {
        if (x.parent.left === x) {
            x.parent.left = y;
        } else {
            x.parent.right = y;
        }
    }
    y.left = x;
    x.parent = y;

    applyUpdate.call(this, x);
    // no need to apply update on y, since it'll y is an ancestor
    // of x, and will be touched by applyUpdate().
}


function rightRotate(x) {
    var y = x.left;
    x.left = y.right;
    if (y.right !== NIL$2) {
        y.right.parent = x;
    }
    y.parent = x.parent;
    if (x.parent === NIL$2) {
        this.root = y;
    } else {
        if (x.parent.right === x) {
            x.parent.right = y;
        } else {
            x.parent.left = y;
        }
    }
    y.right = x;
    x.parent = y;


    applyUpdate.call(this, x);
    // no need to apply update on y, since it'll y is an ancestor
    // of x, and will be touched by applyUpdate().
}


// Applies the statistic update on the node and its ancestors.
function applyUpdate(node) {
    while (node !== NIL$2) {
        var nodeMax = node.left.max > node.right.max ? node.left.max : node.right.max;
        var intervalHigh = node.interval.high;
        node.max = nodeMax > intervalHigh ? nodeMax : intervalHigh;

        var nodeMin = node.left.min < node.right.min ? node.left.min : node.right.min;
        var intervalLow = node.interval.low;
        node.min = nodeMin < intervalLow ? nodeMin : intervalLow;

        node = node.parent;
    }
}


class Interval {
    constructor(low, high, value) {
        this.low = low;
        this.high = high;
        this.value = value;
    }

    equals(other) {
        if (!other) {
            return false;
        }
        if (this === other) {
            return true;
        }
        return (this.low === other.low &&
            this.high === other.high);

    }

    compareTo(other) {
        if (this.low < other.low)
            return -1;
        if (this.low > other.low)
            return 1;

        if (this.high < other.high)
            return -1;
        if (this.high > other.high)
            return 1;

        return 0;
    }

    /**
     * Returns true if this interval overlaps the other.
     */
    overlaps(other) {
            return (this.low <= other.high && other.low <= this.high);
    }
}

function Node$1(interval) {
    this.parent = NIL$2;
    this.left = NIL$2;
    this.right = NIL$2;
    this.interval = interval;
    this.color = RED;
}

/**
 * Object for caching lists of features.  Supports efficient range queries (chr, start, end)
 *
 * @param featureList
 * @param The genomic range spanned by featureList (optional)
 * @constructor
 */

class FeatureCache$1 {

    constructor(featureList, range) {

        featureList = featureList || [];
        this.treeMap = this.buildTreeMap(featureList);
        this.range = range;
        this.count = featureList.length;
    }

    containsRange(genomicRange) {
        // No range means cache contains all features
        return (this.range === undefined || this.range.contains(genomicRange.chr, genomicRange.start, genomicRange.end));
    }

    /**
     * Search loaded features
     * @param fn
     */
    findFeatures(fn) {
        const found = [];
        for(let featureList of Object.values(this.allFeatures)) {
            for(let f of featureList) {
                if(fn(f)) {
                    found.push(f);
                }
            }
        }
        return found
    }

    queryFeatures(chr, start, end) {

        const tree = this.treeMap[chr];

        if (!tree) return [];

        const intervals = tree.findOverlapping(start, end);

        if (intervals.length === 0) {
            return [];
        } else {
            // Trim the list of features in the intervals to those
            // overlapping the requested range.
            // Assumption: features are sorted by start position

            const featureList = [];
            const all = this.allFeatures[chr];
            if (all) {
                for (let interval of intervals) {
                    const indexRange = interval.value;
                    for (let i = indexRange.start; i < indexRange.end; i++) {
                        let feature = all[i];
                        if (feature.start > end) break;
                        else if (feature.end >= start) {
                            featureList.push(feature);
                        }
                    }
                }
                featureList.sort(function (a, b) {
                    return a.start - b.start;
                });
            }
            return featureList;
        }
    };

    /**
     * Returns all features, unsorted.
     *
     * @returns {Array}
     */
    getAllFeatures() {
        return this.allFeatures;
    }

    buildTreeMap(featureList) {

        const treeMap = {};
        const chromosomes = [];
        this.allFeatures = {};

        if (featureList) {
            for (let feature of featureList) {

                const chr = feature.chr;
                let geneList = this.allFeatures[chr];
                if (!geneList) {
                    chromosomes.push(chr);
                    geneList = [];
                    this.allFeatures[chr] = geneList;
                }
                geneList.push(feature);
            }


            // Now build interval tree for each chromosome
            for (let chr of chromosomes) {
                const chrFeatures = this.allFeatures[chr];
                chrFeatures.sort(function (f1, f2) {
                    return (f1.start === f2.start ? 0 : (f1.start > f2.start ? 1 : -1));
                });
                treeMap[chr] = buildIntervalTree(chrFeatures);
            }
        }

        return treeMap;
    }
}

/**
 * Build an interval tree from the feature list for fast interval based queries.   We lump features in groups
 * of 10, or total size / 100,   to reduce size of the tree.
 *
 * @param featureList
 */
function buildIntervalTree(featureList) {

    const tree = new IntervalTree();
    const len = featureList.length;
    const chunkSize = Math.max(10, Math.round(len / 10));

    for (let i = 0; i < len; i += chunkSize) {
        const e = Math.min(len, i + chunkSize);
        const subArray = new IndexRange(i, e); //featureList.slice(i, e);
        const iStart = featureList[i].start;
        //
        let iEnd = iStart;
        for (let j = i; j < e; j++) {
            iEnd = Math.max(iEnd, featureList[j].end);
        }
        tree.insert(iStart, iEnd, subArray);
    }

    return tree;
}


class IndexRange {
    constructor(start, end) {
        this.start = start;
        this.end = end;
    }
}

/**
 * Decoder for bedpe records.
 *
 * Bedpe format was created by Aaron Quinlan et al as part of the bedtools project.
 * The spec is here:  https://bedtools.readthedocs.io/en/latest/content/general-usage.html,
 *
 * 1      2      3    4      5      6    7    8     9       10      11-
 * chrom1 start1 end1 chrom2 start2 end2 name score strand1 strand2 <Any number of additional, user-defined fields>
 *
 * However there are off spec variants, an important one being a 7 column format with score in place of the standard
 * name column.
 *
 * A common variant is a "hiccups" output file, which is standard bedpe with the exception of a header line
 * of the form
 *
 * chr1    x1    x2    chr2    y1    y2    name    score    strand1    strand2    color    observed    expectedBL    expectedDonut    expectedH    expectedV    fdrBL    fdrDonut    fdrH    fdrV
 *
 * The "hiccups" output is apparently not standardized as this is found at ENCODE, with a non-helpful "tsv" extension
 *
 * chr1    x1    x2    chr2    y1    y2    color    observed    expectedBL    expectedDonut    expectedH    expectedV    fdrBL    fdrDonut    fdrH    fdrV    numCollapsed    centroid1    centroid2    radius
 * chr9    136790000    136795000    chr9    136990000    136995000    0,255,255    101.0    31.100368    38.40316    56.948116    34.040756    1.1876738E-13    1.05936405E-13    2.5148233E-4    1.7220993E-13    1    136792500    136992500    25590
 *
 * The "hiccups" documentation specfies yet another set of column headers
 * chromosome1    x1    x2    chromosome2    y1    y2    color    observed expected_bottom_left    expected_donut    expected_horizontal    expected_vertical fdr_bottom_left    fdr_donut    fdr_horizontal    fdr_vertical number_collapsed    centroid1    centroid2    radius
 *
 * @param tokens
 * @param ignore
 * @returns {{start1: number, end2: number, end1: number, chr1: *, chr2: *, start2: number}|undefined}
 */

function decodeBedpe(tokens, header) {

    if (tokens.length < 6) {
        return undefined
    }

    var feature = {
        chr1: tokens[0],
        start1: Number.parseInt(tokens[1]),
        end1: Number.parseInt(tokens[2]),
        chr2: tokens[3],
        start2: Number.parseInt(tokens[4]),
        end2: Number.parseInt(tokens[5])
    };

    if (isNaN(feature.start1) || isNaN(feature.end1) || isNaN(feature.start2) || isNaN(feature.end2)) {
        //throw Error(`Error parsing line: ${tokens.join('\t')}`);
        return undefined
    }

    // Determine if this is a "hiccups" file.  Store result on "header" so it doesn't need repeated for every feature
    if(header && header.hiccups === undefined) {
        header.hiccups = header.columnNames ? isHiccups(header.columnNames) : false;
    }
    const hiccups = header && header.hiccups;
    const stdColumns = hiccups ? 6 : 10;

    if(!hiccups) {
        if (tokens.length > 6 && tokens[6] !== ".") {
            feature.name = tokens[6];
        }

        if (tokens.length > 7 && tokens[7] !== ".") {
            feature.score = Number(tokens[7]);
        }

        if (tokens.length > 8 && tokens[8] !== ".") {
            feature.strand1 = tokens[8];
        }

        if (tokens.length > 9 && tokens[9] !== ".") {
            feature.strand2 = tokens[9];
        }
    }

    // Optional extra columns
    if (header) {
        const colorColumn = header.colorColumn;
        if (colorColumn && colorColumn < tokens.length) {
            feature.color = IGVColor.createColorString(tokens[colorColumn]);
        }
        const thicknessColumn = header.thicknessColumn;
        if (thicknessColumn && thicknessColumn < tokens.length) {
            feature.thickness = tokens[thicknessColumn];
        }

        if (tokens.length > stdColumns && header.columnNames && header.columnNames.length === tokens.length) {
            feature.extras = tokens.slice(stdColumns);
        }
    }


    // Set total extent of feature
    if (feature.chr1 === feature.chr2) {
        feature.chr = feature.chr1;
        feature.start = Math.min(feature.start1, feature.start2);
        feature.end = Math.max(feature.end1, feature.end2);

    }
    return feature
}

/**
 * Hack for non-standard bedPE formats, where numeric score can be in column 7 (name field from spec)
 * @param features
 */
function fixBedPE(features) {

    if (features.length == 0) return

    // Assume all features have same properties
    const firstFeature = features[0];
    if (firstFeature.score === undefined && firstFeature.name !== undefined) {
        // Name field (col 7) is sometimes used for score.
        for (let f of features) {
            if (!(isNumber(f.name) || f.name === '.')) return
        }
        for (let f of features) {
            f.score = Number(f.name);
            delete f.name;
        }
    }

    // Make copies of inter-chr features, one for each chromosome
    const interChrFeatures = features.filter(f => f.chr1 !== f.chr2);
    for (let f1 of interChrFeatures) {
        const f2 = Object.assign({}, f1);
        f2.dup = true;
        features.push(f2);

        f1.chr = f1.chr1;
        f1.start = f1.start1;
        f1.end = f1.end1;

        f2.chr = f2.chr2;
        f2.start = f2.start2;
        f2.end = f2.end2;
    }
}


/**
 * Special decoder for Hic Domain files.   In these files feature1 == feature2, they are really bed records.
 * @param tokens
 * @param ignore
 * @returns {*}
 */
function decodeBedpeDomain(tokens, header) {

    if (tokens.length < 8) return undefined

    return {
        chr: tokens[0],
        start: Number.parseInt(tokens[1]),
        end: Number.parseInt(tokens[2]),
        color: IGVColor.createColorString(tokens[6]),
        value: Number(tokens[7])
    }
}

function isHiccups(columns) {
    return columns && (columns.includes("fdrDonut") || columns.includes("fdr_donut"))
}

/**
 * Decode UCSC "interact" files.  See https://genome.ucsc.edu/goldenpath/help/interact.html
 *
 0  string chrom;        "Chromosome (or contig, scaffold, etc.). For interchromosomal, use 2 records"
 1  uint chromStart;     "Start position of lower region. For interchromosomal, set to chromStart of this region"
 2  uint chromEnd;       "End position of upper region. For interchromosomal, set to chromEnd of this region"
 3  string name;         "Name of item, for display.  Usually 'sourceName/targetName/exp' or empty"
 4  uint score;          "Score (0-1000)"
 5  double value;        "Strength of interaction or other data value. Typically basis for score"
 6  string exp;          "Experiment name (metadata for filtering). Use . if not applicable"
 7  string color;        "Item color.  Specified as r,g,b or hexadecimal #RRGGBB or html color name, as in //www.w3.org/TR/css3-color/#html4. Use 0 and spectrum setting to shade by score"
 8  string sourceChrom;  "Chromosome of source region (directional) or lower region. For non-directional interchromosomal, chrom of this region."
 9  uint sourceStart;    "Start position in chromosome of source/lower/this region"
 10 uint sourceEnd;      "End position in chromosome of source/lower/this region"
 11 string sourceName;   "Identifier of source/lower/this region"
 12 string sourceStrand; "Orientation of source/lower/this region: + or -.  Use . if not applicable"
 13 string targetChrom;  "Chromosome of target region (directional) or upper region. For non-directional interchromosomal, chrom of other region"
 14 uint targetStart;    "Start position in chromosome of target/upper/this region"
 15 uint targetEnd;      "End position in chromosome of target/upper/this region"
 16 string targetName;   "Identifier of target/upper/this region"
 17 string targetStrand; "Orientation of target/upper/this region: + or -.  Use . if not applicable"
 *
 * @param tokens
 * @param ignore
 * @returns {*}
 */
function decodeInteract(tokens, header) {

    if (tokens.length < 6) {
        return undefined
    }

    var feature = {
        chr: tokens[0],
        start: Number.parseInt(tokens[1]),
        end: Number.parseInt(tokens[2]),

        chr1: tokens[8],
        start1: Number.parseInt(tokens[9]),
        end1: Number.parseInt(tokens[10]),

        chr2: tokens[13],
        start2: Number.parseInt(tokens[14]),
        end2: Number.parseInt(tokens[15]),

        name: tokens[3],
        score: Number(tokens[4]),
        value: Number(tokens[5]),
        color: tokens[7] === '.' ? undefined : tokens[7] === "0" ? "rgb(0,0,0)" : tokens[7],

    };

    return feature
}

/**
 * Wrapper class to record a decoding error.
 */

class DecodeError {
    constructor(message) {
        this.message = message;
    }
}

const encodings$2 = new Map([
    ["%09", "\t"],
    ["%0A", "\n"],
    ["%0D", "\r"],
    ["%25", "%"],
    ["%3B", ";"],
    ["%3D", "="],
    ["%26", "&"],
    ["%2C", ","]
]);


// GFF3 attributes have specific percent encoding rules, the list below are required, all others are forbidden
/*
tab (%09)
newline (%0A)
carriage return (%0D)
% percent (%25)
control characters (%00 through %1F, %7F)
In addition, the following characters have reserved meanings in column 9 and must be escaped when used in other contexts:
; semicolon (%3B)
= equals (%3D)
& ampersand (%26)
, comma (%2C)
 */

function decodeGFFAttribute(str) {

    if (!str.includes("%")) {
        return str
    }
    let decoded = "";
    for (let i = 0; i < str.length; i++) {

        if (str.charCodeAt(i) === 37 && i < str.length - 2) {
            const key = str.substring(i, i + 3);
            if (encodings$2.has(key)) {
                decoded += encodings$2.get(key);
            } else {
                decoded += key;
            }
            i += 2;
        } else {
            decoded += str.charAt(i);
        }
    }
    return decoded

}

/**
 * Parse the attribute string, returning an array of key-value pairs.  An array is used rather than a map as attribute
 * keys are not required to be unique.
 *
 * @param attributeString
 * @param keyValueDelim
 * @returns {[]}
 */
function parseAttributeString(attributeString, keyValueDelim = "=") {
    // parse 'attributes' string (see column 9 docs in https://github.com/The-Sequence-Ontology/Specifications/blob/master/gff3.md)
    const isGff3 = ('=' === keyValueDelim);
    var attributes = [];
    for (let kv of attributeString.split(';')) {
        kv = kv.trim();
        const idx = kv.indexOf(keyValueDelim);
        if (idx > 0 && idx < kv.length - 1) {
            let key = decodeGFFAttribute(kv.substring(0, idx).trim());
            let value = decodeGFFAttribute(kv.substring(idx + 1).trim());
            if (!isGff3) {
                key = stripQuotes$1(key);
                value = stripQuotes$1(value);
            }
            attributes.push([key, value]);
        }
    }
    return attributes
}

function stripQuotes$1(value) {
    if (value.startsWith('"') && value.endsWith('"')) {
        value = value.substr(1, value.length - 2);
    }
    return value
}

/**
 * Some interpretations of the sequence ontology needed to assemble GFF transcripts.
 *
 */

const transcriptTypes = new Set(['transcript', 'primary_transcript', 'processed_transcript', 'mRNA', 'mrna',
    'lnc_RNA', 'miRNA', 'ncRNA', 'rRNA', 'scRNA', 'snRNA', 'snoRNA', 'tRNA']);
const cdsTypes = new Set(['CDS', 'cds']);
const codonTypes = new Set(['start_codon', 'stop_codon']);
const utrTypes = new Set(['5UTR', '3UTR', 'UTR', 'five_prime_UTR', 'three_prime_UTR', "3'-UTR", "5'-UTR"]);
const exonTypes = new Set(['exon', 'coding-exon']);

const transcriptPartTypes = new Set();
for (let cltn of [cdsTypes, codonTypes, utrTypes, exonTypes]) {
    for (let t of cltn) {
        transcriptPartTypes.add(t);
    }
}

function isExon(type) {
    return exonTypes.has(type)
}

function isIntron(type) {
    return type.includes("intron")
}

function isCoding(type) {
    return cdsTypes.has(type) || codonTypes.has(type)
}

function isUTR(type) {
    return utrTypes.has(type)
}

function isTranscript(type) {
    return transcriptTypes.has(type) || type.endsWith("RNA") || type.endsWith("transcript")
}

function isTranscriptPart(type) {
    return transcriptPartTypes.has(type) || type.endsWith("RNA") || isIntron(type)
}

const filterPopupProperties = new Set(["id", "parent", "name"]);

class GFFFeature {

    constructor(properties) {

        Object.assign(this, properties);

        if (properties.phase !== undefined && "." !== properties.phase) {
            this.readingFrame = (3 - parseInt(properties.phase)) % 3;
        }

    }


    popupData(genomicLocation) {

        const pd = this.geneObject ? this.geneObject.popupData() : [];

        if (this.geneObject) {
            pd.push('<hr/>');
        }

        if (this.name) {
            pd.push({name: 'Name', value: this.name});
        }

        pd.push({name: 'Type', value: this.type});
        pd.push({name: 'Source', value: this.source});
        if (this.score !== undefined) {
            pd.push({name: 'Score', value: this.score});
        }
        pd.push({name: 'Phase', value: this.phase});

        if (this.attributeString) {
            const atts = parseAttributeString(this.attributeString, this.delim);
            for (let [key, value] of atts) {
                if (value !== undefined && value.length > 0 && !filterPopupProperties.has(key.toLowerCase())) {
                    pd.push({name: key + ":", value: value});
                }
            }
        }
        pd.push({
            name: 'Location',
            value: `${this.chr}:${numberFormatter$1(this.start + 1)}-${numberFormatter$1(this.end)}`
        });
        return pd
    }

    getAttributeValue(attributeName) {
        if (this.hasOwnProperty(attributeName)) {
            return this[attributeName]
        } else {
            // TODO -- fetch from attribute string and cache
            if (!this._attributeCache) {
                this._attributeCache = new Map();
            }
            if (this._attributeCache.has(attributeName)) {
                return this._attributeCache.get(attributeName)
            } else {
                const atts = parseAttributeString(this.attributeString, this.delim);
                let v;
                for (let [key, value] of atts) {
                    if (key === attributeName) {
                        v = value;
                        break
                    }
                }
                this._attributeCache.set(attributeName, v);
                return v
            }
        }
    }
}

class GFFTranscript extends GFFFeature {

    constructor(feature) {
        super(feature);
        this.exons = [];
        this.parts = [];
    }

    addExon(feature) {

        this.exons.push(feature);

        // Expand feature --  for transcripts not explicitly represented in the file (gtf)
        this.start = Math.min(this.start, feature.start);
        this.end = Math.max(this.end, feature.end);
    }

    addPart(feature) {
        this.parts.push(feature);
    }

    assembleParts() {

        if (this.parts.length === 0) return

        this.parts.sort(function (a, b) {
            return a.start - b.start
        });

        // Create exons, if neccessary
        let lastStart = this.parts[0].start;
        let lastEnd = this.parts[0].end;
        for (let i = 1; i < this.parts.length; i++) {
            const part = this.parts[i];
            if (isIntron(part.type)) {
                continue
            }
            if (part.start <= lastEnd) {
                lastEnd = Math.max(lastEnd, part.end);
            } else {
                let exon = this.findExonContaining({start: lastStart, end: lastEnd});
                if (!exon) {
                    this.exons.push({start: lastStart, end: lastEnd, psuedo: true});
                }
                lastStart = part.start;
                lastEnd = part.end;
            }
        }
        let exon = this.findExonContaining({start: lastStart, end: lastEnd});
        if (!exon) {
            this.exons.push({start: lastStart, end: lastEnd, psuedo: true});
            this.start = Math.min(this.start, lastStart);
            this.end = Math.max(this.end, lastEnd);
        }


        for (let part of this.parts) {
            const type = part.type;
            if (isCoding(type)) {
                this.addCDS(part);
            } else if (isUTR(type)) {
                this.addUTR(part);
            }
        }
    }

    findExonContaining({start, end}) {
        for (let exon of this.exons) {
            if (exon.end >= end && exon.start <= start) {
                return exon
            }
        }
        return undefined
    }

    addCDS(cds) {

        let exon;
        const exons = this.exons;

        for (let e of exons) {
            if (e.start <= cds.start && e.end >= cds.end) {
                exon = e;
                break
            }
        }

        if (exon) {
            exon.cdStart = exon.cdStart ? Math.min(cds.start, exon.cdStart) : cds.start;
            exon.cdEnd = exon.cdEnd ? Math.max(cds.end, exon.cdEnd) : cds.end;
            exon.readingFrame = cds.readingFrame;
            // TODO -- merge attributes?
        } else {
            // cds.cdStart = cds.start
            // cds.cdEnd = cds.end
            // exons.push(cds)
            console.error("No exon found spanning " + cds.start + "-" + cds.end);
        }

        // Expand feature --  for transcripts not explicitly represented in the file (gtf files)
        // this.start = Math.min(this.start, cds.start);
        // this.end = Math.max(this.end, cds.end);

        this.cdStart = this.cdStart ? Math.min(cds.start, this.cdStart) : cds.start;
        this.cdEnd = this.cdEnd ? Math.max(cds.end, this.cdEnd) : cds.end;
    }

    addUTR(utr) {

        let exon;
        const exons = this.exons;

        // Find exon containing CDS
        for (let i = 0; i < exons.length; i++) {
            if (exons[i].start <= utr.start && exons[i].end >= utr.end) {
                exon = exons[i];
                break
            }
        }

        if (exon) {
            if (utr.start === exon.start && utr.end === exon.end) {
                exon.utr = true;
            } else {
                if (utr.end < exon.end) {
                    exon.cdStart = utr.end;
                }
                if (utr.start > exon.start) {
                    exon.cdEnd = utr.start;
                }
            }

        } else {
            // utr.utr = true
            // exons.push(utr)
            console.error("No exon found spanning " + cds.start + "-" + cds.end);
        }

        // Expand feature --  for transcripts not explicitly represented in the file
        // this.start = Math.min(this.start, utr.start);
        // this.end = Math.max(this.end, utr.end);

    }

    finish() {

        this.assembleParts();

        var cdStart = this.cdStart;
        var cdEnd = this.cdEnd;

        this.exons.sort(function (a, b) {
            return a.start - b.start
        });

        // Search for UTR exons that were not explicitly tagged
        if (cdStart) {
            this.exons.forEach(function (exon) {
                if (exon.end < cdStart || exon.start > cdEnd) exon.utr = true;
            });
        }
    }

    popupData(genomicLocation) {

        const pd = super.popupData(genomicLocation);

        // If clicked over an exon add its attributes
        for (let exon of this.exons) {
            if (exon.pseudo) continue  // An implicit exon
            if (genomicLocation >= exon.start && genomicLocation < exon.end && typeof exon.popupData === 'function') {
                pd.push('<hr/>');
                const exonData = exon.popupData(genomicLocation);
                for (let att of exonData) {
                    pd.push(att);
                }
            }
        }

        for (let part of this.parts) {
            if (genomicLocation >= part.start && genomicLocation < part.end && typeof part.popupData === 'function') {
                pd.push('<hr/>');
                const partData = part.popupData(genomicLocation);
                for (let att of partData) {
                    pd.push(att);
                }
            }
        }


        return pd
    }
}

class GFFHelper {

    static gffNameFields = new Set(["Name", "transcript_id", "gene_name", "gene", "gene_id", "alias", "locus", "name" ])
    constructor(options) {
        this.format = options.format;
        this.nameField = options.nameField;
        this.filterTypes = options.filterTypes === undefined ?
            new Set(['chromosome']) :
            new Set(options.filterTypes);
    }

    combineFeatures(features, genomicInterval) {

        let combinedFeatures;

        const filterTypes = this.filterTypes;
        features = features.filter(f => filterTypes === undefined || !filterTypes.has(f.type));

        if ("gff3" === this.format) {
            const tmp = this.combineFeaturesById(features);
            combinedFeatures = this.combineFeaturesByType(tmp);
        } else {
            combinedFeatures = this.combineFeaturesByType(features);
        }

        this.numberExons(combinedFeatures, genomicInterval);
        this.nameFeatures(combinedFeatures);
        return combinedFeatures
    }

    /**
     * Combine multiple non-transcript model features with the same ID on the same chromosome into a single feature.
     * Features that are part of the transcript model (e.g. exon, mRNA, etc) are combined later.
     *
     * @param features
     * @returns {[]}
     */
    combineFeaturesById(features) {

        const chrIdMap = new Map();
        const combinedFeatures = [];

        for (let f of features) {
            if (isTranscriptPart(f.type) || isTranscript(f.type) || !f.id) {
                combinedFeatures.push(f);
            } else {
                let idMap = chrIdMap.get(f.chr);
                if (!idMap) {
                    idMap = new Map();
                    chrIdMap.set(f.chr, idMap);
                }

                let featureArray = idMap.get(f.id);
                if (featureArray) {
                    featureArray.push(f);
                } else {
                    idMap.set(f.id, [f]);
                }
            }
        }

        for (let idMap of chrIdMap.values()) {
            for (let featureArray of idMap.values()) {
                if (featureArray.length > 1) {
                    // Use the first feature as prototypical (for column 9 attributes), and adjust start/end
                    // Parts are represented as "exons", as that is how they are presented visually
                    const cf = featureArray[0];
                    cf.exons = [];
                    for (let f of featureArray) {
                        cf.start = Math.min(cf.start, f.start);
                        cf.end = Math.max(cf.end, f.end);
                        cf.exons.push({
                            start: f.start,
                            end: f.end
                        });
                    }
                    combinedFeatures.push(cf);
                } else {
                    combinedFeatures.push(featureArray[0]);
                }
            }
        }

        return combinedFeatures
    }

    combineFeaturesByType(features) {

        // Build dictionary of genes
        const genes = features.filter(f => "gene" === f.type || f.type.endsWith("_gene"));
        const geneMap = Object.create(null);
        for (let g of genes) {
            geneMap[g.id] = g;
        }

        // 1. Build dictionary of transcripts
        const transcripts = Object.create(null);
        const combinedFeatures = [];
        const consumedFeatures = new Set();
        const filterTypes = this.filterTypes;

        features = features.filter(f => filterTypes === undefined || !filterTypes.has(f.type));

        for (let f of features) {
            if (isTranscript(f.type)) {
                const transcriptId = f.id; // getAttribute(f.attributeString, "transcript_id", /\s+/);
                if (undefined !== transcriptId) {
                    const gffTranscript = new GFFTranscript(f);
                    transcripts[transcriptId] = gffTranscript;
                    combinedFeatures.push(gffTranscript);
                    consumedFeatures.add(f);
                    const g = geneMap[f.parent];
                    if (g) {
                        gffTranscript.geneObject = g;
                        consumedFeatures.add(g);
                    }
                }
            }
        }

        // Add exons and transcript parts
        for (let f of features) {
            if (isTranscriptPart(f.type)) {
                const parents = getParents(f);
                if (parents) {
                    for (let id of parents) {

                        let transcript = transcripts[id];
                        if (!transcript && this.format === "gtf") {
                            // GTF does not require explicit a transcript or mRNA record, start one with this feature.
                            const psuedoTranscript = Object.assign({}, f);
                            psuedoTranscript.type = "transcript";
                            transcript = new GFFTranscript(psuedoTranscript);
                            transcripts[id] = transcript;
                            combinedFeatures.push(transcript);
                        }
                        if (transcript !== undefined) {

                            if (isExon(f.type)) {
                                if (parents.length > 1) {
                                    // Multiple parents, this is unusual.  Make a copy as exon can be modified
                                    // differently by CDS, etc, for each parent
                                    const e2 = new GFFFeature(f);
                                    transcript.addExon(e2);
                                } else {
                                    transcript.addExon(f);
                                }
                            } else {
                                transcript.addPart(f);
                            }
                            consumedFeatures.add(f);
                        }
                    }
                }
            }
        }

        // Finish transcripts
        combinedFeatures.forEach(function (f) {
            if (typeof f.finish === "function") {
                f.finish();
            }
        });

        // Add other features
        const others = features.filter(f => !consumedFeatures.has(f));
        for (let f of others) {
            combinedFeatures.push(f);
        }

        return combinedFeatures

        function getParents(f) {
            if (f.parent && f.parent.trim() !== "") {
                return f.parent.trim().split(",")
            } else {
                return null
            }
        }
    }

    numberExons(features, genomicInterval) {

        for (let f of features) {
            if (f.exons &&
                (!genomicInterval ||
                    (f.end <= genomicInterval.end && f.start > genomicInterval.start))) {
                for (let i = 0; i < f.exons.length; i++) {
                    const exon = f.exons[i];
                    exon.number = f.strand === "-" ? f.exons.length - i : i + 1;
                }
            }
        }
    }

    nameFeatures(features) {
        // Find name (label) property
        for (let f of features) {
            if(typeof f.getAttributeValue === 'function') {
                if (this.nameField) {
                    f.name = f.getAttributeValue(this.nameField);
                } else {
                    for (let nameField of GFFHelper.gffNameFields) {
                        const v = f.getAttributeValue(nameField);
                        if (v) {
                            f.name = v;
                            break
                        }
                    }
                }
            }
        }
    }
}

/**
 * Decode the UCSC bed format.  Only the first 3 columns (chr, start, end) are required.   The remaining columns
 * must follow standard bed order, but we will tolerate deviations after column 3.
 *
 * @param tokens
 * @param ignore
 * @returns decoded feature, or null if this is not a valid record
 */
function decodeBed(tokens, header, maxColumnCount = Number.MAX_SAFE_INTEGER) {

    if (tokens.length < 3) return undefined

    const gffTags = header && header.gffTags;

    const chr = tokens[0];
    const start = parseInt(tokens[1]);
    const end = tokens.length > 2 ? parseInt(tokens[2]) : start + 1;
    if (isNaN(start) || isNaN(end)) {
        return new DecodeError(`Unparsable bed record.`)
    }
    const feature = new UCSCBedFeature({chr: chr, start: start, end: end, score: 1000});

    let columnCount = 3;
    try {
        if (tokens.length > 3 && columnCount++ < maxColumnCount) {

            // Potentially parse name field as GFF column 9 style streng.
            if (tokens[3].indexOf(';') > 0 && tokens[3].indexOf('=') > 0) {
                const attributeKVs = parseAttributeString(tokens[3], '=');
                feature.attributes = {};
                for (let kv of attributeKVs) {
                    feature.attributes[kv[0]] = kv[1];
                    if(gffTags) {
                        if (header.nameField != undefined && kv[0] === header.nameField) {
                            feature.name = kv[1];
                        } else if (!feature.name && GFFHelper.gffNameFields.has(kv[0])) {
                            feature.name = kv[1];
                        }
                    }
                }
            }
            if (!feature.name && !gffTags) {
                feature.name = tokens[3] === '.' ? '' : tokens[3];
            }
        }

        if (tokens.length > 4 && columnCount++ < maxColumnCount) {
            feature.score = tokens[4] === '.' ? 0 : Number(tokens[4]);
            if (isNaN(feature.score)) {
                return feature
            }
        }

        if (tokens.length > 5 && columnCount++ < maxColumnCount) {
            feature.strand = tokens[5];
            if (!(feature.strand === '.' || feature.strand === '+' || feature.strand === '-')) {
                return feature
            }
        }

        if (tokens.length > 6 && columnCount++ < maxColumnCount) {
            feature.cdStart = parseInt(tokens[6]);
            if (isNaN(feature.cdStart)) {
                return feature
            }
        }

        if (tokens.length > 7 && columnCount++ < maxColumnCount) {
            feature.cdEnd = parseInt(tokens[7]);
            if (isNaN(feature.cdEnd)) {
                return feature
            }
        }

        if (tokens.length > 8 && columnCount++ < maxColumnCount) {
            if (tokens[8] !== "." && tokens[8] !== "0")
                feature.color = IGVColor.createColorString(tokens[8]);
        }

        if (tokens.length > 11 && columnCount++ < maxColumnCount) {
            const exonCount = parseInt(tokens[9]);
            // Some basic validation
            if (exonCount > 1000) {
                // unlikely
                return feature
            }

            const exonSizes = tokens[10].replace(/,$/, '').split(',');
            const exonStarts = tokens[11].replace(/,$/, '').split(',');
            if (!(exonSizes.length === exonStarts.length && exonCount === exonSizes.length)) {
                return feature
            }

            const exons = [];
            for (let i = 0; i < exonCount; i++) {
                const eStart = start + parseInt(exonStarts[i]);
                const eEnd = eStart + parseInt(exonSizes[i]);
                exons.push({start: eStart, end: eEnd});
            }
            if (exons.length > 0) {
                findUTRs$1(exons, feature.cdStart, feature.cdEnd);
                feature.exons = exons;
            }
        }

        // Optional extra columns
        if (header) {
            let thicknessColumn = header.thicknessColumn;
            let colorColumn = header.colorColumn;
            if (colorColumn && colorColumn < tokens.length) {
                feature.color = IGVColor.createColorString(tokens[colorColumn]);
            }
            if (thicknessColumn && thicknessColumn < tokens.length) {
                feature.thickness = tokens[thicknessColumn];
            }
        }
    } catch
        (e) {

    }

    return feature
}


function decodeGappedPeak(tokens, header) {
    const feature = decodeBed(tokens, header);
    if (feature && tokens.length > 14) {
        feature.signal = Number(tokens[12]);
        feature.pValue = Number(tokens[13]);
        feature.qValue  = Number(tokens[14]);
    }
    return feature
}

/**
 * Decode a bedMethyl file.
 * Reference: https://www.encodeproject.org/data-standards/wgbs/
 * @param tokens
 * @param header
 */
function decodeBedmethyl(tokens, header) {

    // Bedmethyl is a 9+9 format
    const feature = decodeBed(tokens, header, 9);
    if (feature) {
        const extraColumnHeadings = ["Coverage", "% Showing Methylation", "N-mod", "N-canonical", "N-other mod",
            "N-delete", "N-fail", "N-dff", "N-nocall"];
        for (let i = 9; i < tokens.length; i++) {
            const heading = extraColumnHeadings[i - 9];
            feature[heading] = tokens[i];
        }
    }


    return feature
}


/**
 * Decode a UCSC repeat masker record.
 */
function decodeRepeatMasker(tokens, header) {

    /**
     * Columns, from UCSC documentation
     *
     * 0  bin    585    smallint(5) unsigned    Indexing field to speed chromosome range queries.
     * 1  swScore    1504    int(10) unsigned    Smith Waterman alignment score
     * 2  milliDiv    13    int(10) unsigned    Base mismatches in parts per thousand
     * 3  milliDel    4    int(10) unsigned    Bases deleted in parts per thousand
     * 4  milliIns    13    int(10) unsigned    Bases inserted in parts per thousand
     * 5  genoName    chr1    varchar(255)    Genomic sequence name
     * 6  genoStart    10000    int(10) unsigned    Start in genomic sequence
     * 7  genoEnd    10468    int(10) unsigned    End in genomic sequence
     * 8  genoLeft    -249240153    int(11)    -#bases after match in genomic sequence
     * 9  strand    +    char(1)    Relative orientation + or -
     * 10 repName    (CCCTAA)n    varchar(255)    Name of repeat
     * 11 repClass    Simple_repeat    varchar(255)    Class of repeat
     * 12 repFamily    Simple_repeat    varchar(255)    Family of repeat
     * 13 repStart    1    int(11)    Start (if strand is +) or -#bases after match (if strand is -) in repeat sequence
     * 14 repEnd    463    int(11)    End in repeat sequence
     * 15 repLeft    0    int(11)    -#bases after match (if strand is +) or start (if strand is -) in repeat sequence
     * 16 id    1    char(1)    First digit of id field in RepeatMasker .out file. Best ignored.
     */
    if (tokens.length <= 15) return undefined

    const feature = {
        swScore: Number.parseInt(tokens[1]),
        milliDiv: Number.parseInt(tokens[2]),
        milliDel: Number.parseInt(tokens[3]),
        milliIns: Number.parseInt(tokens[4]),
        chr: tokens[5],
        start: Number.parseInt(tokens[6]),
        end: Number.parseInt(tokens[7]),
        //genoLeft: tokens[8],
        strand: tokens[9],
        repName: tokens[10],
        repClass: tokens[11],
        repFamily: tokens[12],
        repStart: Number.parseInt(tokens[13]),
        repEnd: Number.parseInt(tokens[14]),
        repLeft: Number.parseInt(tokens[15])
    };

    return feature

}

/**
 * Decode a UCSC "genePred" record.
 *
 * @param tokens
 * @param ignore
 * @returns {*}
 */
function decodeGenePred(tokens, header) {

    var shift = header.shift === undefined ? 0 : 1;

    if (tokens.length <= 9 + shift) return undefined

    const cdStart = parseInt(tokens[5 + shift]);
    const cdEnd = parseInt(tokens[6 + shift]);
    var feature = {
        name: tokens[0 + shift],
        chr: tokens[1 + shift],
        strand: tokens[2 + shift],
        start: parseInt(tokens[3 + shift]),
        end: parseInt(tokens[4 + shift]),
        cdStart: cdStart,
        cdEnd: cdEnd,
        id: tokens[0 + shift]
    };
    const exons = decodeExons(parseInt(tokens[7 + shift]), tokens[8 + shift], tokens[9 + shift]);
    findUTRs$1(exons, cdStart, cdEnd);

    feature.exons = exons;

    return feature

}

/**
 * Decode a UCSC "genePredExt" record.  refGene files are in this format.
 *
 * @param tokens
 * @param ignore
 * @returns {*}
 */
function decodeGenePredExt(tokens, header) {

    var shift = header.shift === undefined ? 0 : 1;

    if (tokens.length <= 11 + shift) return undefined

    const cdStart = parseInt(tokens[5 + shift]);
    const cdEnd = parseInt(tokens[6 + shift]);
    const feature = {
        name: tokens[11 + shift],
        chr: tokens[1 + shift],
        strand: tokens[2 + shift],
        start: parseInt(tokens[3 + shift]),
        end: parseInt(tokens[4 + shift]),
        cdStart: cdStart,
        cdEnd: cdEnd,
        id: tokens[0 + shift]
    };

    const exons = decodeExons(
        parseInt(tokens[7 + shift]),
        tokens[8 + shift],
        tokens[9 + shift],
        tokens[14 + shift]);
    findUTRs$1(exons, cdStart, cdEnd);

    feature.exons = exons;

    return feature
}

/**
 * Decode a UCSC "refFlat" record
 * @param tokens
 * @param ignore
 * @returns {*}
 */
function decodeReflat(tokens, header) {

    var shift = header.shift === undefined ? 0 : 1;

    if (tokens.length <= 10 + shift) return undefined

    const cdStart = parseInt(tokens[6 + shift]);
    const cdEnd = parseInt(tokens[7 + shift]);
    var feature = {
        name: tokens[0 + shift],
        id: tokens[1 + shift],
        chr: tokens[2 + shift],
        strand: tokens[3 + shift],
        start: parseInt(tokens[4 + shift]),
        end: parseInt(tokens[5 + shift]),
        cdStart: cdStart,
        cdEnd: cdEnd
    };

    const exons = decodeExons(parseInt(tokens[8 + shift]), tokens[9 + shift], tokens[10 + shift]);
    findUTRs$1(exons, cdStart, cdEnd);
    feature.exons = exons;

    return feature
}

/**
 * Decode a UCS PSL record *
 * @param tokens
 * @param header
 * @returns {DecodeError|UCSCBedFeature|undefined}
 */
function decodePSL(tokens, header) {

    /*
    * 0 matches - Number of bases that match that aren't repeats
    * 1 misMatches - Number of bases that don't match
    * 2 repMatches - Number of bases that match but are part of repeats
    * 3 nCount - Number of "N" bases
    * 4 qNumInsert - Number of inserts in query
    * 5 qBaseInsert - Number of bases inserted in query
    * 6 tNumInsert - Number of inserts in target
    * 7 tBaseInsert - Number of bases inserted in target
    * 8 strand - "+" or "-" for query strand. For translated alignments, second "+"or "-" is for target genomic strand.
    * 9 qName - Query sequence name
    * 10 qSize - Query sequence size.
    * 11 qStart - Alignment start position in query
    * 12 qEnd - Alignment end position in query
    * 13 tName - Target sequence name
    * 14 tSize - Target sequence size
    * 15 tStart - Alignment start position in target
    * 16 tEnd - Alignment end position in target
    * 17 blockCount - Number of blocks in the alignment (a block contains no gaps)
    * 18 blockSizes - Comma-separated list of sizes of each block. If the query is a protein and the target the genome, blockSizes are in amino acids. See below for more information on protein query PSLs.
    * 19 qStarts - Comma-separated list of starting positions of each block in query
    * 20 tStarts - Comma-separated list of starting positions of each block in target
     */


    if (tokens.length < 21) return undefined

    const chr = tokens[13];
    const start = parseInt(tokens[15]);
    const end = parseInt(tokens[16]);
    const strand = tokens[8].charAt(0);
    const exonCount = parseInt(tokens[17]);
    const exons = [];
    const exonStarts = tokens[20].replace(/,$/, '').split(',');
    const exonSizes = tokens[18].replace(/,$/, '').split(',');

    for (let i = 0; i < exonCount; i++) {
        const start = parseInt(exonStarts[i]);
        const end = start + parseInt(exonSizes[i]);
        exons.push({start: start, end: end});
    }

    return new PSLFeature({chr, start, end, strand, exons, tokens})
}


function decodeExons(exonCount, startsString, endsString, frameOffsetsString) {

    const exonStarts = startsString.replace(/,$/, '').split(',');
    const exonEnds = endsString.replace(/,$/, '').split(',');
    const frameOffsets = frameOffsetsString ? frameOffsetsString.replace(/,$/, '').split(',') : undefined;
    const exons = [];
    for (let i = 0; i < exonCount; i++) {
        const start = parseInt(exonStarts[i]);
        const end = parseInt(exonEnds[i]);
        const exon = {start, end};
        if (frameOffsets) {
            const fo = parseInt(frameOffsets[i]);
            if (fo != -1) exon.readingFrame = fo;
        }
        exons.push(exon);
    }
    return exons

}

function findUTRs$1(exons, cdStart, cdEnd) {

    for (let exon of exons) {
        const end = exon.end;
        const start = exon.start;
        if (end < cdStart || start > cdEnd) {
            exon.utr = true;
        } else {
            if (cdStart >= start && cdStart <= end) {
                exon.cdStart = cdStart;
            }
            if (cdEnd >= start && cdEnd <= end) {
                exon.cdEnd = cdEnd;
            }
        }
    }

}

function decodePeak(tokens, header) {

    var tokenCount, chr, start, end, strand, name, score, qValue, signal, pValue;

    tokenCount = tokens.length;
    if (tokenCount < 9) {
        return undefined
    }

    chr = tokens[0];
    start = parseInt(tokens[1]);
    end = parseInt(tokens[2]);
    name = tokens[3];
    score = Number(tokens[4]);
    strand = tokens[5].trim();
    signal = Number(tokens[6]);
    pValue = Number(tokens[7]);
    qValue = Number(tokens[8]);

    if (score === 0) score = signal;

    return {
        chr: chr, start: start, end: end, name: name, score: score, strand: strand, signal: signal,
        pValue: pValue, qValue: qValue
    }
}

function decodeNarrowPeak(tokens, header) {

    const feature = decodePeak(tokens);
    if(tokens.length > 9) {
        feature.peak = Number(tokens[9]);
    }
    return feature
}

function decodeBedGraph(tokens, header) {

    var chr, start, end, value;

    if (tokens.length <= 3) return undefined

    chr = tokens[0];
    start = parseInt(tokens[1]);
    end = parseInt(tokens[2]);
    value = Number(tokens[3]);
    const feature = {chr: chr, start: start, end: end, value: value};

    // Optional extra columns
    if (header) {
        let colorColumn = header.colorColumn;
        if (colorColumn && colorColumn < tokens.length) {
            feature.color = IGVColor.createColorString(tokens[colorColumn]);
        }
    }

    return feature
}

function decodeWig(tokens, header) {

    const wig = header.wig;

    if (wig && wig.format === "fixedStep") {
        const ss = (wig.index * wig.step) + wig.start;
        const ee = ss + wig.span;
        const value = Number(tokens[0]);
        ++(wig.index);
        return isNaN(value) ? null : {chr: wig.chrom, start: ss, end: ee, value: value}
    } else if (wig && wig.format === "variableStep") {

        if (tokens.length < 2) return null
        const ss = parseInt(tokens[0], 10) - 1;
        const ee = ss + wig.span;
        const value = Number(tokens[1]);
        return isNaN(value) ? null : {chr: wig.chrom, start: ss, end: ee, value: value}

    } else {
        return decodeBedGraph(tokens)
    }
}

/**
 * Decode a UCSC SNP record.
 * Reference: https://genome.ucsc.edu/FAQ/FAQformat.html#format1
 * @param tokens
 * @param header
 * @returns {undefined|{chr: *, start: number, end: number, name: *, score: number}}
 */
function decodeSNP(tokens, header) {

    if (tokens.length < 6) return undefined

    const autoSql = [
        'bin',
        'chr',
        'start',
        'end',
        'name',
        'score',
        'strand',
        'refNCBI',
        'refUCSC',
        'observed',
        'molType',
        'class',
        'valid',
        'avHet',
        'avHetSE',
        'func',
        'locType',
        'weight',
        'exceptions',
        'submitterCount',
        'submitters',
        'alleleFreqCount',
        'alleles',
        'alleleNs',
        'alleleFreqs',
        'bitfields'
    ];


    const feature = {
        chr: tokens[1],
        start: Number.parseInt(tokens[2]),
        end: Number.parseInt(tokens[3]),
        name: tokens[4],
        score: Number.parseInt(tokens[5])
    };

    const n = Math.min(tokens.length, autoSql.length);
    for (let i = 6; i < n; i++) {
        feature[autoSql[i]] = tokens[i];
    }
    return feature

}


class UCSCBedFeature {

    constructor(properties) {
        Object.assign(this, properties);
    }

    getAttributeValue(attributeName) {
        if (this.hasOwnProperty(attributeName)) {
            return this[attributeName]
        } else if (this.attributes) {
            return this.attributes[attributeName]
        }
    }
}

/*
* 0 matches - Number of bases that match that aren't repeats
* 1 misMatches - Number of bases that don't match
* 2 repMatches - Number of bases that match but are part of repeats
* 3 nCount - Number of "N" bases
* 4 qNumInsert - Number of inserts in query
* 5 qBaseInsert - Number of bases inserted in query
* 6 tNumInsert - Number of inserts in target
* 7 tBaseInsert - Number of bases inserted in target
* 8 strand - "+" or "-" for query strand. For translated alignments, second "+"or "-" is for target genomic strand.
* 9 qName - Query sequence name
* 10 qSize - Query sequence size.
* 11 qStart - Alignment start position in query
* 12 qEnd - Alignment end position in query
* 13 tName - Target sequence name
* 14 tSize - Target sequence size
* 15 tStart - Alignment start position in target
* 16 tEnd - Alignment end position in target
* 17 blockCount - Number of blocks in the alignment (a block contains no gaps)
* 18 blockSizes - Comma-separated list of sizes of each block. If the query is a protein and the target the genome, blockSizes are in amino acids. See below for more information on protein query PSLs.
* 19 qStarts - Comma-separated list of starting positions of each block in query
* 20 tStarts - Comma-separated list of starting positions of each block in target
 */

class PSLFeature {


    constructor(properties) {
        Object.assign(this, properties);
    }

    get score() {
        const tokens = this.tokens;
        const match = parseInt(tokens[0]);
        const repMatch = parseInt(tokens[2]);
        const misMatch = parseInt(tokens[1]);
        const qGapCount = parseInt(tokens[4]);
        const tGapCount = parseInt(tokens[6]);
        const qSize = parseInt(tokens[10]);
        return Math.floor((1000 * (match + repMatch - misMatch - qGapCount - tGapCount)) / qSize)
    }

    get matches() {
        return this.tokens[0]
    }

    get misMatches() {
        return this.tokens[1]
    }

    get repMatches() {
        return this.tokens[2]
    }

    get nCount() {
        return this.tokens[3]
    }

    get qNumInsert() {
        return this.tokens[4]
    }

    get qBaseInsert() {
        return this.tokens[5]
    }

    get tNumInsert() {
        return this.tokens[6]
    }

    get tBaseInsert() {
        return this.tokens[7]

    }

    popupData() {
        return [
            {name: 'chr', value: this.chr},
            {name: 'start', value: this.start + 1},
            {name: 'end', value: this.end},
            {name: 'strand', value: this.strand},
            {name: 'score', value: this.score},
            {name: 'match', value: this.matches},
            {name: "mis-match", value: this.misMatches},
            {name: "rep. match", value: this.repMatches},
            {name: "N's", value: this.nCount},
            {name: 'Q gap count', value: this.qNumInsert},
            {name: 'Q gap bases', value: this.qBaseInsert},
            {name: 'T gap count', value: this.tNumInsert},
            {name: 'T gap bases', value: this.tBaseInsert},
        ]
    }

}

function decode(tokens, header) {

    const format = header.format;
    if (tokens.length < 9) {
        return undefined      // Not a valid gff record
    }

    const delim = ('gff3' === format) ? '=' : ' ';
    return new GFFFeature({
        source: decodeGFFAttribute(tokens[1]),
        type: tokens[2],
        chr: tokens[0],
        start: parseInt(tokens[3]) - 1,
        end: parseInt(tokens[4]),
        score: "." === tokens[5] ? undefined : Number(tokens[5]),
        strand: tokens[6],
        phase: "." === tokens[7] ? "." : parseInt(tokens[7]),
        attributeString: tokens[8],
        delim: delim
    })
}


/**
 * Decode a single gff record (1 line in file).  Aggregations such as gene models are constructed at a higher level.
 *      ctg123 . mRNA            1050  9000  .  +  .  ID=mRNA00001;Parent=gene00001
 * @param tokens
 * @param ignore
 * @returns {*}
 */
function decodeGFF3(tokens, header) {

    const feature = decode(tokens, header);

    if (!feature) {
        return
    }

    const attributes =  parseAttributeString(feature.attributeString, feature.delim);

    // Search for color value as case insenstivie key
    for (let [key, value] of attributes) {
        const keyLower = key.toLowerCase();
        if ("color" === keyLower || "colour" === keyLower) {
            feature.color = IGVColor.createColorString(value);
        } else if (key === "ID") {
            feature.id = value;
        } else if (key === "Parent") {
            feature.parent = value;
        }
    }
    return feature
}

/**
 * GTF format example:
 NC_000008.11    BestRefSeq    gene    127735434    127742951    .    +    .    gene_id "MYC"; transcript_id ""; db_xref "GeneID:4609"; db_xref "HGNC:HGNC:7553"; db_xref "MIM:190080"; description "MYC proto-oncogene, bHLH transcription factor"; gbkey "Gene"; gene "MYC"; gene_biotype "protein_coding"; gene_synonym "bHLHe39"; gene_synonym "c-Myc"; gene_synonym "MRTL"; gene_synonym "MYCC";
 NC_000008.11    BestRefSeq    transcript    127735434    127742951    .    +    .    gene_id "MYC"; transcript_id "NM_001354870.1"; db_xref "GeneID:4609"; gbkey "mRNA"; gene "MYC"; product "MYC proto-oncogene, bHLH transcription factor, transcript variant 2"; transcript_biotype "mRNA";
 NC_000008.11    BestRefSeq    exon    127735434    127736623    .    +    .    gene_id "MYC"; transcript_id "NM_001354870.1"; db_xref "GeneID:4609"; gene "MYC"; product "MYC proto-oncogene, bHLH transcription factor, transcript variant 2"; transcript_biotype "mRNA"; exon_number "1";
 *
 * @param tokens
 * @param ignore
 * @returns {*}
 */
function decodeGTF(tokens, header) {

    const feature = decode(tokens, header);

    if (!feature) {
        return
    }

    const attributes = parseAttributeString(feature.attributeString, feature.delim);

    // GTF files specify neither ID nor parent fields, but they can be inferred from common conventions
    let idField;
    let parentField;
    switch (feature.type) {
        case "gene":
            idField = "gene_id";
            break
        case "transcript":
            idField = "transcript_id";
            parentField = "gene_id";
            break
        default:
            parentField = "transcript_id";
    }

    for (let [key, value] of attributes) {
        const keyLower = key.toLowerCase();
        if ("color" === keyLower || "colour" === keyLower) {
            feature.color = IGVColor.createColorString(value);
        } else if (key === idField) {
            feature.id = value;
        } else if (key === parentField) {
            feature.parent = value;
        }
    }
    return feature

}

function decodeFusionJuncSpan(tokens, header) {

    /*
     Format:

     0       #scaffold
     1       fusion_break_name
     2       break_left
     3       break_right
     4       num_junction_reads
     5       num_spanning_frags
     6       spanning_frag_coords

     0       B3GNT1--NPSR1
     1       B3GNT1--NPSR1|2203-10182
     2       2203
     3       10182
     4       189
     5       1138
     6       1860-13757,1798-13819,1391-18127,1443-17174,...

     */


    if (tokens.length < 7) return undefined

    var chr = tokens[0];
    var fusion_name = tokens[1];
    var junction_left = parseInt(tokens[2]);
    var junction_right = parseInt(tokens[3]);
    var num_junction_reads = parseInt(tokens[4]);
    var num_spanning_frags = parseInt(tokens[5]);

    var spanning_frag_coords_text = tokens[6];

    var feature = {
        chr: chr,
        name: fusion_name,
        junction_left: junction_left,
        junction_right: junction_right,
        num_junction_reads: num_junction_reads,
        num_spanning_frags: num_spanning_frags,
        spanning_frag_coords: [],

        start: -1,
        end: -1
    }; // set start and end later based on min/max of span coords

    var min_coord = junction_left;
    var max_coord = junction_right;

    if (num_spanning_frags > 0) {

        var coord_pairs = spanning_frag_coords_text.split(',');

        for (var i = 0; i < coord_pairs.length; i++) {
            var split_coords = coord_pairs[i].split('-');

            var span_left = split_coords[0];
            var span_right = split_coords[1];

            if (span_left < min_coord) {
                min_coord = span_left;
            }
            if (span_right > max_coord) {
                max_coord = span_right;
            }
            feature.spanning_frag_coords.push({left: span_left, right: span_right});

        }
    }

    feature.start = min_coord;
    feature.end = max_coord;


    return feature

}

function decodeGtexGWAS(tokens, header) {
    //chrom	chromStart	chromEnd	Strongest SNP-risk allele	Disease/Phenotype	P-value	Odds ratio or beta	PUBMEDID
    //1	1247493	1247494	rs12103-A	Inflammatory bowel disease	8.00E-13	1.1	23128233

    const tokenCount = tokens.length;
    if (tokenCount < 7) {
        return null
    }
    const feature = {
        chr: tokens[0],
        start: parseInt(tokens[1]) - 1,
        end: parseInt(tokens[2]),
        'Strongest SNP-risk allele': tokens[3],
        'Disease/Phenotype': tokens[4],
        'P-value': tokens[5],
        'Odds ratio or beta': tokens[6],
    };
    if (tokens.length > 6) {
        feature['PUBMEDID'] = `<a target = "blank" href = "https://www.ncbi.nlm.nih.gov/pubmed/${tokens[7]}">${tokens[7]}</a>`;
    }
    return feature
}

/**
 * Decode a custom columnar format.  Required columns are 'chr' and 'start'
 *
 * @param tokens
 * @param ignore
 * @returns decoded feature, or null if this is not a valid record
 */
function decodeCustom(tokens, header) {

    const format = header.customFormat;

    if (tokens.length < format.fields.length) return undefined

    const coords = format.coords || 0;

    const chr = tokens[format.chr];
    const start = parseInt(tokens[format.start]) - coords;
    const end = format.end !== undefined ? parseInt(tokens[format.end]) : start + 1;

    const feature = {chr: chr, start: start, end: end};

    if (format.fields) {
        format.fields.forEach(function (field, index) {

            if (index !== format.chr &&
                index !== format.start &&
                index !== format.end) {

                feature[field] = tokens[index];
            }
        });
    }

    return feature

}

/**
 * Decode a gcnv record, a bed style format encoding copy number variation
 *
 * @param tokens
 * @param header
 */

function decodeGcnv(tokens, header) {

    const columnNames = header.columnNames;
    if (!columnNames) {
        throw Error("Sample names are not defined.   Missing column headers?")
    }
    const sampleCount = columnNames.length - 3;

    const chr = tokens[0];
    const start = parseInt(tokens[1]);
    const end = parseInt(tokens[2]);
    const values = tokens.slice(3).map(Number);

    if (values.length == sampleCount) {
        return {
            chr: chr,
            start: start,
            end: end,
            values: values,
        }
    } else {
        // TODO Throw error?
        console.warn(`${chr}:${start}-${end} row contains ${values.length} sample columns instead of the expected ${sampleCount} columns. Skipping...`);
        return undefined
    }
}

function decodeShoebox(tokens, header, maxColumnCount = Number.MAX_SAFE_INTEGER) {

    if (tokens.length < 4) return undefined

    const chr = tokens[0];
    const start = parseInt(tokens[1]);
    const end = parseInt(tokens[2]);
    if (isNaN(start) || isNaN(end)) {
        return new DecodeError(`Unparsable bed record.`)
    }
    const feature = {chr, start, end};

    const values = [];
    for(let i = 3; i< tokens.length; i++) {
        values.push(Number.parseFloat(tokens[i]));
    }
    feature.values = values;


    return feature
}

/**
 * Utilities for creating and registering custom file formats for generic delimited files.  A format definition consists
 * of an ordered list of fields, and optional delimiter specified.
 *
 *
 */


/**
 * Register a new custom file format.
 * @param name
 * @param fields
 */
function registerFileFormats(name, fields) {
    FileFormats[name] = {fields: fields};
}

/**
 * Return a custom format object with the given name.
 * @param name
 * @returns {*}
 */
function getFormat(name) {

    if (FileFormats && FileFormats[name]) {
        return expandFormat(FileFormats[name])
    } else {
        return undefined
    }

    function expandFormat(format) {

        const fields = format.fields;
        const keys = ['chr', 'start', 'end'];

        for (let i = 0; i < fields.length; i++) {
            for (let key of keys) {
                if (key === fields[i]) {
                    format[key] = i;
                }
            }
        }

        return format
    }
}



/**
 * Table of custom formats, with several pre-defined.
 *
 * @type {{wgrna: {fields: string[]}, cpgislandext: {fields: string[]}, clinVarMain: {fields: string[]}, gwascatalog: {fields: string[]}}}
 */
const FileFormats = {

    gwascatalog: {
        fields: [
            'bin',
            'chr',
            'start',
            'end',
            'name',
            'pubMedID',
            'author',
            'pubDate',
            'journal',
            'title',
            'trait',
            'initSample',
            'replSample',
            'region',
            'genes',
            'riskAllele',
            'riskAlFreq',
            'pValue',
            'pValueDesc',
            'orOrBeta',
            'ci95',
            'platform',
            'cnv'
        ]
    },

    wgrna: {
        fields:
            [
                'bin',
                'chr',
                'start',
                'end',
                'name',
                'score',
                'strand',
                'thickStart',
                'thickEnd',
                'type'
            ]
    },

    cpgislandext: {
        fields:
            [
                'bin',
                'chr',
                'start',
                'end',
                'name',
                'length',
                'cpgNum',
                'gcNum',
                'perCpg',
                'perGc',
                'obsExp'
            ]
    },

    clinVarMain: {
        fields: [
            'chr1',
            'start',
            'end',
            'name',
            'score',
            'strand',
            'thickStart',
            'thickEnd',
            'reserved',
            'blockCount',  // Number of blocks
            'blockSizes',  // Comma separated list of block sizes
            'chromStarts', // Start positions relative to chromStart
            'origName',    // NM_198053.2(CD247):c.462C>T (p.Asp154=)	ClinVar Variation Report
            'clinSign',    // Likely benign	Clinical significance
            'reviewStatus', // 	based on: criteria provided,single submitter	Review Status
            'type',         // single nucleotide variant	Type of Variant
            'geneId', 	    // CD247	Gene Symbol
            'snpId',       //	181656780	dbSNP ID
            'nsvId',       //		dbVar ID
            'rcvAcc',      //	RCV000642347	ClinVar Allele Submission
            'testedInGtr', //	N	Genetic Testing Registry
            'phenotypeList', //	Immunodeficiency due to defect in cd3-zeta	Phenotypes
            'phenotype', //	MedGen:C1857798, OMIM:610163	Phenotype identifiers
            'origin', //	germline	Data origin
            'assembly', //	GRCh37	Genome assembly
            'cytogenetic', //	1q24.2	Cytogenetic status
            'hgvsCod', //	NM_198053.2:c.462C>T	Nucleotide HGVS
            'hgvsProt', //	NP_932170.1:p.Asp154=	Protein HGVS
            'numSubmit', //	1	Number of submitters
            'lastEval', //	Dec 19,2017	Last evaluation
            'guidelines', //		Guidelines
            'otherIds'
        ]
    }
};

/**
 *  Parser for column style (tab delimited, etc) text file formats (bed, gff, vcf, etc).
 *
 *
 */


/**
 * Return a parser for the given file format.
 */
class FeatureParser {

    constructor(config) {

        this.config = config;
        this.header = {};
        if (config.nameField) {
            this.header.nameField = config.nameField;
        }

        this.skipRows = 0;   // The number of fixed header rows to skip.  Override for specific types as needed

        if (config.decode) {
            this.decode = config.decode;
            this.delimiter = config.delimiter || "\t";
        } else if (config.format) {
            this.header.format = config.format.toLowerCase();
            this.setDecoder(this.header.format);
        }

        if (!this.delimiter) {
            this.delimiter = "\t";
        }
    }

    /**
     * Parse metadata from the file.   A variety of conventions are in use to supply metadata about file contents
     * through header lines (e.g. 'track') and # directives. This method unifies metadata as properties of a
     * 'header' object.
     *
     * @param data
     * @returns {{}}
     */
    async parseHeader(dataWrapper) {

        let header = this.header;
        let columnNames;
        let line;
        while ((line = await dataWrapper.nextLine()) !== undefined) {
            if (line.startsWith("track") || line.startsWith("#track")) {
                let h = parseTrackLine$1(line);
                Object.assign(header, h);
            } else if (line.startsWith("browser")) ; else if (line.startsWith("#columns")) {
                let h = parseColumnsDirective$1(line);
                Object.assign(header, h);
            } else if (line.startsWith("##gff-version 3")) {
                header.format = "gff3";
            } else if (line.startsWith("#gffTags")) {
                header.gffTags = true;
            } else if (line.startsWith("fixedStep") || line.startsWith("variableStep")) {
                // Wig directives -- we are in the data section
                break
            } else if (line.startsWith("#")) {
                const tokens = line.split(this.delimiter || "\t");
                if (tokens.length > 1) {
                    columnNames = tokens;   // Possible column names
                }
            } else {
                // All directives that could change the format, and thus decoder, should have been read by now.
                // Set the decoder, unless it is explicitly set in the track configuration (not common)
                if(!this.config.decode) {
                    this.setDecoder(header.format);
                }

                // If the line can be parsed as a feature assume we are beyond the header, if any
                const tokens = line.split(this.delimiter || "\t");
                try {
                    const tmpHeader = Object.assign({columnNames}, header);
                    let firstFeature;
                    if (firstFeature = this.decode(tokens, tmpHeader)) {
                        header.firstFeature = firstFeature;
                        break
                    } else {
                        if (tokens.length > 1) {
                            columnNames = tokens; // possible column names
                        }
                    }
                } catch (e) {
                    // Not a feature
                    if (tokens.length > 1) {
                        columnNames = tokens; // possible column names
                    }
                }
            }
        }

        if (columnNames) {
            header.columnNames = columnNames;
            for (let n = 0; n < columnNames.length; n++) {
                if (columnNames[n] === "color" || columnNames[n] === "colour") {
                    header.colorColumn = n;
                } else if (columnNames[n] === "thickness") {
                    header.thicknessColumn = n;
                }
            }
        }

        this.header = header;    // Directives might be needed for parsing lines
        return header
    }

    async parseFeatures(dataWrapper) {

        const allFeatures = [];
        const decode = this.decode;
        const format = this.header.format;
        const delimiter = this.delimiter || "\t";
        let i = 0;
        let errorCount = 0;
        let line;
        while ((line = await dataWrapper.nextLine()) !== undefined) {
            i++;
            if (i <= this.skipRows) continue

            if (!line || line.startsWith("track") || line.startsWith("#") || line.startsWith("browser")) {
                continue
            } else if (format === "wig" && line.startsWith("fixedStep")) {
                this.header.wig = parseFixedStep(line);
                continue
            } else if (format === "wig" && line.startsWith("variableStep")) {
                this.header.wig = parseVariableStep(line);
                continue
            }

            const tokens = line.split(delimiter);
            if (tokens.length < 1) {
                continue
            }

            const feature = decode(tokens, this.header);

            if (feature instanceof DecodeError) {
                errorCount++;
                if (errorCount > 0) {
                    console.error(`Error parsing line '${line}': ${feature.message}`);
                }
                continue
            }

            if (feature) {
                allFeatures.push(feature);
            }
        }

        // Special hack for bedPE
        if (decode === decodeBedpe) {
            fixBedPE(allFeatures);
        }

        if (("gtf" === this.config.format || "gff3" === this.config.format || "gff" === this.config.format) &&
            this.config.assembleGFF !== false) {
            return (new GFFHelper(this.config)).combineFeatures(allFeatures)
        } else {
            return allFeatures
        }

    }

    setDecoder(format) {

        switch (format) {
            case "broadpeak":
            case "regionpeak":
            case "peaks":
                this.decode = decodePeak;
                this.delimiter = this.config.delimiter || /\s+/;
                break
            case "narrowpeak":
                this.decode = decodeNarrowPeak;
                this.delimiter = this.config.delimiter || /\s+/;
                break
            case "bedgraph":
                this.decode = decodeBedGraph;
                this.delimiter = /\s+/;
                break
            case "wig":
                this.decode = decodeWig;
                this.delimiter = this.config.delimiter || /\s+/;
                break
            case "gff3" :
            case "gff":
                this.decode = decodeGFF3;
                this.delimiter = "\t";
                break
            case "gtf" :
                this.decode = decodeGTF;
                this.delimiter = "\t";
                break
            case "fusionjuncspan":
                // bhaas, needed for FusionInspector view
                this.decode = decodeFusionJuncSpan;
                this.delimiter = this.config.delimiter || /\s+/;
                break
            case "gtexgwas":
                this.skipRows = 1;
                this.decode = decodeGtexGWAS;
                this.delimiter = "\t";
                break
            case "refflat":
                this.decode = decodeReflat;
                this.delimiter = this.config.delimiter || /\s+/;
                break
            case "genepred":
                this.decode = decodeGenePred;
                this.delimiter = this.config.delimiter || /\s+/;
                break
            case "genepredext":
                this.decode = decodeGenePredExt;
                this.delimiter = this.config.delimiter || /\s+/;
                break
            case "ensgene":
                this.decode = decodeGenePred;
                this.header.shift = 1;
                this.delimiter = this.config.delimiter || /\s+/;
                break
            case "refgene":
                this.decode = decodeGenePredExt;
                this.delimiter = this.config.delimiter || /\s+/;
                this.header.shift = 1;
                break
            case "bed":
                this.decode = decodeBed;
                this.delimiter = this.config.delimiter || /\s+/;
                break
            case "gappedpeak":
                this.decode = decodeGappedPeak;
                this.delimiter = this.config.delimiter || /\s+/;
                break
            case "bedmethyl":
                this.decode = decodeBedmethyl;
                this.delimiter = this.config.delimiter || /\s+/;
                break
            case "bedpe":
            case "hiccups":
                this.decode = decodeBedpe;
                this.delimiter = this.config.delimiter || "\t";
                break
            case "bedpe-domain":
                this.decode = decodeBedpeDomain;
                this.headerLine = true;
                this.delimiter = this.config.delimiter || "\t";
                break
            case "bedpe-loop":
                this.decode = decodeBedpe;
                this.delimiter = this.config.delimiter || "\t";
                this.header = {colorColumn: 7};
                break
            case "interact":
                this.decode = decodeInteract;
                this.delimiter = this.config.delimiter || /\s+/;
                break
            case "snp":
                this.decode = decodeSNP;
                this.delimiter = "\t";
                break
            case "rmsk":
                this.decode = decodeRepeatMasker;
                this.delimiter = "\t";
                break
            case "gcnv":
                this.decode = decodeGcnv;
                this.delimiter = "\t";
                break
            case "shoebox":
                this.decode = decodeShoebox;
                this.delimiter = "\t";
                break
            default:
                const customFormat = getFormat(format);
                if (customFormat !== undefined) {
                    this.decode = decodeCustom;
                    this.header.customFormat = customFormat;
                    this.delimiter = customFormat.delimiter || "\t";
                } else {
                    this.decode = decodeBed;
                    this.delimiter = this.config.delimiter || /\s+/;
                }
        }

    }
}

function parseTrackLine$1(line) {

    const properties = {};
    const tokens = line.split(/(?:")([^"]+)(?:")|([^\s"]+)(?=\s+|$)/g);

    // Clean up tokens array
    let curr;
    const tmp = [];
    for (let tk of tokens) {
        if (!tk || tk.trim().length === 0) continue
        if (tk.endsWith("=")) {
            curr = tk;
        } else if (curr) {
            tmp.push(curr + tk);
            curr = undefined;
        } else {
            tmp.push(tk);
        }
    }

    for (let str of tmp) {
        if (!str) return
        var kv = str.split('=', 2);
        if (kv.length === 2) {
            const key = kv[0].trim();
            const value = kv[1].trim();
            if (properties.hasOwnProperty(key)) {
                let currentValue = properties[key];
                if (Array.isArray(currentValue)) {
                    currentValue.push(value);
                } else {
                    properties[key] = [currentValue, value];
                }
            } else {
                properties[key] = value;
            }
        }
    }
    if ("interact" == properties["type"]) {
        properties["format"] = "interact";
    } else if ("gcnv" === properties["type"]) {
        properties["format"] = "gcnv";
    }
    return properties
}

function parseColumnsDirective$1(line) {

    let properties = {};
    let t1 = line.split(/\s+/);

    if (t1.length === 2) {
        let t2 = t1[1].split(";");
        t2.forEach(function (keyValue) {
            let t = keyValue.split("=");
            if (t[0] === "color") {
                properties.colorColumn = Number.parseInt(t[1]) - 1;
            } else if (t[0] === "thickness") {
                properties.thicknessColumn = Number.parseInt(t[1]) - 1;
            }
        });
    }

    return properties
}

function parseFixedStep(line) {
    const tokens = line.split(/\s+/);
    const chrom = tokens[1].split("=")[1];
    const start = parseInt(tokens[2].split("=")[1], 10) - 1;
    const step = parseInt(tokens[3].split("=")[1], 10);
    const span = (tokens.length > 4) ? parseInt(tokens[4].split("=")[1], 10) : 1;
    return {format: "fixedStep", chrom, start, step, span, index: 0}
}

function parseVariableStep(line) {
    const tokens = line.split(/\s+/);
    const chrom = tokens[1].split("=")[1];
    const span = tokens.length > 2 ? parseInt(tokens[2].split("=")[1], 10) : 1;
    return {format: "variableStep", chrom, span}
}

function createCheckbox(name, initialState) {

    const container = div$2({class: 'igv-menu-popup-check-container'});

    const div$1 = div$2();
    container.appendChild(div$1);

    const svg = createIcon$1('check', (true === initialState ? '#444' : 'transparent'));
    div$1.appendChild(svg);

    const label = div$2();
    label.innerText = name;
    container.appendChild(label);

    return container
}

// Assigns a row # to each feature.  If the feature does not fit in any row and #rows == maxRows no
// row number is assigned.
function pack(featureList, maxRows) {

    maxRows = maxRows || Number.MAX_SAFE_INTEGER;
    const rows = [];
    featureList.sort(function (a, b) {
        return a.start - b.start
    });
    rows.push(-1000);

    for (let feature of featureList) {
        let r = 0;
        const len = Math.min(rows.length, maxRows);
        for (r = 0; r < len; r++) {
            if (feature.start >= rows[r]) {
                feature.row = r;
                rows[r] = feature.end;
                break
            }
        }
        feature.row = r;
        rows[r] = feature.end;
    }
}

const DEFAULT_MAX_WG_COUNT$1 = 10000;

/**
 * Return a collection of "whole genome" features wrapping the supplied features, possibly downsampled.  The purpose
 * is to support painting features in "whole genome view".
 *
 * @param allFeatures - dictionary (object), keys are chromosome names, values are lists of features
 * @param genome
 * @param maxWGCount - optional, maximum # of whole genome features to computer
 * @returns {*[]}
 */
async function computeWGFeatures(allFeatures, genome, chromAliasManager, maxWGCount) {


    const aliasTable = new Map();
    const chrTable = new Map();

    const makeWGFeature = (f) => {
        const wg = Object.assign({}, f);
        wg.chr = "all";


        if (f.chr2 && f.end2) {
            const c1 = aliasTable.get(f.chr1) || f.chr1;
            const c2 = aliasTable.get(f.chr2) || f.chr2;
            wg.start = genome.getGenomeCoordinate(c1, f.start1);
            wg.end = genome.getGenomeCoordinate(c2, f.end2);
        } else {
            const c = aliasTable.get(f.chr) || f.chr;
            wg.start = genome.getGenomeCoordinate(c, f.start);
            wg.end = genome.getGenomeCoordinate(c, f.end);
        }
        wg._f = f;
        // Don't draw exons in whole genome view
        if (wg["exons"]) delete wg["exons"];
        return wg
    };

    const wgChromosomeNames = new Set(genome.wgChromosomeNames);

    if (chromAliasManager) {
        for (let c of genome.wgChromosomeNames) {
            const alias = await chromAliasManager.getAliasName(c);
            aliasTable.set(c, alias);
            chrTable.set(alias, c);  // Reverse lookup
        }
    }

    const wgFeatures = [];
    let count = 0;
    for (let c of genome.wgChromosomeNames) {

        if (Array.isArray(allFeatures)) {
            const featureDict = {};
            for (let f of allFeatures) {
                const chr = genome.getChromosomeName(f.chr);
                if (!featureDict.hasOwnProperty(chr)) {
                    featureDict[chr] = [];
                }
                featureDict[chr].push(f);
            }
            allFeatures = featureDict;
        }

        // Look up the chromosome name in the alias table.  This maps names in genome => names in dataset.
        const queryChr = aliasTable.get(c) || c;
        const features = allFeatures[queryChr];

        if (features) {
            const max = maxWGCount || DEFAULT_MAX_WG_COUNT$1;
            for (let f of features) {
                if (f.dup) continue  // Skip duplicates, these are pseudo features for inter-chromosomal features

                // Reverse lookup for chromosome names, names in dataset => names in genome
                const chr = chrTable.get(f.chr) || f.chr;
                const chr2 = f.chr2 ? (chrTable.get(f.chr2) || f.chr2) : chr;
                if (wgChromosomeNames.has(chr) && wgChromosomeNames.has(chr2)) {
                    if (wgFeatures.length < max) {
                        wgFeatures.push(makeWGFeature(f));
                    } else {
                        //Reservoir sampling
                        const samplingProb = max / (count + 1);
                        if (Math.random() < samplingProb) {
                            const idx = Math.floor(Math.random() * (max - 1));
                            wgFeatures[idx] =  makeWGFeature(f);
                        }
                    }
                }
                count++;
            }
        }
    }

    wgFeatures.sort(function (a, b) {
        return a.start - b.start
    });

    return wgFeatures
}

/**
 * Assigns a row to each feature such that features do not overlap.
 *
 * @param features
 * @param maxRows
 * @param filter Function thta takes a feature and returns a boolean indicating visibility
 */
function packFeatures(features, maxRows, filter) {

    maxRows = maxRows || 1000;
    if (features == null || features.length === 0) {
        return
    }
    // Segregate by chromosome
    const chrFeatureMap = {};
    const chrs = [];
    for (let feature of features) {
        if (filter && !filter(feature)) {
            feature.row = undefined;
        } else {
            const chr = feature.chr;
            let flist = chrFeatureMap[chr];
            if (!flist) {
                flist = [];
                chrFeatureMap[chr] = flist;
                chrs.push(chr);
            }
            flist.push(feature);
        }
    }

    // Loop through chrosomosomes and pack features;
    for (let chr of chrs) {
        pack(chrFeatureMap[chr], maxRows);
    }
}


/**
 * Return the index at which a new feature should be inserted in the sorted featureList.  It is assumed
 * that featureList is sorted by the compare function.  If featureList has 1 or more features with compare === 0
 * the new feature should be inserted at the end.
 *
 * @param featureList
 * @param center
 * @param direction -- forward === true, reverse === false
 * @returns {feature}
 */

function findFeatureAfterCenter(featureList, center, direction = true) {

    const featureCenter = (feature) => (feature.start + feature.end) / 2;

    const compare = direction ?
        (o1, o2) => o1.start - o2.start + o1.end - o2.end :
        (o2, o1) => o1.start - o2.start + o1.end - o2.end;
    const sortedList = Array.from(featureList);
    sortedList.sort(compare);

    let low = 0;
    let high = sortedList.length;
    while (low < high) {
        let mid = Math.floor((low + high) / 2);
        if (direction) {
            if (featureCenter(sortedList[mid]) <= center) {
                low = mid + 1;
            } else {
                high = mid;
            }
        } else {
            if (featureCenter(sortedList[mid]) >= center) {
                low = mid + 1;
            } else {
                high = mid;
            }

        }
    }
    return sortedList[low]
}

const fixColor = (colorString) => {
    if (isString$3(colorString)) {
        return (colorString.indexOf(",") > 0 && !(colorString.startsWith("rgb(") || colorString.startsWith("rgba("))) ?
            `rgb(${colorString})` : colorString
    } else {
        return colorString
    }
};

/**
 * A collection of properties and methods shared by all (or most) track types.
 *
 * @param config
 * @param browser
 * @constructor
 */
class TrackBase {

    static defaultColor = 'rgb(150,150,150)'

    static defaults = {
        height: 50,
        autoHeight: false,
        visibilityWindow: undefined,   // Identifies property that should be copied from config
        color: undefined,  // Identifies property that should be copied from config
        altColor: undefined,  // Identifies property that should be copied from config
        supportHiDPI: true,
        selected: false
    }

    constructor(config, browser) {
        this.browser = browser;
        this.init(config);
    }

    /**
     * Initialize track properties from the config object.  This method is typically overriden in subclasses, which
     * will call this implementation as super.init(config).
     *
     * @param config
     */
    init(config) {

        this.config = config;

        if (config.displayMode) {
            config.displayMode = config.displayMode.toUpperCase();
        }

        // Base default settings
        const defaults = Object.assign({}, TrackBase.defaults);

        // Overide with class specific default settings
        if (this.constructor.defaults) {
            for (let key of Object.keys(this.constructor.defaults)) {
                defaults[key] = this.constructor.defaults[key];
            }
        }

        for (let key of Object.keys(defaults)) {
            this[key] = config.hasOwnProperty(key) ? config[key] : defaults[key];
            if ((key === 'color' || key === 'altColor') && this[key]) {
                this[key] = fixColor(this[key]);
            }
        }

        // this._initialColor = this.color || this.constructor.defaultColor
        // this._initialAltColor = this.altColor || this.constructor.defaultColor

        if (config.name || config.label) {
            this.name = config.name || config.label;
        } else if (isFile$1(config.url)) {
            this.name = config.url.name;
        } else if (isString$3(config.url) && !config.url.startsWith("data:")) {
            this.name = getFilename$2(config.url);
        }

        this.url = config.url;
        if (this.config.type) this.type = this.config.type;
        this.id = this.config.id === undefined ? this.name : this.config.id;
        this.order = config.order;
        this.autoscaleGroup = config.autoscaleGroup;
        this.removable = config.removable === undefined ? true : config.removable;      // Defaults to true
        this.minHeight = config.minHeight || Math.min(25, this.height);
        this.maxHeight = config.maxHeight || Math.max(1000, this.height);

        if (config.onclick) {
            this.onclick = config.onclick;
            config.onclick = undefined;   // functions cannot be saved in sessions, clear it here.
        }

        if (config.description) {
            // Override description -- displayed when clicking on track label.  Convert to function if neccessary
            if (typeof config.description === 'function') {
                this.description = config.description;
            } else {
                this.description = () => config.description;
            }
        }
    }

    async postInit() {

        this._initialColor = this.color || this.constructor.defaultColor;
        this._initialAltColor = this.altColor || this.constructor.defaultColor;
        return this
    }

    get name() {
        return this._name
    }

    set name(name) {
        this._name = name;
        if (this.trackView) {
            this.trackView.setTrackLabelName(name);
        }
    }

    clearCachedFeatures() {
        if (this.trackView) {
            this.trackView.clearCachedFeatures();
        }
    }

    updateViews() {
        if (this.trackView) {
            this.trackView.updateViews();
        }
    }

    repaintViews() {
        if (this.trackView) {
            this.trackView.repaintViews();
        }
    }

    /**
     * Used to create session object for bookmarking, sharing.  Only simple property values (string, number, boolean)
     * are saved.
     */
    getState() {

        const isJSONable = item => !(item === undefined || typeof item === 'function' || item instanceof Promise);

        // Create copy of config, minus transient properties (convention is name starts with '_').  Also, all
        // function properties are transient as they cannot be saved in json
        const state = {};

        const jsonableConfigKeys = Object.keys(this.config).filter(key => isJSONable(this.config[key]));

        for (const key of jsonableConfigKeys) {
            if (!key.startsWith("_")) {
                state[key] = this.config[key];
            }
        }

        // Update original config values with any changes
        for (let key of Object.keys(state)) {
            if (key.startsWith("_")) continue   // transient property
            const value = this[key];
            if (value !== undefined && (isSimpleType(value) || typeof value === "boolean" || key === "metadata")) {
                state[key] = value;
            }
        }

        // If user has changed other properties from defaults update state.
        const defs = Object.assign({}, TrackBase.defaults);
        if (this.constructor.defaults) {
            for (let key of Object.keys(this.constructor.defaults)) {
                defs[key] = this.constructor.defaults[key];
            }
        }
        for (let key of Object.keys(defs)) {
            if (undefined !== this[key] && defs[key] !== this[key]) {
                state[key] = this[key];
            }
        }

        // Flatten dataRange if present
        if (!this.autoscale && this.dataRange) {
            state.min = this.dataRange.min;
            state.max = this.dataRange.max;
        }

        if (this.autoscaleGroup) {
            state.autoscaleGroup = this.autoscaleGroup;
        }

        return state
    }

    get supportsWholeGenome() {
        return this.config.supportsWholeGenome === true
    }

    /**
     * Does the track support sample names.  Current sample aware tracks include VCF (with genotypes), MUT, MAF, and SEG
     * @returns {boolean}
     */
    hasSamples() {
        return false
    }

    getGenomeId() {
        return this.browser.genome ? this.browser.genome.id : undefined
    }

    /**
     * Set certain track properties, usually from a "track" line.  Not all UCSC properties are supported.
     *
     * Track configuration settings have precendence over track line properties, so if both are present ignore the
     * track line.
     *
     * @param properties
     */
    setTrackProperties(properties) {

        if (this.disposed) return   // This track was removed during async load

        const tracklineConfg = {};
        let tokens;
        for (let key of Object.keys(properties)) {
            switch (key.toLowerCase()) {
                case "usescore":
                    tracklineConfg.useScore = (
                        properties[key] === 1 || properties[key] === "1" || properties[key] === "on" || properties[key] === true);
                    break
                case "visibility":
                    //0 - hide, 1 - dense, 2 - full, 3 - pack, and 4 - squish
                    switch (properties[key]) {
                        case "2":
                        case "3":
                        case "pack":
                        case "full":
                            tracklineConfg.displayMode = "EXPANDED";
                            break
                        case "4":
                        case "squish":
                            tracklineConfg.displayMode = "SQUISHED";
                            break
                        case "1":
                        case "dense":
                            tracklineConfg.displayMode = "COLLAPSED";
                    }
                    break
                case "color":
                case "altcolor":
                    tracklineConfg[key] = properties[key].startsWith("rgb(") ? properties[key] : "rgb(" + properties[key] + ")";
                    break
                case "featurevisiblitywindow":
                case "visibilitywindow":
                    tracklineConfg.visibilityWindow = Number.parseInt(properties[key]);
                    break
                case "maxheightpixels":
                    tokens = properties[key].split(":");
                    if (tokens.length === 3) {
                        tracklineConfg.minHeight = Number.parseInt(tokens[2]);
                        tracklineConfg.height = Number.parseInt(tokens[1]);
                        tracklineConfg.maxHeight = Number.parseInt(tokens[0]);
                    }
                    break
                case "viewlimits":
                    if (!this.config.autoscale && !this.config.max) {   //config has precedence
                        tokens = properties[key].split(":");
                        let min = 0;
                        let max;
                        if (tokens.length == 1) {
                            max = Number(tokens[0]);
                        } else if (tokens.length == 2) {
                            min = Number(tokens[0]);
                            max = Number(tokens[1]);
                        }
                        if (Number.isNaN(max) || Number.isNaN(min)) {
                            console.warn(`Unexpected viewLimits value in track line: ${properties["viewLimits"]}`);
                        } else {
                            tracklineConfg.autoscale = false;
                            tracklineConfg.dataRange = {min, max};
                            this.viewLimitMin = min;
                            this.viewLimitMax = max;
                        }
                    }
                case "name":
                    tracklineConfg[key] = properties[key];
                    break
                case "url":
                    tracklineConfg["infoURL"] = properties[key];
                    break
                case "type":
                    const v = properties[key];
                    if (UCSCTypeMappings.has(v)) {
                        tracklineConfg[key] = UCSCTypeMappings.get(v);
                    } else {
                        tracklineConfg[key] = v;
                    }
                    break
                case "graphtype":
                    tracklineConfg["graphType"] = properties[key];
                    break
                default:
                    tracklineConfg[key] = properties[key];
            }
        }

        // Track configuration objects have precedence over track line properties in general.  The "name" property
        // is an exception if it was derived and not explicitly entered (that is derived from the web app from filename).
        for (let key of Object.keys(tracklineConfg)) {

            if (!this.config.hasOwnProperty(key) || (key === "name" && this.config._derivedName)) {
                let value = tracklineConfg[key];
                if ("true" === value) value = true;
                if ("false" === value) value = false;

                this[key] = value;
                if (key === "height" && this.trackView) {
                    try {
                        const h = Number.parseInt(value);
                        this.trackView.setTrackHeight(h);
                    } catch (e) {
                        console.error(e);
                    }
                }
            }
        }
    }

    /**
     * Return the features clicked over.  Default implementation assumes an array of features and only considers
     * the genomic location.   Overriden by most subclasses.
     *
     * @param clickState
     * @returns {[]|*[]}
     */
    clickedFeatures(clickState) {

        // We use the cached features rather than method to avoid async load.  If the
        // feature is not already loaded this won't work,  but the user wouldn't be mousing over it either.
        const features = clickState.viewport.cachedFeatures;

        if (!features || !Array.isArray(features) || features.length === 0) {
            return []
        }

        const genomicLocation = clickState.genomicLocation;

        // When zoomed out we need some tolerance around genomicLocation
        const tolerance = (clickState.referenceFrame.bpPerPixel > 0.2) ? 3 * clickState.referenceFrame.bpPerPixel : 0.2;
        const ss = genomicLocation - tolerance;
        const ee = genomicLocation + tolerance;
        return (FeatureUtils.findOverlapping(features, ss, ee))
    }

    /**
     * Default popup text function -- just extracts string and number properties in random order.
     * @param feature     * @returns {Array}
     */
    extractPopupData(feature, genomeId) {

        const filteredProperties = new Set(['row', 'color', 'chr', 'start', 'end', 'cdStart', 'cdEnd', 'strand', 'alpha']);
        const data = [];

        let alleles, alleleFreqs;
        for (let property in feature) {

            if (feature.hasOwnProperty(property) &&
                !filteredProperties.has(property) &&
                isSimpleType(feature[property])) {

                let value = feature[property];
                data.push({name: capitalize(property), value: value});

                if (property === "alleles") {
                    alleles = feature[property];
                } else if (property === "alleleFreqs") {
                    alleleFreqs = feature[property];
                }
            }
        }

        if (alleles && alleleFreqs) {

            if (alleles.endsWith(",")) {
                alleles = alleles.substr(0, alleles.length - 1);
            }
            if (alleleFreqs.endsWith(",")) {
                alleleFreqs = alleleFreqs.substr(0, alleleFreqs.length - 1);
            }

            let a = alleles.split(",");
            let af = alleleFreqs.split(",");
            if (af.length > 1) {
                let b = [];
                for (let i = 0; i < af.length; i++) {
                    b.push({a: a[i], af: Number(af[i])});
                }
                b.sort(function (x, y) {
                    return x.af - y.af
                });

                let ref = b[b.length - 1].a;
                if (ref.length === 1) {
                    for (let i = b.length - 2; i >= 0; i--) {
                        let alt = b[i].a;
                        if (alt.length === 1) {
                            if (!genomeId) genomeId = this.getGenomeId();
                            const cravatLink = TrackBase.getCravatLink(feature.chr, feature.start + 1, ref, alt, genomeId);
                            if (cravatLink) {
                                data.push('<hr/>');
                                data.push({html: cravatLink});
                                data.push('<hr/>');
                            }
                        }
                    }
                }
            }
        }

        if (feature.attributes) {
            for (let key of Object.keys(feature.attributes)) {
                data.push({name: key, value: feature.attributes[key]});
            }
        }

        // final chr position
        let posString = `${feature.chr}:${numberFormatter$1(feature.start + 1)}-${numberFormatter$1(feature.end)}`;
        if (feature.strand) {
            posString += ` (${feature.strand})`;
        }

        data.push({name: 'Location', value: posString});

        return data

    }


    /**
     * Default track description -- displayed on click of track label.  This can be overriden in the track
     * configuration, or in subclasses.
     */
    description() {

        const createKeyValueRow = (key, value) => {
            const row = document.createElement('div');
            row.className = 'igv-track-label-popover__row';
            
            const keySpan = document.createElement('span');
            keySpan.className = 'igv-track-label-popover__key';
            keySpan.textContent = key + ':';
            
            const valueSpan = document.createElement('span');
            valueSpan.className = 'igv-track-label-popover__value';
            valueSpan.textContent = value;
            
            row.appendChild(keySpan);
            row.appendChild(valueSpan);
            return row;
        };

        const fragment = document.createDocumentFragment();
        
        if (this.url) {
            if (isFile$1(this.url)) {
                fragment.appendChild(createKeyValueRow('Filename', this.url.name));
            } else {
                fragment.appendChild(createKeyValueRow('URL', this.url));
            }
        } else {
            // If no URL, just return the name as a simple text node
            const nameDiv = document.createElement('div');
            nameDiv.className = 'igv-track-label-popover__row';
            nameDiv.textContent = this.name;
            fragment.appendChild(nameDiv);
            return fragment;
        }
        
        if (this.config) {
            if (this.config.metadata) {
                for (let key of Object.keys(this.config.metadata)) {
                    const value = this.config.metadata[key];
                    fragment.appendChild(createKeyValueRow(key, value));
                }
            }

            // Add any config properties that are capitalized
            for (let key of Object.keys(this.config)) {
                if (key.startsWith("_")) continue;   // transient property
                let first = key.substr(0, 1);
                if (first !== first.toLowerCase()) {
                    const value = this.config[key];
                    if (value && isSimpleType(value)) {
                        fragment.appendChild(createKeyValueRow(key, value));
                    }
                }
            }
        }
        
        return fragment;
    }

    /**
     * Return color for a specific feature of this track.  This default implementation is overriden by subclasses*
     * @param f
     * @returns {*|string|string}
     */
    getColorForFeature(f) {
        return (typeof this.color === "function") ? this.color(feature) : this.color
    }

    numericDataMenuItems() {

        const menuItems = [];

        // Data range or color scale

        if ("heatmap" !== this.graphType) {

            menuItems.push('<hr/>');

            function dialogPresentationHandler(e) {

                if (this.trackView.track.selected) {
                    this.browser.dataRangeDialog.configure(this.trackView.browser.getSelectedTrackViews());
                } else {
                    this.browser.dataRangeDialog.configure(this.trackView);
                }
                this.browser.dataRangeDialog.present(e);
            }

            menuItems.push({label: 'Set data range', dialog: dialogPresentationHandler});

            if (this.logScale !== undefined) {

                function logScaleHandler() {
                    this.logScale = !this.logScale;
                    this.trackView.repaintViews();
                }

                menuItems.push({element: createCheckbox("Log scale", this.logScale), click: logScaleHandler});
            }

            function autoScaleHandler() {
                this.autoscaleGroup = undefined;
                this.autoscale = !this.autoscale;
                this.browser.updateViews();
            }

            menuItems.push({element: createCheckbox("Autoscale", this.autoscale), click: autoScaleHandler});
        }

        return menuItems
    }

    setDataRange({min, max}) {

        this.dataRange = {min, max};
        this.autoscale = false;
        this.autoscaleGroup = undefined;
        this.trackView.repaintViews();
    }

    /**
     * Return the first feature in this track whose start position is > position
     * @param chr
     * @param position
     * @param direction
     * @returns {Promise<void>}
     */
    async nextFeatureAfter(chr, position, direction) {
        const viewport = this.trackView.viewports[0];
        let features = viewport.cachedFeatures;
        if (features && Array.isArray(features) && features.length > 0) {
            // Check chromosome, all cached features will share a chromosome
            const chrName = this.browser.genome.getChromosomeName(features[0].chr);
            if (chrName === chr) {
                const next = findFeatureAfterCenter(features, position, direction);
                if (next) {
                    return next
                }
            }
        }

        if (typeof this.featureSource.nextFeature === 'function') {
            return this.featureSource.nextFeature(chr, position, direction, this.visibilityWindow)
        }
    }

    /**
     * Track has been permanently removed.  Release resources and other cleanup
     */
    dispose() {

        this.disposed = true;

        // This should not be neccessary, but in case there is some unknown reference holding onto this track object,
        // for example in client code, release any resources here.
        for (let key of Object.keys(this)) {
            this[key] = undefined;
        }
    }

    static getCravatLink(chr, position, ref, alt, genomeID) {

        if ("hg38" === genomeID || "GRCh38" === genomeID) {

            const cravatChr = chr.startsWith("chr") ? chr : "chr" + chr;
            return `<a target="_blank" href="https://run.opencravat.org/result/nocache/variant.html` +
                `?chrom=${cravatChr}&pos=${position}&ref_base=${ref}&alt_base=${alt}"><b>Cravat ${ref}->${alt}</b></a>`

        } else {
            return undefined
        }
    }

    static localFileInspection(config) {

        const cooked = Object.assign({}, config);
        const lut =
            {
                url: 'file',
                indexURL: 'indexFile'
            };

        for (const key of ['url', 'indexURL']) {
            if (cooked[key] && cooked[key] instanceof File) {
                cooked[lut[key]] = cooked[key].name;
                delete cooked[key];
            }
        }

        return cooked
    }

    // Methods to support filtering api


    set filter(f) {
        this._filter = f;
        this.trackView.repaintViews();
    }

    /**
     * Return features visible in current viewports.
     *
     * @returns {*[]}
     */
    getInViewFeatures() {
        const inViewFeatures = [];
        for (let viewport of this.trackView.viewports) {
            if (viewport.isVisible()) {
                const referenceFrame = viewport.referenceFrame;
                referenceFrame.chr;
                const start = referenceFrame.start;
                const end = start + referenceFrame.toBP(viewport.getWidth());

                // We use the cached features  to avoid async load.  If the
                // feature is not already loaded it is by definition not in view.
                if (viewport.cachedFeatures) {
                    const viewFeatures = FeatureUtils.findOverlapping(viewport.cachedFeatures, start, end);
                    for (let f of viewFeatures) {
                        if (!this._filter || this._filter(f)) {
                            inViewFeatures.push(f);
                        }
                    }
                }
            }
        }
        return inViewFeatures
    }

    /**
     * Return an object enumerating filterable attributes, that is attributes that can be used to filter features.
     * The base implementation returns an empty object, overriden in subclasses.
     *
     * @returns {{}}
     */
    getFilterableAttributes() {
        return {}
    }
}

/**
 * Map UCSC track line "type" setting to file format.  In igv.js "type" refers to the track type, not the input file format
 * @type {Map<string, string>}
 */
const UCSCTypeMappings = new Map([
    ["wiggle_0", "wig"],
    ["bed", "bed"],
    ["bigBed", "bigBed"],
    ["bigWig", "bigWig"]
]);

/**
 *  Define parser for seg files  (.bed, .gff, .vcf, etc).  A parser should implement 2 methods
 *
 *     parseHeader(data) - return an object representing a header.  Details are format specific
 *
 *     parseFeatures(data) - return a list of features
 *
 */


class SegParser {

    constructor(type) {
        this.type = type || 'seg';   // One of seg, mut, or maf

        switch (this.type) {
            case 'mut':
                this.sampleColumn = 3;
                this.chrColumn = 0;
                this.startColumn = 1;
                this.endColumn = 2;
                this.dataColumn = 4;
                break
            case 'maf':
                this.sampleColumn = 15;
                this.chrColumn = 4;
                this.startColumn = 5;
                this.endColumn = 6;
                this.dataColumn = 8;
                break
            default:
                this.sampleColumn = 0;
                this.chrColumn = 1;
                this.startColumn = 2;
                this.endColumn = 3;
            // Data column determined after reading header
        }
    }

    async parseHeader(dataWrapper) {
        let line;
        while ((line = await dataWrapper.nextLine()) !== undefined) {
            if (line.startsWith("#")) ; else {
                const tokens = line.split("\t");
                this.header = {headings: tokens};
                break
            }
        }
        return this.header
    }

    async parseFeatures(dataWrapper) {

        const allFeatures = [];
        let extraHeaders;
        if (!this.header) {
            this.header = await this.parseHeader(dataWrapper);  // This will only work for non-indexed files
        }
        if ('seg' === this.type) {
            this.dataColumn = this.header.headings.length - 1;
        }
        if (this.header.headings.length > 5) {
            extraHeaders = this.extractExtraColumns(this.header.headings);
        }
        const valueColumnName = this.header.headings[this.dataColumn];

        let line;
        while ((line = await dataWrapper.nextLine()) !== undefined) {
            const tokens = line.split("\t");
            const value = ('seg' === this.type) ? Number(tokens[this.dataColumn]) : tokens[this.dataColumn];
            if (tokens.length > this.dataColumn) {
                const feature = new SegFeature({
                    sample: tokens[this.sampleColumn],
                    chr: tokens[this.chrColumn],
                    start: parseInt(tokens[this.startColumn]) - 1,
                    end: parseInt(tokens[this.endColumn]),
                    value,
                    valueColumnName
                });
                if (extraHeaders) {
                    const extraValues = this.extractExtraColumns(tokens);
                    feature.setAttributes({names: extraHeaders, values: extraValues});
                }
                allFeatures.push(feature);
            }
        }
        return allFeatures
    }

    extractExtraColumns(tokens) {
        const extras = [];
        for (let i = 0; i < tokens.length; i++) {
            if (i !== this.chrColumn && i !== this.startColumn && i !== this.endColumn && i !== this.sampleColumn) {
                extras.push(tokens[i]);
            }
        }
        return extras
    }

}

class SegFeature {

    constructor({sample, chr, start, end, value, valueColumnName}) {
        this.sample = sample;
        this.chr = chr;
        this.start = start;
        this.end = end;
        this.value = value;
        this.valueColumnName = valueColumnName;
    }

    setAttributes({names, values}) {
        this.attributeNames = names;
        this.attributeValues = values;
    }

    getAttributeValue(name) {
        if (this.attributeNames) {
            const idx = this.attributeNames.indexOf(name);
            if (idx >= 0) {
                return this.attributeValues[idx]
            }
        }
        return undefined
    }


    popupData(type, genomeID) {
        const filteredProperties = new Set(['chr', 'start', 'end', 'sample', 'value', 'row', 'color', 'sampleKey',
            'uniqueSampleKey', 'sampleId', 'chromosome', 'uniquePatientKey']);
        const locationString = (this.chr + ":" +
            numberFormatter$1(this.start + 1) + "-" +
            numberFormatter$1(this.end));
        const pd = [
            {name: "Sample", value: this.sample},
            {name: "Location", value: locationString},
            {name: this.valueColumnName ? capitalize(this.valueColumnName) : "Value", value: this.value}
        ];

        // TODO -- the Cravat stuff should probably be in the track (SegTrack)
        if ("mut" === type && "hg38" === genomeID) {
            const l = this.extractCravatLink(genomeID);
            if (l) {
                pd.push('<hr/>');
                pd.push({html: l});
                pd.push('<hr/>');
            }
        }

        if (this.attributeNames && this.attributeNames.length > 0) {
            for (let i = 0; i < this.attributeNames.length; i++) {
                if (!filteredProperties.has(this.attributeNames[i]) & this.valueColumnName !== this.attributeNames[i]) {
                    pd.push({name: capitalize(this.attributeNames[i]), value: this.attributeValues[i]});
                }
            }
        }
        return pd
    }

    extractCravatLink(genomeId) {

        let ref, alt;
        if (this.attributeNames && this.attributeNames.length > 0) {
            for (let i = 0; i < this.attributeNames.length; i++) {
                if (!ref && "Reference_Allele" === this.attributeNames[i]) {
                    ref = this.attributeValues[i];
                }
                if (!alt && this.attributeNames[i].startsWith("Tumor_Seq_Allele") && this.attributeValues[i] !== ref) {
                    alt = this.attributeValues[i];
                }
                if (ref && alt) {
                    return TrackBase.getCravatLink(this.chr, this.start + 1, ref, alt, genomeId)
                }
            }
        }

    }
}

/**
 * Create a variant from an array of tokens representing a line in a "VCF" file
 * @param tokens
 */

const STANDARD_FIELDS = new Map([["REF", "referenceBases"], ["ALT", "alternateBases"], ["QUAL", "quality"], ["FILTER", "filter"]]);


class Variant {

    constructor(tokens) {
        this.chr = tokens[0]; // TODO -- use genome aliases
        this.pos = parseInt(tokens[1]);
        this.names = tokens[2];    // id in VCF
        this.referenceBases = tokens[3];
        this.alternateBases = tokens[4];
        this.quality = tokens[5];
        this.filter = tokens[6];
        this.info = {};
        const infoStr = tokens[7];
        if (infoStr && infoStr !== '.') {
            for (let elem of infoStr.split(';')) {
                var element = elem.split('=');
                this.info[element[0]] = element[1];
            }
        }
        this.init();
    }


    getAttributeValue(key) {
        if (STANDARD_FIELDS.has(key)) {
            key = STANDARD_FIELDS.get(key);
        }
        return this.hasOwnProperty(key) ? this[key] : this.info[key]
    }


    init() {

        const ref = this.referenceBases;
        const altBases = this.alternateBases;

        if (this.info) {
            if (this.info["VT"]) {
                this.type = this.info["VT"];
            } else if (this.info["SVTYPE"]) {
                this.type = "SV";
            } else if (this.info["PERIOD"]) {
                this.type = "STR";
            }
        }
        if (this.type === undefined) {
            this.type = determineType(ref, altBases);
        }

        // Determine start/end coordinates -- these are the coordinates representing the actual variant,
        // not the leading or trailing reference
        if (this.info["END"]) {
            this.start = this.pos - 1;
            if (this.info["CHR2"] && this.info["CHR2"] !== this.chr) {
                this.end = this.start + 1;
            } else {
                this.end = Number.parseInt(this.info["END"]);
            }
        } else {
            if (this.type === "NONVARIANT") {
                this.start = this.pos - 1;      // convert to 0-based coordinate convention
                this.end = this.start + ref.length;
            } else {

                const altTokens = altBases.split(",").filter(token => token.length > 0);
                this.alleles = [];
                this.start = undefined;
                this.end = undefined;

                for (let alt of altTokens) {

                    this.alleles.push(alt);

                    // We don't yet handle  SV and other special alt representations
                    if ("SV" !== this.type && isKnownAlt(alt)) {

                        let altLength = alt.length;
                        let lengthOnRef = ref.length;
                        const lmin = Math.min(altLength, lengthOnRef);

                        // Trim off matching bases.  Try first match, then right -> left,  then any remaining left -> right
                        let s = 0;

                        while (s < lmin && (ref.charCodeAt(s) === alt.charCodeAt(s))) {
                            s++;
                            altLength--;
                            lengthOnRef--;
                        }

                        // right -> left from end
                        while (altLength > 0 && lengthOnRef > 0) {
                            const altIdx = s + altLength - 1;
                            const refIdx = s + lengthOnRef - 1;
                            if (alt.charCodeAt(altIdx) === ref.charCodeAt(refIdx)) {
                                altLength--;
                                lengthOnRef--;
                            } else {
                                break
                            }
                        }

                        // if any remaining, left -> right
                        while (altLength > 0 && lengthOnRef > 0) {
                            const altIdx = s;
                            const refIdx = s;
                            if (alt.charCodeAt(altIdx) === ref.charCodeAt(refIdx)) {
                                s++;
                                altLength--;
                                lengthOnRef--;
                            } else {
                                break
                            }
                        }

                        const alleleStart = this.pos + s - 1;      // -1 for zero based coordinates
                        const alleleEnd = alleleStart + lengthOnRef;
                        this.start = this.start === undefined ? alleleStart : Math.min(this.start, alleleStart);
                        this.end = this.end === undefined ? alleleEnd : Math.max(this.end, alleleEnd);
                    }
                }

                // Default to single base representation @ position for variant types not otherwise handled
                if (this.start === undefined) {
                    this.start = this.pos - 1;
                    this.end = this.pos;
                }

                // Infer an insertion from start === end
                if (this.start === this.end) {
                    this.start -= 0.5;
                    this.end += 0.5;
                }
            }
        }
    }

    popupData(genomicLocation, genomeId) {

        const posString = `${numberFormatter$1(this.pos)}`;
        const locString = this.start === this.end ?
            `${numberFormatter$1(this.start)} | ${numberFormatter$1(this.start + 1)}` :
            `${numberFormatter$1(this.start + 1)}-${numberFormatter$1(this.end)}`;
        const fields = [
            {name: "Chr", value: this.chr},
            {name: "Pos", value: posString},
            {name: "Loc", value: locString},
            {name: "ID", value: this.names ? this.names : ""},
            {name: "Ref", value: this.referenceBases},
            {name: "Alt", value: this.alternateBases.replace("<", "&lt;")},
            {name: "Qual", value: this.quality},
            {name: "Filter", value: this.filter}
        ];

        if (this.type) {
            fields.push({name: "Type", value: this.type});
        }

        if ("SNP" === this.type) {
            let ref = this.referenceBases;
            if (ref.length === 1) {
                let altArray = this.alternateBases.split(",");
                for (let alt of altArray) {
                    if (alt.length === 1) {
                        let l = TrackBase.getCravatLink(this.chr, this.pos, ref, alt, genomeId);
                        if (l) {
                            fields.push('<hr/>');
                            fields.push({html: l});
                        }
                    }
                }
            }
        }

        const infoKeys = Object.keys(this.info);
        if (this.info && infoKeys.length > 0) {
            fields.push({html: '<hr style="border-top: dotted 1px;border-color: #c9c3ba" />'});
            for (let key of infoKeys) {
                fields.push({name: key, value: arrayToString(decodeURIComponent(this.info[key]))});
            }
        }

        return fields
    }

    getInfo(tag) {
        return this.info ? this.info[tag] : undefined
    }

    isRefBlock() {
        return "NONVARIANT" === this.type
    }

    isFiltered() {
        return !("." === this.filter || "PASS" === this.filter)
    }

    alleleFreq() {
        return this.info ? this.info["AF"] : undefined
    }
}

/**
 * Represents the "other end" of an SV which specifies the breakpoint as CHR2 and END info fields.
 */
class SVComplement {

    constructor(v) {
        this.mate = v;
        this.chr = v.info.CHR2;
        this.pos = Number.parseInt(v.info.END);
        this.start = this.pos - 1;
        this.end = this.pos;
    }

    get info() {
        return this.mate.info
    }

    get names() {
        return this.mate.names
    }

    get referenceBases() {
        return this.mate.referenceBases
    }

    get alternateBases() {
        return this.mate.alternateBases
    }

    get quality() {
        return this.mate.quality
    }

    get filter() {
        return this.mate.filter
    }

    get calls() {
        return this.mate.calls
    }

    getAttributeValue(key) {
        return this.mate.getAttributeValue(key)
    }

    getInfo(tag) {
        this.mate.getInfo(tag);
    }

    isFiltered() {
        return this.mate.isFiltered()
    }

    alleleFreq() {
        return this.mate.alleleFreq()
    }

    popupData(genomicLocation, genomeId) {
        const popupData = [];

        popupData.push("SV Breakpoint");
        popupData.push({name: 'Chr', value: this.chr});
        popupData.push({name: 'Pos', value: `${numberFormatter$1(this.pos)}`});
        popupData.push({html: '<hr style="border-top: dotted 1px;border-color: #c9c3ba" />'});
        popupData.push("SV");
        popupData.push(...this.mate.popupData(genomicLocation, genomeId));

        return popupData
    }
}


class Call {

    constructor({formatFields, sample, token}) {

        this.info = {};
        this.sample = sample;
        const ct = token.split(":");
        for (let idx = 0; idx < ct.length; idx++) {
            const callToken = ct[idx];
            if (idx == formatFields.genotypeIndex) {
                this.genotype = [];
                for (let s of callToken.split(/[\|\/]/)) {
                    this.genotype.push('.' === s ? s : parseInt(s));
                }
            } else {
                this.info[formatFields.fields[idx]] = callToken;
            }
        }

    }


    get zygosity() {
        if (!this._zygosity) {
            if (!this.genotype) {
                this._zygosity = 'unknown';
            } else {
                let allVar = true;  // until proven otherwise
                let allRef = true;
                let noCall = false;

                for (let g of this.genotype) {
                    if ('.' === g) {
                        noCall = true;
                        break
                    } else {
                        if (g !== 0) allRef = false;
                        if (g === 0) allVar = false;
                    }
                }
                if (noCall) {
                    this._zygosity = 'nocall';
                } else if (allRef) {
                    this._zygosity = 'homref';
                } else if (allVar) {
                    this._zygosity = 'homvar';
                } else {
                    this._zygosity = 'hetvar';
                }
            }
        }
        return this._zygosity
    }

    /**
     * Used in sorting
     */
    zygosityScore() {
        const zygosity = this.zygosity;
        switch (zygosity) {
            case 'homvar':
                return 4
            case 'hetvar':
                return 3
            case 'homref':
                return 2
            case 'nocall':
                return 1
            default:
                return 0
        }
    }

    #zygosityLabel() {
        const zygosity = this.zygosity;
        switch (zygosity) {
            case 'homref':
                return 'Homozygous reference'
            case 'homvar':
                return 'Homozygous variant'
            case 'hetvar':
                return 'Heterozygous'
            default:
                return ''
        }
    }


    popupData(genomicLocation, genomeID) {

        const popupData = [];

        if (this.sample !== undefined) {
            popupData.push({name: 'Sample', value: this.sample});
        }

        // Genotype string is set in VariantTrack when call is clicked -- this is for memory efficienty, very few
        // calls will get clicked
        if (this.genotypeString) {
            popupData.push({name: 'Genotype', value: this.genotypeString});
        }

        const zygosity = this.#zygosityLabel();
        if (zygosity) {
            popupData.push({name: 'Zygosity', value: zygosity});
        }


        var infoKeys = Object.keys(this.info);
        if (infoKeys.length) {
            popupData.push('<hr/>');
        }
        for(let key of infoKeys) {
            popupData.push({name: key, value: decodeURIComponent(this.info[key])});
        }

        return popupData
    }
}

const knownAltBases = new Set(["A", "C", "T", "G"].map(c => c.charCodeAt(0)));

function isKnownAlt(alt) {
    for (let i = 0; i < alt.length; i++) {
        if (!knownAltBases.has(alt.charCodeAt(i))) {
            return false
        }
    }
    return true
}


function determineType(ref, altAlleles) {
    const refLength = ref.length;
    if (altAlleles === undefined) {
        return "UNKNOWN"
    } else if (altAlleles.trim().length === 0 ||
        altAlleles === "<NON_REF>" ||
        altAlleles === "<*>" ||
        altAlleles === ".") {
        return "NONVARIANT"
    } else {
        const alleles = altAlleles.split(",");
        const types = alleles.map(function (a) {
            if (refLength === 1 && a.length === 1) {
                return "SNP"
            } else if ("<NON_REF>" === a) {
                return "NONVARIANT"
            } else if (a.length > refLength && isKnownAlt(a)) {
                return "INSERTION"
            } else if (a.length < refLength && isKnownAlt(a)) {
                return "DELETION"
            } else {
                return "OTHER"
            }
        });
        let type = types[0];
        for (let t of types) {
            if (t !== type) {
                return "MIXED"
            }
        }
        return type
    }
}

function arrayToString(value, delim) {

    if (delim === undefined) delim = ",";

    if (!(Array.isArray(value))) {
        return value
    }
    return value.join(delim)
}

/**
 * Parser for VCF files.
 */

class VcfParser {

    construtor() {
    }

    async parseHeader(dataWrapper, genome) {

        const header = {};

        header.sequenceNames = [];

        // First line must be file format
        let line = await dataWrapper.nextLine();
        if (line.startsWith("##fileformat")) {
            header.version = line.substr(13);
        } else {
            throw new Error("Invalid VCF file: missing fileformat line")
        }

        while ((line = await dataWrapper.nextLine()) !== undefined) {

            if (line.startsWith("#")) {

                let id;
                const values = {};

                if (line.startsWith("##")) {

                    if (line.startsWith("##INFO") || line.startsWith("##FILTER") || line.startsWith("##FORMAT")) {

                        const ltIdx = line.indexOf("<");
                        const gtIdx = line.lastIndexOf(">");

                        if (!(ltIdx > 2 && gtIdx > 0)) {
                            continue
                        }

                        const type = line.substring(2, ltIdx - 1);
                        if (!header[type]) header[type] = {};

                        //##INFO=<ID=AF,Number=A,Type=Float,Description="Allele frequency based on Flow Evaluator observation counts">
                        // ##FILTER=<ID=NOCALL,Description="Generic filter. Filtering details stored in FR info tag.">
                        // ##FORMAT=<ID=AF,Number=A,Type=Float,Description="Allele frequency based on Flow Evaluator observation counts">

                        const tokens = splitStringRespectingQuotes(line.substring(ltIdx + 1, gtIdx - 1), ",");

                        for (let token of tokens) {
                            var kv = token.split("=");
                            if (kv.length > 1) {
                                if (kv[0] === "ID") {
                                    id = kv[1];
                                } else {
                                    values[kv[0]] = kv[1];
                                }
                            }
                        }

                        if (id) {
                            header[type][id] = values;
                        }
                    } else if (line.startsWith("##contig") && genome) {
                        const idx1 = line.indexOf("<ID=");
                        let idx2 = line.indexOf(",", idx1);
                        if (idx2 == -1) {
                            idx2 = line.indexOf(">", idx1);
                        }
                        const chr = line.substring(idx1 + 4, idx2);
                        header.sequenceNames.push(chr);
                    } else ;
                } else if (line.startsWith("#CHROM")) {
                    const tokens = line.split("\t");
                    if (tokens.length > 8) {
                        // Map of sample name -> index
                        header.sampleNameMap = new Map();
                        for (let j = 9; j < tokens.length; j++) {
                            header.sampleNameMap.set(tokens[j], j - 9);
                        }
                    }
                }

            } else {
                break
            }
        }

        this.header = header;  // Will need to intrepret genotypes and info field

        return header
    }


    /**
     * Parse data as a collection of Variant objects.
     *
     * @param data
     * @returns {Array}
     */
    async parseFeatures(dataWrapper) {

        const allFeatures = [];
        const sampleNames = this.header.sampleNameMap ? Array.from(this.header.sampleNameMap.keys()) : undefined;
        const nExpectedColumns = 8 + (sampleNames ? sampleNames.length + 1 : 0);
        let line;
        while ((line = await dataWrapper.nextLine()) !== undefined) {
            if (line && !line.startsWith("#")) {

                const tokens = line.trim().split("\t");
                if (tokens.length === nExpectedColumns) {
                    const variant = new Variant(tokens);
                    variant.header = this.header;       // Keep a pointer to the header to interpret fields for popup text
                    //variant.line = line              // Uncomment for debugging
                    allFeatures.push(variant);

                    if (tokens.length > 9) {
                        //example...	GT	0|0	0|0	0|0	0|0	0|0	0|0	0|0	0|0	0|0	0|0	0|0
                        //example...    GT:DR:DV	./.:.:11

                        const formatFields = extractFormatFields(tokens[8].split(":"));

                        variant.calls = [];
                        for (let index = 9; index < tokens.length; index++) {
                            const sample = sampleNames[index-9];
                            const token = tokens[index];
                            const call = new Call({formatFields, sample, token});
                            variant.calls.push(call);

                        }

                        // If this is a structural variant create a complement of this variant for the other end
                        // The test for "SV" is not comprehensive, there is not yet a standard for this
                        if (variant.info && variant.info.CHR2 && variant.info.END) {
                            allFeatures.push(svComplement(variant));
                        }
                    }
                }
            }
        }
        return allFeatures

    }
}

function extractFormatFields(tokens) {

    const callFields = {
        genotypeIndex: -1,
        fields: tokens
    };
    for (let i = 0; i < tokens.length; i++) {
        if ("GT" === tokens[i]) {
            callFields.genotypeIndex = i;
        }
    }
    return callFields
}

function svComplement(v) {

    return new SVComplement(v)

}

const MIN_EXPONENT$1 = Math.log10(Number.MIN_VALUE);

/**
 * Parser for IGV desktop GWAS files.  See http://software.broadinstitute.org/software/igv/GWAS
 */
class GWASParser {

    constructor(config) {
        // Defaults - can be overriden by header
        this.config = config;
        if (config.columns) {
            if (config.columns.chromosome === undefined ||
                config.columns.position === undefined ||
                config.columns.value === undefined) {
                throw Error("columns property must define chrCol, posCol, and valueCol")
            }
            this.posCol = config.columns.position - 1;
            this.chrCol = config.columns.chromosome - 1;
            this.valueCol = config.columns.value - 1;
        } else {
            // Defaults -- can be overriden in header
            this.posCol = 2;
            this.chrCol = 1;
            this.valueCol = 3;
        }
    }

    async parseHeader(dataWrapper) {
        const headerLine = await dataWrapper.nextLine();
        return this.parseHeaderLine(headerLine)
    }

    parseHeaderLine(headerLine) {
        this.columns = headerLine.split(/\t/);
        if (!this.config.columns) {
            for (let i = 0; i < this.columns.length; i++) {
                const c = this.columns[i].toLowerCase();
                switch (c) {
                    case 'chr':
                    case 'chromosome':
                    case 'chr_id':
                        this.chrCol = i;
                        break
                    case 'bp':
                    case 'pos':
                    case 'position':
                    case 'chr_pos':
                        this.posCol = i;
                        break
                    case 'p':
                    case 'pval':
                    case 'pvalue':
                    case 'p-value':
                    case 'p.value':
                        this.valueCol = i;
                        break
                }
            }
        }
        return this.columns
    }

    async parseFeatures(dataWrapper) {

        const allFeatures = [];
        const headerLine = dataWrapper.nextLine();
        if (!this.columns) {
            this.parseHeaderLine(headerLine);
        }

        let line;

        const parseValue = (valueString) => {
            // Don't try to parse extremely small values
            const idx = valueString.indexOf("E");
            if(idx > 0) {
                const exp = Number.parseInt(valueString.substring(idx + 1));
                if (exp < MIN_EXPONENT$1) {
                   return Number.MIN_VALUE;
                }
            }
            return Number(valueString)
        };

        while ((line = dataWrapper.nextLine()) !== undefined) {
            const tokens = line.split(/\t/);
            if (tokens.length === this.columns.length) {
                const posString = tokens[this.posCol];
                if(posString.indexOf(";") > 0 || posString.length == 0 || posString.indexOf('x') > 0) {
                    continue
                }
                const chr = tokens[this.chrCol];
                const value = parseValue(tokens[this.valueCol]);
                const start = parseInt(posString) - 1;
                const end = start + 1;
                allFeatures.push(new GWASFeature({chr, start, end, value, line, columns: this.columns}));

            }
        }
        return allFeatures
    }

    /**
     * Test first line to see if this is a GWAS file.  Used to determine file format for the case of generic
     * extensions such as "tsv"
     * @param firstLine
     */
    static isGWAS(firstLine) {
        const tokens = firstLine.split('\t');
        if (tokens.length < 5) {
            return false
        }
        const requiredHeaders =
            [
                ['chr', 'chromosome', 'chr_id', 'chrom'],
                ['bp', 'pos', 'position', 'chr_pos', 'chromEnd'],
                ['p', 'pval', 'p-value', 'p.value']
            ];
        for (let h of requiredHeaders) {
            if (!tokens.some(t => h.includes(t.toLowerCase()))) return false
        }
        return true
    }

}

class GWASFeature {

    constructor({chr, start, end, value, line, columns}) {
        this.chr = chr;
        this.start = start;
        this.end = end;
        this.value = value;
        this.line = line;
        this.columns = columns;
    }

    popupData() {
        const tokens = this.line.split(/\t/);
        return this.columns.map(function (c, index) {
            return {name: c, value: tokens[index]}
        })
    }

    getAttributeValue(attrName) {
        const tokens = this.line.split(/\t/);
        for (let i = 0; i < this.columns.length; i++) {
            if (this.columns[i] === attrName) {
                return tokens[i]
            }
        }
        return undefined
    }

}

/**
 *  Define parsers for bed-like files  (.bed, .gff, .vcf, etc).  A parser should implement 2 methods
 *
 *     parseHeader(data) - return an object representing a header or metadata.  Details are format specific
 *
 *     parseFeatures(data) - return an array of features
 *
 */

var aedRegexpNoNamespace = new RegExp("([^:]*)\\(([^)]*)\\)"); // name(type) for AED parsing (namespace undefined)
var aedRegexpNamespace = new RegExp("([^:]*):([^(]*)\\(([^)]*)\\)"); // namespace:name(type) for AED parsing


class AEDParser {

    constructor(config) {
        const decode = config ? config.decode : undefined;
        this.nameField = config ? config.nameField : undefined;
        this.skipRows = 0;   // The number of fixed header rows to skip.  Override for specific types as needed
        if (decode) {
            this.decode = decode;
        } else {
            this.decode = decodeAed;
        }
        this.delimiter = "\t";
    }

    async parseHeader(dataWrapper) {
        let line;
        let header;
        while (line = await dataWrapper.nextLine()) {
            if (line.startsWith("track") || line.startsWith("#") || line.startsWith("browser")) {
                if (line.startsWith("track") || line.startsWith("#track")) {
                    let h = parseTrackLine(line);
                    if (header) {
                        Object.assign(header, h);
                    } else {
                        header = h;
                    }
                } else if (line.startsWith("#columns")) {
                    let h = parseColumnsDirective(line);
                    if (header) {
                        Object.assign(header, h);
                    } else {
                        header = h;
                    }
                } else if (line.startsWith("##gff-version 3")) {
                    this.format = "gff3";
                    if (!header) header = {};
                    header["format"] = "gff3";
                }
            } else {
                break
            }
        }
        this.header = header;    // Directives might be needed for parsing lines
        return header
    }

    async parseFeatures(dataWrapper) {

        const allFeatures = [];
        const decode = this.decode;
        const delimiter = this.delimiter || "\t";
        let i = 0;
        let line;
        let wig;

        while ((line = dataWrapper.nextLine()) !== undefined) {
            i++;
            if (i <= this.skipRows || line.startsWith("track") || line.startsWith("#") || line.startsWith("browser")) {
                continue
            }

            let tokens = readTokensAed();
            if (tokens.length < 1) {
                continue
            }

            if (!this.aed) {
                // Store information about the aed header in the parser itself
                // This is done only once - on the first row
                this.aed = parseAedHeaderRow(tokens);
                continue
            }

            const feature = decode.call(this, tokens, wig);
            if (feature) {
                allFeatures.push(feature);
            }
        }

        return allFeatures

        // Double quoted strings can contain newlines in AED
        // "" is an escape for a ".
        // Parse all this, clean it up, split into tokens in a custom way
        function readTokensAed() {
            var tokens = [],
                token = "",
                quotedString = false,
                n,
                c;

            while (line || line === '') {
                for (n = 0; n < line.length; n++) {
                    c = line.charAt(n);
                    if (c === delimiter) {
                        if (!quotedString) {
                            tokens.push(token);
                            token = "";
                        } else {
                            token += c;
                        }
                    } else if (c === "\"") {
                        // Look ahead to the next character
                        if (n + 1 < line.length && line.charAt(n + 1) === "\"") {
                            if (quotedString) {
                                // Turn "" into a single " in the output string
                                token += "\"";
                            }
                            // Skip the next double quote
                            n++;
                        } else {
                            // We know the next character is NOT a double quote, flip our state
                            quotedString = !quotedString;
                        }
                    } else {
                        token += c;
                    }
                }
                // We are at the end of the line
                if (quotedString) {
                    token += '\n'; // Add newline to the token
                    line = nextLine(); // Keep going
                } else {
                    // We can end the loop
                    break
                }
            }
            // Push the last token
            tokens.push(token);
            return tokens
        }
    }
}


function parseAedToken(value) {
    // Example: refseq:accessionNumber(aed:String)
    // refseq - namespace, will be declared later
    // accessionNumber - name of the field
    // aed:String - type of the field
    // The namespace part may be missing
    var match = aedRegexpNamespace.exec(value);
    if (match) {
        return {
            namespace: match[1],
            name: match[2],
            type: match[3]
        }
    }

    match = aedRegexpNoNamespace.exec(value);
    if (match) {
        return {
            namespace: '?',
            name: match[1],
            type: match[2]
        }
    } else {
        throw new Error("Error parsing the header row of AED file - column not in ns:name(ns:type) format")
    }
}

function parseAedHeaderRow(tokens) {
    // First row of AED file defines column names
    // Each header item is an aed token - see parseAedToken
    var aed,
        k,
        token,
        aedToken;

    // Initialize aed section to be filled in
    aed = {
        columns: [ // Information about the namespace, name and type of each column
            // Example entry:
            // { namespace: 'bio', name: 'start', type: 'aed:Integer' }
        ],
        metadata: { // Metadata about the entire AED file
            // Example:
            // {
            //    aed: {
            //       application: { value: "CHaS Browser 3.3.0.139 (r10838)", type: "aed:String" },
            //       created: { value: "2018-01-02T10:20:30.123+01:00", type: "aed:DateTime" },
            //       modified: { value: "2018-03-04T11:22:33.456+01:00", type: "aed:DateTime" },
            //    }
            //    affx: {
            //       ucscGenomeVersion: { value: "hg19", type: "aed:String" }
            //    },
            //    namespace: {
            //       omim: { value: "http://affymetrix.com/ontology/www.ncbi.nlm.nih.gov/omim/", type: "aed:URI" },
            //       affx: { value: "http://affymetrix.com/ontology/", type: "aed:URI" },
            //       refseq: { value: "http://affymetrix.com/ontology/www.ncbi.nlm.nih.gov/RefSeq/", type: "aed:URI" }
            //    }
            // }
        }
    };
    for (k = 0; k < tokens.length; k++) {
        token = tokens[k];
        aedToken = parseAedToken(token);
        aed.columns.push(aedToken);
    }

    return aed
}

function parseTrackLine(line) {

    const properties = {};
    const tokens = line.split(/(?:")([^"]+)(?:")|([^\s"]+)(?=\s+|$)/g);


    // Clean up tokens array
    let curr;
    const tmp = [];
    for (let tk of tokens) {
        if (!tk || tk.trim().length === 0) continue
        if (tk.endsWith("=") > 0) {
            curr = tk;
        } else if (curr) {
            tmp.push(curr + tk);
            curr = undefined;
        } else {
            tmp.push(tk);
        }
    }
    for (let str of tmp) {
        if (!str) return
        var kv = str.split('=', 2);
        if (kv.length === 2) {
            const key = kv[0].trim();
            const value = kv[1].trim();
            properties[key] = value;
        }

    }

    return properties
}

function parseColumnsDirective(line) {

    let properties = {};
    let t1 = line.split(/\s+/);

    if (t1.length === 2) {

        let t2 = t1[1].split(";");

        t2.forEach(function (keyValue) {

            let t = keyValue.split("=");

            if (t[0] === "color") {
                properties.colorColumn = Number.parseInt(t[1]) - 1;
            } else if (t[0] === "thickness") {
                properties.thicknessColumn = Number.parseInt(t[1]) - 1;
            }
        });
    }

    return properties
}

/**
 * AED file feature.
 *
 * @param aed link to the AED file object containing file-level metadata and column descriptors
 * @param allColumns All columns as parsed from the AED
 *
 * Other values are parsed one by one
 */
function AedFeature(aed, allColumns) {
    var token, aedColumn, aedColumns = aed.columns;

    // Link to AED file (for metadata)
    this.aed = aed;

    // Unparsed columns from AED file
    this.allColumns = allColumns;

    // Prepare space for the parsed values
    this.chr = null;
    this.start = null;
    this.end = null;
    this.score = 1000;
    this.strand = '.';
    this.cdStart = null;
    this.cdEnd = null;
    this.name = null;
    this.color = null;

    for (let i = 0; i < allColumns.length; i++) {
        token = allColumns[i];
        if (!token) {
            // Skip empty fields
            continue
        }
        aedColumn = aedColumns[i];
        if (aedColumn.type === 'aed:Integer') {
            token = parseInt(token);
        }
        var arr = [];
        if (aedColumn.namespace.length > 0) {
            for (let j = 0; j < aedColumn.namespace.length; j++) {
                arr.push(aedColumn.namespace.charCodeAt(j));
            }
        }
        if (aedColumn.namespace.trim() === 'bio') {
            if (aedColumn.name === 'sequence') {
                this.chr = token;
            } else if (aedColumn.name === 'start') {
                this.start = token;
            } else if (aedColumn.name === 'end') {
                this.end = token;
            } else if (aedColumn.name === 'cdsMin') {
                this.cdStart = token;
            } else if (aedColumn.name === 'cdsMax') {
                this.cdEnd = token;
            } else if (aedColumn.name === 'strand') {
                this.strand = token;
            }
        } else if (aedColumn.namespace === 'aed') {
            if (aedColumn.name === 'name') {
                this.name = token;
            }
        } else if (aedColumn.namespace === 'style') {
            if (aedColumn.name === 'color') {
                this.color = IGVColor.createColorString(token);
            }
        }
    }
}

AedFeature.prototype.popupData = function () {
    var data = [],
        aed = this.aed;
    // Just dump everything we have for now
    for (var i = 0; i < this.allColumns.length; i++) {
        var featureValue = this.allColumns[i];
        var name = aed.columns[i].name;
        // Skip columns that are not interesting - you know the sequence, and you can see color
        if (name !== 'sequence' && name !== 'color') {
            if (featureValue) {
                data.push({name: name, value: featureValue});
            }
        }
    }
    return data
};

/**
 * Decode the AED file format
 * @param tokens
 * @param ignore
 * @returns decoded feature, or null if this is not a valid record
 */
function decodeAed(tokens, ignore) {
    var name, value, token,
        nonEmptyTokens = 0,
        aedColumns = this.aed.columns,
        aedColumn,
        aedKey,
        i;

    // Each aed row must match the exact number of columns or we skip it
    if (tokens.length !== aedColumns.length) {
        return undefined
    }

    for (i = 0; i < tokens.length; i++) {
        aedColumn = aedColumns[i];
        token = tokens[i];
        if (token !== '') {
            nonEmptyTokens++;
        }
        if (aedColumn.name === 'name' && aedColumn.namespace === 'aed') {
            name = token;
        } else if (aedColumn.name === 'value' && aedColumn.namespace === 'aed') {
            value = token;
        }
    }

    if (nonEmptyTokens === 2 && name && value) {
        // Special row that defines metadata for the entire file
        aedKey = parseAedToken(name);
        // Store in the metadata section
        if (!this.aed.metadata[aedKey.namespace]) {
            this.aed.metadata[aedKey.namespace] = {};
        }
        if (!this.aed.metadata[aedKey.namespace][aedKey.name]) {
            this.aed.metadata[aedKey.namespace][aedKey.name] = {
                type: aedKey.type,
                value: value
            };
        }
        // Ignore this value
        return undefined
    }

    var feature = new AedFeature(this.aed, tokens);

    if (!feature.chr || (!feature.start && feature.start !== 0) || !feature.end) {
        return undefined
    }

    return feature
}

function optimizeChunks(chunks, lowest) {

    if (chunks.length === 0) return chunks

    chunks.sort(function (c0, c1) {

        const dif = c0.minv.block - c1.minv.block;
        if (dif !== 0) {
            return dif
        } else {
            return c0.minv.offset - c1.minv.offset
        }
    });

    if (chunks.length <= 1) {
        return chunks
    }

    // console.log("Before trimming " + chunks.length)
    // for (let c of chunks) {
    //     console.log(`${c.minv.block} ${c.minv.offset}  -  ${c.maxv.block} ${c.maxv.offset}`)
    // }

    if (lowest) {
        chunks = chunks.filter(c => c.maxv.isGreaterThan(lowest));
    }

    // console.log("Before merging " + chunks.length)
    // for (let c of chunks) {
    //     console.log(`${c.minv.block} ${c.minv.offset}  -  ${c.maxv.block} ${c.maxv.offset}`)
    // }

    const mergedChunks = [];
    let lastChunk;
    for (let chunk of chunks) {

        if (!lastChunk) {
            mergedChunks.push(chunk);
            lastChunk = chunk;
        } else {
            if (canMerge(lastChunk, chunk)) {
                if (chunk.maxv.isGreaterThan(lastChunk.maxv)) {
                    lastChunk.maxv = chunk.maxv;
                }
            } else {
                mergedChunks.push(chunk);
                lastChunk = chunk;
            }
        }
    }

    // console.log("After merging " + mergedChunks.length)
    // for (let c of mergedChunks) {
    //     console.log(`${c.minv.block} ${c.minv.offset}  -  ${c.maxv.block} ${c.maxv.offset}`)
    // }

    return mergedChunks
}


/**
 * Merge 2 blocks if the file position gap between them is < 16 kb, and the total size is < ~5 mb
 * @param chunk1
 * @param chunk2
 * @returns {boolean|boolean}
 */
function canMerge(chunk1, chunk2) {
    const gap = chunk2.minv.block - chunk1.maxv.block;
    const sizeEstimate = chunk2.maxv.block - chunk1.minv.block;
    return sizeEstimate < 5000000 && gap < 65000
    
}

// Represents a CSI Bam or Tabix index

const CSI1_MAGIC$1 = 21582659; // CSI\1
const CSI2_MAGIC$1 = 38359875; // CSI\2

async function parseCsiIndex(arrayBuffer) {

    const idx = new CSIIndex();
    idx.parse(arrayBuffer);
    return idx
}

class CSIIndex {

    constructor() {
        this.tabix = true;  // => i.e. not a tribble index.   This is important, if obtuse
    }

    parse(arrayBuffer) {
        const parser = new BinaryParser$1(new DataView(arrayBuffer));

        const magic = parser.getInt();

        if (magic !== CSI1_MAGIC$1) {
            if (magic === CSI2_MAGIC$1) {
                throw Error("CSI version 2 is not supported.  Please enter an issue at https://github.com/igvteam/igv.js")
            } else {
                throw Error("Not a CSI index")
            }
        }

        this.indices = [];
        this.blockMin = Number.MAX_SAFE_INTEGER;
        this.lastBlockPosition = [];
        this.sequenceIndexMap = {};

        this.minShift = parser.getInt();
        this.depth = parser.getInt();
        const lAux = parser.getInt();
        const seqNames = [];
        let bmax = 0;

        if (lAux >= 28) {
            // Tabix header parameters aren't used, but they must be read to advance the pointer
            parser.getInt();
            parser.getInt();
            parser.getInt();
            parser.getInt();
            parser.getInt();
            parser.getInt();
            const l_nm = parser.getInt();
            const nameEndPos = parser.position + l_nm;
            let i = 0;
            while (parser.position < nameEndPos) {
                let seq_name = parser.getString();
                this.sequenceIndexMap[seq_name] = i;
                seqNames[i] = seq_name;
                i++;
            }
        }

        const MAX_BIN = this.bin_limit() + 1;
        const nref = parser.getInt();
        for (let ref = 0; ref < nref; ref++) {
            const binIndex = [];
            const loffset = [];
            const nbin = parser.getInt();
            for (let b = 0; b < nbin; b++) {

                const binNumber = parser.getInt();
                loffset[binNumber] = parser.getVPointer();

                if (binNumber > MAX_BIN) {
                    // This is a psuedo bin, not used but we have to consume the bytes
                    parser.getInt(); // # of chunks for this bin
                    parser.getVPointer();   // unmapped beg
                    parser.getVPointer();   // unmapped end
                    parser.getLong();
                    parser.getLong();

                } else {

                    binIndex[binNumber] = [];
                    const nchnk = parser.getInt(); // # of chunks for this bin

                    for (let i = 0; i < nchnk; i++) {
                        const cs = parser.getVPointer();    //chunk_beg
                        const ce = parser.getVPointer();    //chunk_end
                        if (cs && ce) {
                            if (cs.block < this.blockMin) {
                                this.blockMin = cs.block;    // Block containing first alignment
                            }
                            if (ce.block > bmax) {
                                bmax = ce.block;
                            }
                            binIndex[binNumber].push([cs, ce]);
                        }
                    }
                }
            }

            if (nbin > 0) {
                this.indices[ref] = {
                    binIndex: binIndex,
                    loffset: loffset
                };
            }
        }
        this.lastBlockPosition = bmax;
    }

    get sequenceNames() {
        return Object.keys(this.sequenceIndexMap)
    }

    /**
     * Fetch blocks for a particular genomic range.  This method is public so it can be unit-tested.
     *
     * @param refId  the sequence dictionary id of the chromosome
     * @param min  genomic start position
     * @param max  genomic end position
     * @param return an array of {minv: {filePointer, offset}, {maxv: {filePointer, offset}}
     */
    chunksForRange(refId, min, max) {

        const ba = this.indices[refId];
        if (!ba) {
            return []
        } else {
            const overlappingBins = this.reg2bins(min, max);        // List of bin #s that overlap min, max
            if (overlappingBins.length == 0) return []

            const chunks = [];
            // Find chunks in overlapping bins.  Leaf bins (< 4681) are not pruned
            for (let binRange of overlappingBins) {
                for (let bin = binRange[0]; bin <= binRange[1]; bin++) {
                    if (ba.binIndex[bin]) {
                        const binChunks = ba.binIndex[bin];
                        for (let c of binChunks) {
                            const cs = c[0];
                            const ce = c[1];
                            chunks.push({minv: cs, maxv: ce, bin: bin});
                        }
                    }
                }
            }

            // Find from the lowest bin level
            let bin = overlappingBins[this.depth][0];
            do {
                const target = ba.binIndex[bin];
                if (target) {
                    break
                }
                const firstBin = (this.getParentBin(bin) << 3) + 1;
                if (bin > firstBin) {
                    bin--;
                } else {
                    bin = this.getParentBin(bin);
                }
            } while (bin != 0)
            
            const lowestOffset = ba.loffset[bin];
            
            return optimizeChunks(chunks, lowestOffset)
        }
    }

    getParentBin(bin) {
        if (bin == 0) {
            return 0;
        }
        return (bin - 1) >> 3;
    }

    // reg2bins implementation adapted from GMOD/tabix-js  https://github.com/GMOD/tabix-js/blob/master/src/csi.ts
    reg2bins(beg, end) {
        beg -= 1; // < convert to 1-based closed
        if (beg < 1) beg = 1;
        if (end > 2 ** 34) end = 2 ** 34; // 17 GiB ought to be enough for anybody
        end -= 1;
        let l = 0;
        let t = 0;
        let s = this.minShift + this.depth * 3;
        const bins = [];
        for (; l <= this.depth; s -= 3, t += (1 << l * 3), l += 1) {
            const b = t + (beg >> s);
            const e = t + (end >> s);
            //
            // ITS NOT CLEAR WHERE THIS TEST CAME FROM,  but maxBinNumber is never set, and its not clear what it represents.
            // if (e - b + bins.length > this.maxBinNumber)
            //     throw new Error(
            //         `query ${beg}-${end} is too large for current binning scheme (shift ${this.minShift}, depth ${this.depth}), try a smaller query or a coarser index binning scheme`,
            //     )
            //
            bins.push([b, e]);
        }
        return bins
    }


    bin_limit() {
        return ((1 << (this.depth + 1) * 3) - 1) / 7
    }

}

// Represents a BAM or Tabix index.

const BAI_MAGIC$1 = 21578050;
const TABIX_MAGIC$1 = 21578324;


async function parseBamIndex(arrayBuffer) {
    const index = new BamIndex();
    await index.parse(arrayBuffer, false);
    return index
}

async function parseTabixIndex(arrayBuffer) {
    const index = new BamIndex();
    await index.parse(arrayBuffer, true);
    return index
}

class BamIndex {

    constructor() {

    }

    async parse(arrayBuffer, tabix) {

        const indices = [];
        let blockMin = Number.MAX_SAFE_INTEGER;
        let blockMax = 0;

        const parser = new BinaryParser$1(new DataView(arrayBuffer));
        const magic = parser.getInt();
        const sequenceIndexMap = {};
        if (magic === BAI_MAGIC$1 || (tabix && magic === TABIX_MAGIC$1)) {

            const nref = parser.getInt();
            if (tabix) {
                // Tabix header parameters aren't used, but they must be read to advance the pointer
                parser.getInt();
                parser.getInt();
                parser.getInt();
                parser.getInt();
                parser.getInt();
                parser.getInt();
                parser.getInt();

                for (let i = 0; i < nref; i++) {
                    let seq_name = parser.getString();
                    sequenceIndexMap[seq_name] = i;
                }
            }

            // Loop through sequences
            for (let ref = 0; ref < nref; ref++) {

                const binIndex = {};
                const linearIndex = [];
                const nbin = parser.getInt();

                for (let b = 0; b < nbin; b++) {
                    const binNumber = parser.getInt();
                    if (binNumber === 37450) {
                        // This is a psuedo bin, not used but we have to consume the bytes
                        parser.getInt(); // # of chunks for this bin
                        parser.getVPointer();   // unmapped beg
                        parser.getVPointer();   // unmapped end
                        parser.getLong();
                        parser.getLong();

                    } else {

                        binIndex[binNumber] = [];
                        const nchnk = parser.getInt(); // # of chunks for this bin

                        for (let i = 0; i < nchnk; i++) {
                            const cs = parser.getVPointer();    //chunk_beg
                            const ce = parser.getVPointer();    //chunk_end
                            if (cs && ce) {
                                if (cs.block < blockMin) {
                                    blockMin = cs.block;    // Block containing first alignment
                                }
                                if (ce.block > blockMax) {
                                    blockMax = ce.block;
                                }
                                binIndex[binNumber].push([cs, ce]);
                            }
                        }
                    }
                }

                const nintv = parser.getInt();
                for (let i = 0; i < nintv; i++) {
                    const cs = parser.getVPointer();
                    linearIndex.push(cs);   // Might be null
                }

                if (nbin > 0) {
                    indices[ref] = {
                        binIndex: binIndex,
                        linearIndex: linearIndex
                    };
                }
            }

            this.firstBlockPosition = blockMin;
            this.lastBlockPosition = blockMax;
            this.indices = indices;
            this.sequenceIndexMap = sequenceIndexMap;
            this.tabix = tabix;

        } else {
            throw new Error(indexURL + " is not a " + (tabix ? "tabix" : "bai") + " file")
        }


    }

    get sequenceNames() {
        return Object.keys(this.sequenceIndexMap)
    }

    /**
     * Fetch chunks for a particular genomic range.  This method is public so it can be unit-tested.
     *
     * @param refId  the sequence dictionary index of the chromosome
     * @param min  genomic start position
     * @param max  genomic end position
     * @param return an array of objects representing chunks (file spans) {minv: {block, offset}, {maxv: {block, offset}}
     */
    chunksForRange(refId, min, max) {

        const bam = this;
        const ba = bam.indices[refId];

        if (!ba) {
            return []
        } else {
            const overlappingBins = reg2bins(min, max);        // List of bin #s that overlap min, max

            //console.log("bin ranges")
            //for(let b of overlappingBins) {
            //    console.log(`${b[0]} - ${b[1]}`)
            //}

            const chunks = [];
            // Find chunks in overlapping bins.  Leaf bins (< 4681) are not pruned
            for (let binRange of overlappingBins) {
                for (let bin = binRange[0]; bin <= binRange[1]; bin++) {
                    if (ba.binIndex[bin]) {
                        const binChunks = ba.binIndex[bin];
                        for (let c of binChunks) {
                            const cs = c[0];
                            const ce = c[1];
                            chunks.push({minv: cs, maxv: ce});
                        }
                    }
                }
            }

            // Use the linear index to find minimum file position of chunks that could contain alignments in the region
            const nintv = ba.linearIndex.length;

            let lowest;
            const minLin = Math.min(min >> 14, nintv - 1);    // i.e. min / 16384
            const maxLin = Math.min(max >> 14, nintv - 1);
            for (let i = minLin; i <= maxLin; i++) {
                const vp = ba.linearIndex[i];
                if (vp) {
                    lowest = vp;       // lowest file offset that contains alignments overlapping (min, max)
                    break
                }
            }

            return optimizeChunks(chunks, lowest)
        }
    }
}



/**
 * Calculate the list of bins that overlap with region [beg, end]
 *
 */
function reg2bins(beg, end) {
    const list = [];
    if (end >= 1 << 29) end = 1 << 29;
    --end;
    list.push([0, 0]);
    list.push([1 + (beg >> 26), 1 + (end >> 26)]);
    list.push([9 + (beg >> 23), 9 + (end >> 23)]);
    list.push([73 + (beg >> 20), 73 + (end >> 20)]);
    list.push([585 + (beg >> 17), 585 + (end >> 17)]);
    list.push([4681 + (beg >> 14), 4681 + (end >> 14)]);

    // for (k = 1 + (beg >> 26); k <= 1 + (end >> 26); ++k) list.push(k);
    // for (k = 9 + (beg >> 23); k <= 9 + (end >> 23); ++k) list.push(k);
    // for (k = 73 + (beg >> 20); k <= 73 + (end >> 20); ++k) list.push(k);
    // for (k = 585 + (beg >> 17); k <= 585 + (end >> 17); ++k) list.push(k);
    // for (k = 4681 + (beg >> 14); k <= 4681 + (end >> 14); ++k) list.push(k);
    return list
}

async function parseTribbleIndex(arrayBuffer) {

    const index = new TribbleIndex();
    index.parse(arrayBuffer);
    return index
}

class TribbleIndex {

    constructor() {
        this.tribble = true;
    }

    async parse(arrayBuffer) {

        let blockMax = 0;
        this.chrIndex = {};
        this.lastBlockPosition = [];
        const parser = new BinaryParser$1(new DataView(arrayBuffer));
        readHeader(parser);

        let nChrs = parser.getInt();
        while (nChrs-- > 0) {

            // todo -- support interval tree index, we're assuming its a linear index

            let chr = parser.getString();
            const binWidth = parser.getInt();
            const nBins = parser.getInt();
            const longestFeature = parser.getInt();
            parser.getInt() > 0;
            parser.getInt();

            // note the code below accounts for > 60% of the total time to read an index
            let pos = parser.getLong();
            const blocks = [];
            for (let binNumber = 0; binNumber < nBins; binNumber++) {
                const nextPos = parser.getLong();
                blocks.push({min: pos, max: nextPos}); //        {position: pos, size: size});
                pos = nextPos;
                if (nextPos > blockMax) {
                    blockMax = nextPos;
                }
            }
            this.chrIndex[chr] = {chr: chr, blocks: blocks, longestFeature: longestFeature, binWidth: binWidth};
        }

        this.lastBlockPosition = blockMax;


        /**
         * Read the header .   Data here is not used in igv.js but we need to read it to advance the pointer.
         * @param parser
         */
        function readHeader(parser) {

            parser.getInt();     //   view._getInt32(offset += 32, true);
            parser.getInt();
            const version = parser.getInt();
            parser.getString();
            parser.getLong();
            parser.getLong();
            parser.getString();
            parser.getInt();
            if (version >= 3) {
                let nProperties = parser.getInt();
                while (nProperties-- > 0) {
                    parser.getString();
                    parser.getString();
                }
            }
        }
    }

    get sequenceNames() {
        return Object.keys(this.chrIndex)
    }


    /**
     * Fetch blocks for a particular genomic range.
     *
     * @param queryChr
     * @param min  genomic start position
     * @param max  genomic end position
     */
    chunksForRange(queryChr, min, max) { //function (refId, min, max) {

        const chrIdx = this.chrIndex[queryChr];

        if (chrIdx) {
            const blocks = chrIdx.blocks;
            const longestFeature = chrIdx.longestFeature;
            const binWidth = chrIdx.binWidth;
            const adjustedPosition = Math.max(min - longestFeature, 0);
            const startBinNumber = Math.floor(adjustedPosition / binWidth);

            if (startBinNumber >= blocks.length) // are we off the end of the bin list, so return nothing
                return []
            else {
                const endBinNumber = Math.min(Math.floor((max - 1) / binWidth), blocks.length - 1);

                // By definition blocks are adjacent in the file for the liner index.  Combine them into one merged block
                const startPos = blocks[startBinNumber].min;
                const endPos = blocks[endBinNumber].max;
                const size = endPos - startPos;
                if (size === 0) {
                    return []
                } else {
                    const mergedBlock = {minv: {block: startPos, offset: 0}, maxv: {block: endPos, offset: 0}};
                    return [mergedBlock]
                }
            }
        } else {
            return undefined
        }
    }
}

const CSI1_MAGIC = 21582659; // CSI\1
const CSI2_MAGIC = 38359875; // CSI\2
const BAI_MAGIC = 21578050;
const TABIX_MAGIC = 21578324;
const TRIBBLE_MAGIC = 1480870228;   //  byte[]{'T', 'I', 'D', 'X'};

/**
 * @param indexURL
 * @param config
 * @param tabix
 *
 */
async function loadIndex(indexURL, config) {

    let arrayBuffer = await igvxhr$1.loadArrayBuffer(indexURL, buildOptions(config));
    let dv = new DataView(arrayBuffer);

    // Some indexs are bgzipped, specifically tabix, and csi.  Bam (bai) are not.  Tribble is usually not.
    // Check first 2 bytes of file for gzip magic number, and inflate if neccessary
    if (dv.getUint8(0) === 0x1f && dv.getUint8(1) === 0x8b) {    // gzipped
        const inflate = unbgzf$1(arrayBuffer);
        arrayBuffer = inflate.buffer;
        dv = new DataView(arrayBuffer);
    }

    const magic = dv.getInt32(0, true);
    switch (magic) {
        case BAI_MAGIC:
            return parseBamIndex(arrayBuffer)
        case TABIX_MAGIC:
            return parseTabixIndex(arrayBuffer)
        case CSI1_MAGIC:
            return parseCsiIndex(arrayBuffer)
        case TRIBBLE_MAGIC:
            return parseTribbleIndex(arrayBuffer)
        case CSI2_MAGIC:
            throw Error("CSI version 2 is not supported.")
        default:
            throw Error(`Unrecognized index type: ${indexURL}`)
    }
}

function getDataWrapper$1(data) {

    if (typeof (data) == 'string' || data instanceof String) {
        return new StringDataWrapper$1(data)
    } else {
        return new ByteArrayDataWrapper$1(data)
    }
}


// Data might be a string, or an UInt8Array
class StringDataWrapper$1 {

    constructor(string) {
        this.data = string;
        this.ptr = 0;
    }

    nextLine() {
        var start = this.ptr,
            idx = this.data.indexOf('\n', start),
            data = this.data;

        if (idx > 0) {
            this.ptr = idx + 1;   // Advance pointer for next line
            if (idx > start && data.charAt(idx - 1) === '\r') {
                // Trim CR manually in CR/LF sequence
                return data.substring(start, idx - 1)
            }
            return data.substring(start, idx)
        } else {
            var length = data.length;
            this.ptr = length;
            // Return undefined only at the very end of the data
            return (start >= length) ? undefined : data.substring(start)
        }
    }
}

class ByteArrayDataWrapper$1 {

    /**
     *
     * @param {Uint8Array} array
     */
    constructor(array) {
        this.data = array;
        this.length = this.data.length;
        this.ptr = 0;
    }

    /**
     * Decode the next line as a UTF-8 string.  From: https://gist.github.com/Yaffle/5458286
     * @returns {undefined|string}
     */
    nextLine() {
        if(this.ptr >= this.data.length) {
            return undefined
        }
        let i = this.ptr;
        const octets = this.data;
        let string = "";
        let maybeCR = false;
        while (i < octets.length) {
            var octet = octets[i];
            var bytesNeeded = 0;
            var codePoint = 0;
            if (octet <= 0x7F) {
                bytesNeeded = 0;
                codePoint = octet & 0xFF;
            } else if (octet <= 0xDF) {
                bytesNeeded = 1;
                codePoint = octet & 0x1F;
            } else if (octet <= 0xEF) {
                bytesNeeded = 2;
                codePoint = octet & 0x0F;
            } else if (octet <= 0xF4) {
                bytesNeeded = 3;
                codePoint = octet & 0x07;
            }
            if (octets.length - i - bytesNeeded > 0) {
                var k = 0;
                while (k < bytesNeeded) {
                    octet = octets[i + k + 1];
                    codePoint = (codePoint << 6) | (octet & 0x3F);
                    k += 1;
                }
            } else {
                codePoint = 0xFFFD;
                bytesNeeded = octets.length - i;
            }
            i += bytesNeeded + 1;

            const c = String.fromCodePoint(codePoint);
            if(c === '\r') {
                maybeCR = true;
            } else if( c === '\n') {
                break
            } else {
                if(maybeCR) {
                    // Add cr's unless immediately followed by a line feed.
                    string += '\r';
                    maybeCR = false;
                }
                string += c;
            }
        }
        this.ptr = i;
        return string
    }


}

/**
 * Class to iterate line-by-line over a BGZipped text file.  This class is useful for iterating from the start of
 * the file.  Not useful for indexed queries.
 */

class BGZLineReader {

    constructor(config) {
        this.config = config;
        this.filePtr = 0;
        this.bufferPtr = 0;
        this.buffer;
    }

    async nextLine() {

        let result = undefined;

        try {
            while (true) {
                const length = this.buffer ? this.buffer.length : 0;
                while (this.bufferPtr < length) {
                    const c = String.fromCharCode(this.buffer[this.bufferPtr++]);
                    if (c === '\r') continue
                    if (c === '\n') {
                        return result
                    }
                    result = result ? result + c : c;
                }
                if (this.eof) {
                    return result
                } else {
                    await this.readNextBlock();
                }
            }
        } catch (e) {
            console.warn(e);
            this.eof = true;
            return result
        }
    }

    async readNextBlock() {

        const bsizeOptions = buildOptions(this.config, {
            range: {
                start: this.filePtr,
                size: 26
            }
        });
        const abuffer = await igvxhr$1.loadArrayBuffer(this.config.url, bsizeOptions);
        const bufferSize = bgzBlockSize$1(abuffer);

        if (bufferSize === 0) {
            this.eof = true;
            this.buffer = undefined;
        } else {

            const options = buildOptions(this.config, {range: {start: this.filePtr, size: bufferSize}});
            const data = await igvxhr$1.loadArrayBuffer(this.config.url, options);
            if (data.byteLength < bufferSize) {
                this.eof = true; // Assumption
            }
            this.buffer = unbgzf$1(data);
            if(this.buffer.byteLength == 0) {
                this.eof = true;
            }
            this.bufferPtr = 0;
            this.filePtr += data.byteLength; //data.byteLength;
        }
    }

}

function concatenateArrayBuffers(arrayBuffers) {

    if (arrayBuffers.length === 1) {
        return arrayBuffers[0]
    }

    let len = 0;
    for (const b of arrayBuffers) {
        len += b.byteLength;
    }
    const c = new Uint8Array(len);
    let offset = 0;
    for (const b of arrayBuffers) {
        c.set(new Uint8Array(b), offset);
        offset += b.byteLength;
    }
    return c.buffer
}

/**
 * Return the block size for the data buffer.
 * @param data
 * @returns {number}
 */
const bgzBlockSize = (data) => {
    const ba = ArrayBuffer.isView(data) ? data : new Uint8Array(data);
    const bsize = (ba[17] << 8 | ba[16]) + 1;
    return bsize
};

class BGZBlockLoader {

    constructor(config) {
        this.config = config;
        this.cacheBlocks = false != config.cacheBlocks;   // Default to true
        this.cache = undefined;
    }

    /**
     * Return inflated data from startBlock through endBlock as an UInt8Array
     *
     * @param minv minimum virtual pointer  {block, offset}
     * @param maxv maximum virtual pointer  {block, offset}
     * @returns {Promise<Uint8Array>}
     */
    async getData(minv, maxv) {

        const startBlock = minv.block;
        const endBlock = maxv.block;
        const skipEnd = maxv.offset === 0;

        const blocks = await this.getInflatedBlocks(startBlock, endBlock, skipEnd);
        if (blocks.length === 1) {
            return blocks[0]
        }

        let len = 0;
        for (const b of blocks) {
            len += b.byteLength;
        }
        const c = new Uint8Array(len);
        let offset = 0;
        for (const b of blocks) {
            c.set(b, offset);
            offset += b.byteLength;
        }
        return c
    }

    /**
     * Return the inflated data for the specified blocks as an array of Uint8Arrays.  This method is public so
     * it can be unit tested. *
     * @param startBlock
     * @param endBlock
     * @returns {Promise<*[Uint8Array]>}
     */
    async getInflatedBlocks(startBlock, endBlock, skipEnd) {

        if (!this.cacheBlocks) {
            const buffer = await this.loadBLockData(startBlock, endBlock, {skipEnd});
            return inflateBlocks(buffer)
        } else {

            const c = this.cache;
            if (c &&
                c.startBlock <= startBlock &&
                (c.endBlock >= endBlock || skipEnd && c.nextEndBlock === endBlock)) {
                //console.log("Complete overlap")
                const startOffset = startBlock - c.startBlock;
                const endOffset = endBlock - c.startBlock;
                return inflateBlocks(c.buffer, startOffset, endOffset)
                // Don't update cache, still valid
            } else {

                let buffer;
                if (!c || (c.startBlock > endBlock || c.endBlock < startBlock)) {
                    // no overlap with cache
                    buffer = await this.loadBLockData(startBlock, endBlock, {skipEnd});
                } else {

                    //console.log("Some overlap")
                    const arrayBuffers = [];

                    // Load blocks preceding cache start, if any
                    if (startBlock < c.startBlock) {
                        // load first blocks
                        const startBuffer = await this.loadBLockData(startBlock, c.startBlock, {skipEnd: true});
                        arrayBuffers.push(startBuffer);
                    }

                    // Slice cached buffer as needed
                    let cachedBuffer;
                    if (startBlock <= c.startBlock && endBlock >= c.endBlock) {
                        cachedBuffer = c.buffer;
                    } else {
                        const start = Math.max(0, startBlock - c.startBlock);
                        let end;
                        if (endBlock >= c.endBlock) {
                            end = c.buffer.byteLength;
                        } else {
                            // We need to find the byte position of the end of "endBlock"
                            const boundaries = findBlockBoundaries(c.buffer);
                            for (let i = 0; i < boundaries.length - 1; i++) {
                                if (c.startBlock + boundaries[i] === endBlock) {
                                    end = boundaries[i + 1];
                                    break
                                }
                            }
                            // Do something if end not found
                        }
                        cachedBuffer = c.buffer.slice(start, end);
                    }
                    arrayBuffers.push(cachedBuffer);

                    // Load end blocks, if any
                    if (endBlock > c.endBlock) {
                        const endBuffer = await this.loadBLockData(c.endBlock, endBlock, {skipStart: true, skipEnd});
                        arrayBuffers.push(endBuffer);
                    }

                    buffer = concatenateArrayBuffers(arrayBuffers);
                }

                // If skipEnd === true we need to find boundary of last block in cache
                let nextEndBlock = endBlock;
                if(skipEnd) {
                    const boundaries = findBlockBoundaries(buffer);
                    endBlock = boundaries[boundaries.length - 1];
                }

                this.cache = {startBlock, endBlock, nextEndBlock, buffer};
                return inflateBlocks(buffer)
            }
        }
    }

    async loadBLockData(startBlock, endBlock, options) {

        const config = this.config;
        const skipStart = options && options.skipStart;
        const skipEnd = options && options.skipEnd;

        // Get size of last block if not skipped
        let lastBlockSize = 0;
        if (!skipEnd) {
            const bsizeOptions = buildOptions(config, {range: {start: endBlock, size: 26}});
            const abuffer = await igvxhr$1.loadArrayBuffer(config.url, bsizeOptions);
            lastBlockSize = bgzBlockSize(abuffer);
        }

        if (skipStart) {
            const bsizeOptions = buildOptions(config, {range: {start: startBlock, size: 26}});
            const abuffer = await igvxhr$1.loadArrayBuffer(config.url, bsizeOptions);
            startBlock += bgzBlockSize(abuffer);
        }

        // Load data for all blocks
        const loadOptions = buildOptions(config, {
            range: {
                start: startBlock,
                size: endBlock + lastBlockSize - startBlock
            }
        });

        //console.log(`${this.config.name}  Loaded ${startBlock} - ${endBlock + lastBlockSize}   (${(endBlock + lastBlockSize - startBlock) / 1000} kb)`)

        return igvxhr$1.loadArrayBuffer(config.url, loadOptions)
    }
}

function findBlockBoundaries(arrayBuffer) {

    const byteLengh = arrayBuffer.byteLength;
    let offset = 0;
    const blockBoundaries = [0];
    while (offset < byteLengh) {
        //console.log("Cache block "  + offset)
        const ba = new Uint8Array(arrayBuffer, offset);
        const bsize = (ba[17] << 8 | ba[16]) + 1;
        offset += bsize;
        if (offset < byteLengh) {
            blockBoundaries.push(offset);
        }
    }
    return blockBoundaries
}


/**
 * Inflate compressed blocks within the data buffer*
 * @param data
 * @param startBlock - optional file location for start block.  Default == 0
 * @param endBlock - optional file location for last block to decompress.
 * @returns {*[]}
 */
function inflateBlocks(data, startBlock, endBlock) {

    startBlock = startBlock || 0;

    const oBlockList = [];
    let ptr = startBlock;

    const lim = data.byteLength - 18;
    while (ptr < lim) {
        try {
            //console.log(113873 + ptr)
            const header = new Uint8Array(data, ptr, 18);
            const xlen = (header[11] << 8) | (header[10]);
            const bsize = ((header[17] << 8) | (header[16]));  // Total block size, including header, minus 1
            const start = 12 + xlen + ptr;    // Start of CDATA
            const bytesLeft = data.byteLength - start;
            const cDataSize = bsize - xlen - 18;

            if (bytesLeft < cDataSize || cDataSize <= 0) {
                // This is unexpected.  Throw error?
                break
            }

            const cdata = new Uint8Array(data, start, cDataSize);
            const unc = inflateRaw_1$2(cdata);
            oBlockList.push(unc);

            if (endBlock === ptr) {
                break
            } else {
                // Advance to next block
                ptr += bsize + 1;
            }

        } catch (e) {
            console.error(e);
            break
        }
    }
    return oBlockList
}

const MIN_EXPONENT = Math.log10(Number.MIN_VALUE);

/**
 * Parser for xQTL files.
 *
 * Quantitative trait loci (QTL) are genomic variants that are significantly associated to a measurable phenotype.
 *
 * Currently there is no standard file format for this data. For igv.js the essential data is genomic position of
 * the variant and the phenotype (e.g. a gene for eQtls)
 *
 * From https://github.com/igvteam/igv.js/issues/1833
 * CHR	SNP	BP	P	Phenotype
 * 10	rs146165798	50023438	0.000106	A1CF
 *
 * UCSC (based on GTEX)
 * https://genome.ucsc.edu/cgi-bin/hgTables?db=hg19&hgta_group=regulation&hgta_track=gtexEqtlTissue&hgta_table=gtexEqtlTissueAdiposeSubcut&hgta_doSchema=describe+table+schema
 *
 * bin	590	int(10) unsigned	range	Indexing field to speed chromosome range queries.
 * chrom	chr1	varchar(255)	values	Reference sequence chromosome or scaffold
 * chromStart	701834	int(10) unsigned	range	Start position in chromosome
 * chromEnd	701835	int(10) unsigned	range	End position in chromosome
 * name	rs189800799/RP11-206L10.9	varchar(255)	values	Variant/gene pair
 * score	22	int(10) unsigned	range	Score from 0-1000 (highest probabiliity in cluster * 1000)
 * strand	.	char(1)	values	.
 * thickStart	701834	int(10) unsigned	range	Start position
 * thickEnd	701835	int(10) unsigned	range	End position
 * itemRgb	16752800	int(10) unsigned	range	R,G,B color: red +effect, blue -effect. Bright for high, pale for lower (cutoff effectSize 2.0 RPKM).
 * variant	rs189800799	varchar(255)	values	Variant (rsID or GTEx identifier if none)
 * geneId	ENSG00000237491.4	varchar(255)	values	Target gene identifier
 * gene	RP11-206L10.9	varchar(255)	values	Target gene symbol
 * distance	-12315	int(11)	range	Distance from TSS
 * effectSize	0.229	float	range	Effect size (FPKM)
 * pValue	13.862	float	range	Nominal p-value
 * causalProb	0.022	float	range	Probability variant is in high confidence causal set
 *
 * EMBL Eqtl catalog
 * https://github.com/eQTL-Catalogue/eQTL-Catalogue-resources/blob/master/tabix/Columns.md
 *
 * variant - The variant ID (chromosome_position_ref_alt) e.g. chr19_226776_C_T. Based on GRCh38 coordinates and reference genome. The chromosome, position, ref and alt values should exactly match same fields in the summary statistics file, with 'chr' prefix added to the chromosome number.
 * r2 - Optional imputation quality score from the imputation software, can be replaced with NA if not available.
 * pvalue - Nominal p-value of association between the variant and the molecular trait.
 * molecular_trait_object_id - For phenotypes with multiple correlated alternatives (multiple alternative transcripts or exons within a gene, multple alternative promoters in txrevise, multiple alternative intons in Leafcutter), this defines the level at which the phenotypes were aggregated. Permutation p-values are calculated accross this set of alternatives.
 * molecular_trait_id - ID of the molecular trait used for QTL mapping. Depending on the quantification method used, this can be either a gene id, exon id, transcript id or a txrevise promoter, splicing or 3'end event id. Examples: ENST00000356937, ENSG00000008128.
 * maf - Minor allele frequency within a QTL mapping context (e.g. cell type or tissues within a study).
 * gene_id - Ensembl gene ID of the molecular trait.
 * median_tpm - Median transcripts per million (TPM) expression value of the gene. Can be replaced with NA if not availble (e.g. in microarray studies).
 * beta - Regression coefficient from the linear model.
 * se - Standard error of the beta.
 * an - Total number of alleles. For autosomal variants, this is usually two times the sample size. Conversly, for autosomal variants, sample size is equal to an/2.
 * ac - Count of the alternative allele.
 * ma_samples - Number of samples carrying at least one copy of the minor allele.
 * chromosome - GRCh38 chromosome name of the variant (e.g. 1,2,3 ...,X).
 * position - GRCh38 position of the variant.
 * ref - GRCh38 reference allele.
 * alt - GRCh38 alternative allele (also the effect allele).
 * type - Type of the genetic variant; SNP, INDEL or OTHER.
 * rsid - The dbSNP v151 rsid of the variant. If the same variant has multiple rsids then these should be split over multiple rows so that all of the other values are duplicated.
 *
 */
class QTLParser {

    chrCol = -1
    snpCol = -1
    posCol = -1
    pValueCol = -1
    phenotypeColumn = -1
    delimiter = '\t'

    constructor(config) {
        this.config = config;

        //TODO -- allow specifying column
        // this.pValueField = config.pValueField || "pValue"
        // this.phenotypeField = config.phenotypeField || config.geneField ||  "geneSymbol"
        // this.snpField = config.snpField || "snp"
    }

    async parseHeader(dataWrapper) {

        const config = this.config;
        if (config.delimiter) this.delimiter = config.delimiter;

        const headerLine = await dataWrapper.nextLine();
        const columns = this.parseHeaderLine(headerLine);

        // Config overrides

        if (config.chrColumn) this.chrColumn = config.chrColumn - 1;
        if (config.snpColumn) this.snpColumn = config.snpColumn - 1;
        if (config.posColumn) this.posColumn = config.posColumn - 1;
        if (config.pValueColumn) this.pValueColumn = config.pValueColumn - 1;
        if (config.phenotypeColumn) this.phenotypeColumn = config.phenotypeColumn - 1;

        return columns
    }

    parseHeaderLine(headerLine) {
        this.columns = headerLine.split(this.delimiter);
        for (let i = 0; i < this.columns.length; i++) {
            const c = this.columns[i].toLowerCase();
            switch (c) {
                case 'chr':
                case 'chromosome':
                case 'chr_id':
                case 'chrom':
                    this.chrColumn = i;
                    break
                case 'bp':
                case 'pos':
                case 'position':
                case 'chr_pos':
                case 'chromEnd':
                    this.posColumn = i;
                    break
                case 'p':
                case 'pval':
                case 'pvalue':
                case 'p-value':
                case 'p.value':
                    this.pValueColumn = i;
                    break
                case 'rsid':
                case 'variant':
                case 'snp':
                    this.snpColumn = i;
                    break
                case 'phenotype':
                case 'gene':
                case 'gene_id':
                case 'molecular_trait_id':
                    this.phenotypeColumn = i;
                    break
            }
        }

        // TODO validate
        return this.columns
    }

    async parseFeatures(dataWrapper) {

        const allFeatures = [];
        const headerLine = dataWrapper.nextLine();
        if (!this.columns) {
            this.parseHeaderLine(headerLine);
        }

        let line;

        const parseValue = (valueString) => {
            // Don't try to parse extremely small values
            const idx = valueString.indexOf("E");
            if (idx > 0) {
                const exp = Number.parseInt(valueString.substring(idx + 1));
                if (exp < MIN_EXPONENT) {
                    return Number.MIN_VALUE
                }
            }
            return Number(valueString)
        };

        while ((line = dataWrapper.nextLine()) !== undefined) {
            const tokens = line.split(this.delimiter);
            if (tokens.length === this.columns.length) {
                const posString = tokens[this.posColumn];
                if (posString.indexOf(";") > 0 || posString.length == 0 || posString.indexOf('x') > 0) {
                    continue
                }
                const chr = tokens[this.chrColumn];
                const pValue = parseValue(tokens[this.pValueColumn]);
                const start = parseInt(posString) - 1;
                const end = start + 1;
                const snp = tokens[this.snpColumn];
                const phenotype = tokens[this.phenotypeColumn];
                const qtl = new QTL({chr, start, end, pValue, snp, phenotype}, this.columns, tokens);

                allFeatures.push(qtl);

            }
        }
        return allFeatures
    }

    /**
     * Test first line to see if this is a QTL file.  Used to determine file format for the case of generic
     * extensions such as "tsv"
     * @param firstLine
     */
    static isQTL(firstLine) {
        const tokens = firstLine.split('\t');
        if (tokens.length < 5) {
            return false
        }
        const requiredHeaders =
            [
                ['chr', 'chromosome', 'chr_id', 'chrom'],
                ['bp', 'pos', 'position', 'chr_pos', 'chromEnd'],
                ['p', 'pval', 'p-value', 'p.value'],
                ['rsid', 'variant', 'snp'],
                ['phenotype', 'gene', 'gene_id', 'molecular_trait_id']
            ];
        for (let h of requiredHeaders) {
            if (!tokens.some(t => h.includes(t.toLowerCase()))) return false
        }
        return true
    }
}


class QTL {

    constructor({chr, start, end, pValue, snp, phenotype}, headers, tokens) {
        this.chr = chr;
        this.start = start;
        this.end = end;
        this.pValue = pValue;
        this.snp = snp;
        this.phenotype = phenotype;
        this.headers = headers;
        this.tokens = tokens;
    }

    popupData() {
        const data = [];
        for (let i = 0; i < this.headers.length; i++) {
            data.push({name: this.headers[i], value: this.tokens[i]});
        }
        return data
        // return [
        //     {name: 'chromosome', value: this.chr},
        //     {name: 'position', value: this.start + 1},
        //     {name: 'pValue', value: this.pValue},
        //     {name: 'snp', value: this.snp},
        //     {name: 'phenotype', value: this.phenotype}
        // ]
    }

}

// Conservative estimate of the maximum allowed string length
const MAX_STRING_LENGTH = 500000000;

/**
 * Reader for "bed like" files (tab delimited files with 1 feature per line: bed, gff, vcf, etc)
 *
 * @param config
 * @constructor
 */
class FeatureFileReader {

    sequenceNames

    constructor(config, genome) {

        this.config = config || {};
        this.genome = genome;
        this.indexURL = config.indexURL;
        this.indexed = config.indexed || this.indexURL !== undefined;
        this.queryable = this.indexed;

        if (isFile$1(this.config.url)) {
            this.filename = this.config.url.name;
        } else if (isDataURL(this.config.url)) {
            this.indexed = false;  // by definition
            this.dataURI = config.url;
        } else {
            const uriParts = parseUri$1(this.config.url);
            this.filename = config.filename || uriParts.file;
        }

        this.parser = this.getParser(this.config);

        if (this.config.format === "vcf" && !this.config.indexURL) {
            console.warn("Warning: index file not specified.  The entire vcf file will be loaded.");
        }

    }

    async defaultVisibilityWindow() {
        if (this.config.indexURL) {
            const index = await this.getIndex();
            if (index && index.lastBlockPosition) {
                let gl = 0;
                const s = 10000;
                for (let c of index.sequenceNames) {
                    const chromosome = this.genome.getChromosome(c);
                    if (chromosome) {
                        gl += chromosome.bpLength;
                    }
                }
                return Math.round((gl / index.lastBlockPosition) * s)
            }
        }
    }

    /**
     * Return a promise to load features for the genomic interval
     * @param chr
     * @param start
     * @param end
     */
    async readFeatures(chr, start, end) {

        // insure that header has been loaded
        if (!this.dataURI && !this.header) {
            await this.readHeader();
        }

        let allFeatures;
        const index = await this.getIndex();
        if (index) {
            this.indexed = true;
            allFeatures = await this.loadFeaturesWithIndex(chr, start, end);
        } else if (this.dataURI) {
            this.indexed = false;
            allFeatures = await this.loadFeaturesFromDataURI();
        } else if ("service" === this.config.sourceType) {
            allFeatures = await this.loadFeaturesFromService(chr, start, end);
        } else {
            this.indexed = false;
            allFeatures = await this.loadFeaturesNoIndex();
        }

        allFeatures.sort(function (a, b) {
            if (a.chr === b.chr) {
                return a.start - b.start
            } else {
                return a.chr.localeCompare(b.chr)
            }
        });

        return allFeatures
    }

    async readHeader() {

        if (this.dataURI) {
            await this.loadFeaturesFromDataURI(this.dataURI);
            return this.header
        } else if (this.config.indexURL) {
            const index = await this.getIndex();
            if (!index) {
                // Note - it should be impossible to get here
                throw new Error("Unable to load index: " + this.config.indexURL)
            }
            this.sequenceNames = new Set(index.sequenceNames);

            let dataWrapper;
            if (index.tabix) {
                this._blockLoader = new BGZBlockLoader(this.config);
                dataWrapper = new BGZLineReader(this.config);
            } else {
                // Tribble
                const maxSize = Object.values(index.chrIndex)
                    .flatMap(chr => chr.blocks)
                    .map(block => block.max)
                    .reduce((previous, current) =>
                        Math.min(previous, current), Number.MAX_SAFE_INTEGER);

                const options = buildOptions(this.config, {bgz: index.tabix, range: {start: 0, size: maxSize}});
                const data = await igvxhr$1.loadString(this.config.url, options);
                dataWrapper = getDataWrapper$1(data);
            }

            this.header = await this.parser.parseHeader(dataWrapper);

            return this.header

        } else if ("service" === this.config.sourceType) {
            if (this.config.seqnamesURL) {
                // Side effect, a bit ugly
                const options = buildOptions(this.config, {});
                const seqnameString = await igvxhr$1.loadString(this.config.seqnamesURL, options);
                if (seqnameString) {
                    this.sequenceNames = new Set(seqnameString.split(",").map(sn => sn.trim()).filter(sn => sn));
                }
            }
            if (this.config.headerURL) {
                const options = buildOptions(this.config, {});
                const data = await igvxhr$1.loadString(this.config.headerURL, options);
                const dataWrapper = getDataWrapper$1(data);
                this.header = await this.parser.parseHeader(dataWrapper);  // Cache header, might be needed to parse features
                return this.header
            }

        } else {

            // Non-indexed file, or indexed file without an index
            this.indexed = false;

            let data;

            if (this.config._filecontents) {
                // In rare instances the entire file must be read and decoded to determine the file format.
                // When this occurs the file contents are temporarily stashed to prevent needing to read the file twice
                data = this.config._filecontents;
                delete this.config._filecontents;
            } else {
                // If this is a non-indexed file we will load all features in advance
                const options = buildOptions(this.config);
                data = await igvxhr$1.loadByteArray(this.config.url, options);
            }

            // If the data size is < max string length decode entire string with TextDecoder.  This is much faster
            // than decoding by line
            if (data.length < MAX_STRING_LENGTH) {
                data = new TextDecoder().decode(data);
            }


            let dataWrapper = getDataWrapper$1(data);
            this.header = await this.parser.parseHeader(dataWrapper);

            // Reset data wrapper and parse features
            dataWrapper = getDataWrapper$1(data);
            this.features = await this.parser.parseFeatures(dataWrapper);   // cache features

            // Extract chromosome names from features
            this.sequenceNames = new Set(this.features.map(f => f.chr));

            return this.header
        }

    }


    getParser(config) {

        switch (config.format) {
            case "vcf":
                return new VcfParser(config)
            case "seg" :
                return new SegParser("seg")
            case "mut":
                return new SegParser("mut")
            case "maf":
                return new SegParser("maf")
            case "gwas" :
                return new GWASParser(config)
            case "qtl":
                return new QTLParser(config)
            case "aed" :
                return new AEDParser(config)
            default:
                return new FeatureParser(config)
        }
    }

    async loadFeaturesNoIndex() {

        if (this.features) {
            // An optimization hack for non-indexed files, features are temporarily cached when header is read.
            const tmp = this.features;
            delete this.features;
            return tmp
        } else {
            const options = buildOptions(this.config);    // Add oauth token, if any
            const data = await igvxhr$1.loadByteArray(this.config.url, options);
            if (!this.header) {
                const dataWrapper = getDataWrapper$1(data);
                this.header = await this.parser.parseHeader(dataWrapper);
            }
            const dataWrapper = getDataWrapper$1(data);
            const features = [];
            await this._parse(features, dataWrapper);   // <= PARSING DONE HERE
            return features
        }
    }

    async loadFeaturesWithIndex(chr, start, end) {

        //console.log("Using index"
        const config = this.config;
        this.parser;
        const tabix = this.index.tabix;

        const refId = tabix ? this.index.sequenceIndexMap[chr] : chr;
        if (refId === undefined) {
            return []
        }

        const chunks = this.index.chunksForRange(refId, start, end);
        if (!chunks || chunks.length === 0) {
            return []
        } else {
            const allFeatures = [];
            for (let chunk of chunks) {

                let inflated;
                if (tabix) {
                    inflated = await this._blockLoader.getData(chunk.minv, chunk.maxv);
                } else {
                    const options = buildOptions(config, {
                        range: {
                            start: chunk.minv.block,
                            size: chunk.maxv.block - chunk.minv.block + 1
                        }
                    });
                    inflated = await igvxhr$1.loadString(config.url, options);
                }

                const slicedData = chunk.minv.offset ? inflated.slice(chunk.minv.offset) : inflated;
                const dataWrapper = getDataWrapper$1(slicedData);
                await this._parse(allFeatures, dataWrapper, chr, end, start);

            }

            return allFeatures
        }
    }

    async loadFeaturesFromService(chr, start, end) {

        let url;
        if (typeof this.config.url === 'function') {
            url = this.config.url({chr, start, end});
        } else {
            url = this.config.url
                .replace("$CHR", chr)
                .replace("$START", start)
                .replace("$END", end);
        }
        const options = buildOptions(this.config);    // Adds oauth token, if any
        const data = await igvxhr$1.loadString(url, options);
        const dataWrapper = getDataWrapper$1(data);
        const features = [];
        await this._parse(features, dataWrapper);   // <= PARSING DONE HERE
        return features

    }

    async _parse(allFeatures, dataWrapper, chr, end, start) {

        let features = await this.parser.parseFeatures(dataWrapper);

        features.sort(function (a, b) {
            if (a.chr === b.chr) {
                return a.start - b.start
            } else {
                return a.chr.localeCompare(b.chr)
            }
        });

        // Filter features not in requested range.
        if (undefined === chr) {
            for (let f of features) allFeatures.push(f);   // Don't use spread operator !!!  slicedFeatures might be very large
        } else {
            let inInterval = false;
            for (let i = 0; i < features.length; i++) {
                const f = features[i];
                if (f.chr === chr) {
                    if (f.start > end) {
                        allFeatures.push(f);  // First feature beyond interval
                        break
                    }
                    if (f.end >= start && f.start <= end) {
                        // All this to grab first feature before start of interval.  Needed for some track renderers, like line plot
                        if (!inInterval) {
                            inInterval = true;
                            if (i > 0) {
                                allFeatures.push(features[i - 1]);
                            }
                        }
                        allFeatures.push(f);
                    }
                }
            }
        }
    }

    async getIndex() {
        if (this.index) {
            return this.index
        } else if (this.config.indexURL) {
            this.index = await this.loadIndex();
            return this.index
        }
    }

    /**
     * Return a Promise for the async loaded index
     */
    async loadIndex() {
        const indexURL = this.config.indexURL;
        return loadIndex(indexURL, this.config)
    }

    async loadFeaturesFromDataURI() {

        if (this.features) {
            // An optimization hack for non-indexed files, features are temporarily cached when header is read.
            const tmp = this.features;
            delete this.features;
            return tmp
        } else {
            const plain = decodeDataURI$1(this.dataURI);
            let dataWrapper = getDataWrapper$1(plain);
            this.header = await this.parser.parseHeader(dataWrapper);
            if (this.header instanceof String && this.header.startsWith("##gff-version 3")) {
                this.format = 'gff3';
            }

            dataWrapper = getDataWrapper$1(plain);
            const features = [];
            await this._parse(features, dataWrapper);
            return features
        }
    }

}

const isString$2 = isString$3;


class CustomServiceReader {
    constructor(config) {
        this.config = config;
    }

    async readFeatures(chr, start, end) {

        let url;
        if (typeof this.config.url === 'function') {
            url = this.config.url({chr, start, end});
        } else {
            url = this.config.url
                .replace("$CHR", chr)
                .replace("$START", start)
                .replace("$END", end);
        }

        let config = Object.assign({}, this.config);
        if (this.config.body !== undefined) {
            if (typeof this.config.body === 'function') {
                config.body = this.config.body({chr, start, end});
            } else {
                config.body =
                    this.config.body
                        .replace("$CHR", chr)
                        .replace("$START", start)
                        .replace("$END", end);
            }
        }


        let features;
        const data = await igvxhr$1.load(url, config);
        if (data) {
            if (typeof this.config.parser === "function") {
                features = this.config.parser(data);
            } else if (isString$2(data)) {
                features = JSON.parse(data);
            } else {
                features = data;
            }
        }
        if (this.config.mappings) {
            let mappingKeys = Object.keys(this.config.mappings);
            for (let f of features) {
                for (let key of mappingKeys) {
                    f[key] = f[this.config.mappings[key]];
                }
            }
        }
        return features
    }
}

const UCSCServiceReader = function (config, genome) {
    this.config = config;
    this.genome = genome;
    this.expandQueryInterval = false;
};

UCSCServiceReader.prototype.readFeatures = function (chr, start, end) {

    const s = Math.max(0, Math.floor(start));
    let e = Math.ceil(end);

    if (this.genome) {
        const c = this.genome.getChromosome(chr);
        if (c && e > c.bpLength) {
            e = c.bpLength;
        }
    }


    const url = this.config.url + '?db=' + this.config.db + '&table=' + this.config.tableName + '&chr=' + chr + '&start=' + s + '&end=' + e;

    return igvxhr$1.loadJson(url, this.config)
        .then(function (data) {
            if (data) {
                data.forEach(function (sample) {
                    if (sample.hasOwnProperty('exonStarts') &&
                        sample.hasOwnProperty('exonEnds') &&
                        sample.hasOwnProperty('exonCount') &&
                        sample.hasOwnProperty('cdsStart') &&
                        sample.hasOwnProperty('cdsEnd')) {
                        addExons(sample);
                    }
                });
                return data
            } else {
                return null
            }
        })
};

function addExons(sample) {
    var exonCount, exonStarts, exonEnds, exons, eStart, eEnd;
    exonCount = sample['exonCount'];
    exonStarts = sample['exonStarts'].split(',');
    exonEnds = sample['exonEnds'].split(',');
    exons = [];

    for (var i = 0; i < exonCount; i++) {
        eStart = parseInt(exonStarts[i]);
        eEnd = parseInt(exonEnds[i]);
        var exon = {start: eStart, end: eEnd};

        if (sample.cdsStart > eEnd || sample.cdsEnd < sample.cdsStart) exon.utr = true;   // Entire exon is UTR
        if (sample.cdsStart >= eStart && sample.cdsStart <= eEnd) exon.cdStart = sample.cdsStart;
        if (sample.cdsEnd >= eStart && sample.cdsEnd <= eEnd) exon.cdEnd = sample.cdsEnd;

        exons.push(exon);
    }

    sample.exons = exons;
}

/**
 * EQTL reader for GTEX webservice
 */
class GtexReader {

    featureCaches = []

    gtexChrs = new Set(["chr1", "chr10", "chr11", "chr12", "chr13", "chr14", "chr15", "chr16", "chr17", "chr18",
        "chr19", "chr2", "chr20", "chr21", "chr22", "chr3", "chr4", "chr5", "chr6", "chr7", "chr8", "chr9", "chrM",
        "chrX", "chrY"])

    constructor(config) {

        this.config = config;
        this.url = config.url;
        this.tissueId = config.tissueSiteDetailId;
        this.datasetId = config.datasetId || "gtex_v8";
    }

    async readFeatures(chr, bpStart, bpEnd) {

        // GTEX uses UCSC chromosome naming conventions.
        const queryChr = chr.startsWith("chr") ? chr : chr === "MT" ? "chrM" : "chr" + chr;

        if (!this.gtexChrs.has(queryChr)) {
            return []
        }

        const queryStart = Math.floor(bpStart);
        const queryEnd = Math.ceil(bpEnd);
        const datasetId = this.datasetId;
        const queryURL = this.url + "?chromosome=" + queryChr + "&start=" + queryStart + "&end=" + queryEnd +
            "&tissueSiteDetailId=" + this.tissueId + "&datasetId=" + datasetId;

        const json = await igvxhr$1.loadJson(queryURL, {
            withCredentials: this.config.withCredentials
        });

        if (json && json.singleTissueEqtl) {
            return json.singleTissueEqtl.map(json => new EQTL(json))
        } else {
            return []
        }
    }
}

// Example GTEX eqtl
// {
//     "chromosome": "chr16",
//     "datasetId": "gtex_v8",
//     "gencodeId": "ENSG00000275445.1",
//     "geneSymbol": "CTD-2649C14.3",
//     "geneSymbolUpper": "CTD-2649C14.3",
//     "nes": 0.51295,
//     "pValue": 5.57674e-14,
//     "pos": 21999621,
//     "snpId": "rs35368623",
//     "tissueSiteDetailId": "Muscle_Skeletal",
//     "variantId": "chr16_21999621_G_GA_b38"
// }


class EQTL {

    constructor(eqtl) {
        this.chr = eqtl.chromosome;
        this.start = eqtl.pos - 1;
        this.end = this.start + 1;
        this.snp = eqtl.snpId;
        this.phenotype = eqtl.geneSymbol;
        this.pValue = eqtl.pValue;
        this.json = eqtl;
    }

    popupData() {
        return Object.keys(this.json).map(key => {
            return {name: key, value: this.json[key]}
        })
    }
}

class HtsgetReader {

    constructor(config, genome) {
        this.config = config;
        this.genome = genome;
        if (config.format) {
            this.format = config.format.toUpperCase();
        } else {
            throw Error('Format is required, and must be either "bam" or "cram"')
        }
        if (!(this.format === "BAM" || this.format === "VCF")) {
            throw Error(`htsget format ${config.format} is not supported`)
        }
    }

    async readHeaderData() {
        const url = `${getUrl(this.config)}?class=header&format=${this.format}`;
        const ticket = await igvxhr$1.loadJson(url, buildOptions(this.config));
        return await this.loadUrls(ticket.htsget.urls)
    }

    async readData(chr, start, end) {
        const url = `${getUrl(this.config)}?format=${this.format}&referenceName=${chr}&start=${Math.floor(start)}&end=${Math.ceil(end)}`;
        const ticket = await igvxhr$1.loadJson(url, buildOptions(this.config));
        return this.loadUrls(ticket.htsget.urls)
    }

    async loadUrls(urls) {

        const promiseArray = [];
        for (let urlData of urls) {

            if (urlData.url.startsWith('data:')) {
                // this is a data-uri
                promiseArray.push(Promise.resolve(dataUriToBytes(urlData.url)));

            } else {
                const options = {headers: urlData.headers || {}};
                promiseArray.push(igvxhr$1.loadArrayBuffer(urlData.url, options));
            }
        }
        const arrayBuffers = await Promise.all(promiseArray);
        return concatArrays(arrayBuffers)
    }


    static async inferFormat(config) {
        try {
            const url = getUrl(config);
            const headerURL = `${url}${url.includes("?") ? "&" : "?"}class=header`;
            const ticket = await igvxhr$1.loadJson(headerURL, buildOptions(config));
            if (ticket.htsget) {
                const format = ticket.htsget.format;
                if (!(format === "BAM" || format === "VCF")) {
                    throw Error(`htsget format ${format} is not supported`)
                }
                config.format = format.toLowerCase();
                config.sourceType = "htsget";
                if (!config.name) {
                    config.name = await getFilename$1(config.url);
                }
            }
        } catch (e) {
            // Errors => this is not an htsget source, not an application error.  Ignore
        }
    }
}

/**
 * Extract the full url from the config.  Striving for backward compatibility, "endpoint" and "id" are deprecated.
 *
 * @param config
 */
function getUrl(config) {
    if (config.url && config.endpoint && config.id) {
        return config.url + config.endpoint + config.id    // Deprecated
    } else if (config.endpoint && config.id) {
        return config.endpoint + config.id                // Deprecated
    } else if (config.url) {
        if (config.url.startsWith("htsget://")) {
            return config.url.replace("htsget://", "https://")    // htsget -> http not supported
        } else {
            return config.url
        }
    } else {
        throw Error("Must specify either 'url', or 'endpoint' and 'id")
    }


}

/**
 * Concatenate a list of array buffers, returning an UInt8Array
 * @param arrayBuffers
 */
function concatArrays(arrayBuffers) {

    let len = 0;
    for (let a of arrayBuffers) {
        len += a.byteLength;
    }

    let offset = 0;
    const newArray = new Uint8Array(len);
    for (let buf of arrayBuffers) {
        const a = new Uint8Array(buf);
        newArray.set(a, offset);
        offset += a.length;
    }

    return newArray
}

function dataUriToBytes(dataUri) {

    const split = dataUri.split(',');
    const info = split[0].split(':')[1];
    let dataString = split[1];

    if (info.indexOf('base64') >= 0) {
        dataString = atob(dataString);
    } else {
        dataString = decodeURI(dataString);
    }

    const bytes = new Uint8Array(dataString.length);
    for (var i = 0; i < dataString.length; i++) {
        bytes[i] = dataString.charCodeAt(i);
    }

    return bytes
}

/**
 * A data/feature source helper class for managing chromosome aliasing.  Maps reference sequence names to aliases
 * used by the feature source (e.g. chr20 -> 20).
 */
class ChromAliasManager {

    chrAliasTable = new Map()

    /**
     * @param sequenceNames - Sequence names defined by the data source (e.g. bam or feature file)
     * @param genome        - Reference genome object.
     */
    constructor(sequenceNames, genome) {
        this.sequenceNames = new Set(sequenceNames);
        this.genome = genome;
    }

    async getAliasName(chr) {
        if (!this.genome) {
            return chr   // A no-op manager, used in testing.
        }

        if (!this.chrAliasTable.has(chr)) {
            const aliasRecord = await this.genome.getAliasRecord(chr);
            if (!aliasRecord) {
                this.chrAliasTable.set(chr, chr);  // No know alias, record to prevent searching again
            } else {
                const aliases = Object.keys(aliasRecord)
                    .filter(k => k !== "start" && k !== "end")
                    .map(k => aliasRecord[k])
                    .filter(a => this.sequenceNames.has(a));
                if (aliases.length > 0) {
                    this.chrAliasTable.set(chr, aliases[0]);
                } else {
                    this.chrAliasTable.set(chr, chr);  // No known alias, record to prevent searching again
                }
            }
        }

        return this.chrAliasTable.get(chr)
    }
}

class HtsgetVariantReader extends HtsgetReader {

    constructor(config, genome) {
        super(config, genome);
        this.parser = new VcfParser();
    }

    async readHeader() {
        if (!this.header) {
            let data = await this.readHeaderData();
            if (isgzipped$1(data)) {
                data = ungzip_blocks$1(data);
            }

            const dataWrapper = getDataWrapper$1(data);
            this.header = await this.parser.parseHeader(dataWrapper, this.genome);
            if (this.header.sequenceNames && this.header.sequenceNames.length > 0) {
                this.chromAliasManager = new ChromAliasManager(this.header.sequenceNames, this.genome);
            }
        }
        return this.header
    }

    async readFeatures(chr, start, end) {

        if('all' === chr) {
            return []    // This should never happen, but just in case
        }

        if (this.config.format && this.config.format.toUpperCase() !== "VCF") {
            throw Error(`htsget format ${this.config.format} is not supported`)
        }

        if (!this.header) {
            await this.readHeader();
        }


        let queryChr = this.chromAliasManager ? await this.chromAliasManager.getAliasName(chr) : chr;

        let data = await this.readData(queryChr, start, end);
        if (isgzipped$1(data)) {
            data = ungzip_blocks$1(data);
        }

        const dataWrapper = getDataWrapper$1(data);

        return this.parser.parseFeatures(dataWrapper)

        //  return dataWrapper;

    }
}

class BaseFeatureSource {

    constructor(genome) {
        this.genome = genome;
    }


    // Return the next feature whose start is > position.
    async nextFeature(chr, position, direction, visibilityWindow) {

        let chromosomeNames = this.genome.chromosomeNames || [chr];
        let idx = chromosomeNames.indexOf(chr);
        if (idx < 0) return // This shouldn't happen

        // Look ahead (or behind) in 10 kb intervals, but no further than visibilityWindow
        const window = Math.min(10000, visibilityWindow || 10000);
        let queryStart = direction ? position : Math.max(position - window, 0);
        while (idx < chromosomeNames.length && idx >= 0) {
            chr = chromosomeNames[idx];
            const chromosome = this.genome.getChromosome(chr);
            const chromosomeEnd = chromosome.bpLength;
            while (queryStart < chromosomeEnd && queryStart >= 0) {
                let queryEnd = direction ? queryStart + window : Math.min(position, queryStart + window);
                const featureList = await this.getFeatures({chr, start: queryStart, end: queryEnd, visibilityWindow});
                if (featureList) {

                    const compare = (o1, o2) => o1.start - o2.start + o1.end - o2.end;
                    const sortedList = Array.from(featureList);
                    sortedList.sort(compare);

                    // Search for next or previous feature relative to centers.  We use a linear search because the
                    // feature is likely to be near the first or end of the list
                    let idx = direction ? 0 : sortedList.length - 1;
                    while(idx >= 0 && idx < sortedList.length) {
                        const f = sortedList[idx];
                        const center = (f.start + f.end) / 2;
                        if(direction) {
                            if(center > position) return f
                            idx++;
                        } else {
                            if(center < position) return f
                            idx--;
                        }
                    }
                }
                queryStart = direction ? queryEnd : queryStart - window;
            }
            if (direction) {
                idx++;
                queryStart = 0;
                position = 0;
            } else {
                idx--;
                if (idx < 0) break
                const prevChromosome = this.genome.getChromosome(chromosomeNames[idx]);
                position = prevChromosome.bpLength;
                queryStart = position - window;
            }
        }
    }

    async previousFeature(chr, position, direction, visibilityWindow) {

        let chromosomeNames = this.genome.chromosomeNames || [chr];
        let idx = chromosomeNames.indexOf(chr);
        if (idx < 0) return // This shouldn't happen

        // Look ahead (or behind) in 10 kb intervals, but no further than visibilityWindow
        const window = Math.min(10000, visibilityWindow || 10000);
        let queryStart = direction ? position : Math.max(position - window, 0);
        while (idx < chromosomeNames.length && idx >= 0) {
            chr = chromosomeNames[idx];
            const chromosome = this.genome.getChromosome(chr);
            const chromosomeEnd = chromosome.bpLength;
            while (queryStart < chromosomeEnd && queryStart >= 0) {
                let queryEnd = Math.min(position, queryStart + window);
                const featureList = await this.getFeatures({chr, start: queryStart, end: queryEnd, visibilityWindow});
                if (featureList) {

                    const compare = (o1, o2) => o1.start - o2.start + o1.end - o2.end;
                    const sortedList = Array.from(featureList);
                    sortedList.sort(compare);

                    // Search for next or previous feature relative to centers.  We use a linear search because the
                    // feature is likely to be near the first or end of the list
                    let idx = direction ? 0 : sortedList.length - 1;
                    while(idx >= 0 && idx < sortedList.length) {
                        const f = sortedList[idx];
                        const center = (f.start + f.end) / 2;
                        if(direction) {
                            if(center > position) return f
                            idx++;
                        } else {
                            if(center < position) return f
                            idx--;
                        }
                    }
                }
                queryStart = direction ? queryEnd : queryStart - window;
            }
            if (direction) {
                idx++;
                queryStart = 0;
                position = 0;
            } else {
                idx--;
                if (idx < 0) break
                const prevChromosome = this.genome.getChromosome(chromosomeNames[idx]);
                position = prevChromosome.bpLength;
                queryStart = position - window;
            }
        }
    }

}

const GZIP_FLAG = 0x1;

class TDFReader {

    constructor(config, genome) {
        this.config = config;
        this.genome = genome;
        this.path = config.url;
        this.groupCache = {};
        this.datasetCache = {};
    }


    async readHeader() {

        if (this.magic !== undefined) {
            return this   // Already read
        }

        let data = await igvxhr$1.loadArrayBuffer(this.path, buildOptions(this.config, {range: {start: 0, size: 64000}}));
        let binaryParser = new BinaryParser$1(new DataView(data));
        this.magic = binaryParser.getInt();
        this.version = binaryParser.getInt();
        this.indexPos = binaryParser.getLong();
        this.indexSize = binaryParser.getInt();
        binaryParser.getInt();


        if (this.version >= 2) {
            let nWindowFunctions = binaryParser.getInt();
            this.windowFunctions = [];
            while (nWindowFunctions-- > 0) {
                this.windowFunctions.push(binaryParser.getString());
            }
        }

        this.trackType = binaryParser.getString();
        this.trackLine = binaryParser.getString();

        let nTracks = binaryParser.getInt();
        this.trackNames = [];
        while (nTracks-- > 0) {
            this.trackNames.push(binaryParser.getString());
        }
        this.genomeID = binaryParser.getString();
        this.flags = binaryParser.getInt();
        this.compressed = (this.flags & GZIP_FLAG) !== 0;

        // Now read index
        data = await igvxhr$1.loadArrayBuffer(this.path, buildOptions(this.config, {
            range: {
                start: this.indexPos,
                size: this.indexSize
            }
        }));
        binaryParser = new BinaryParser$1(new DataView(data));
        this.datasetIndex = {};
        let nEntries = binaryParser.getInt();
        while (nEntries-- > 0) {
            const name = binaryParser.getString();
            const pos = binaryParser.getLong();
            const size = binaryParser.getInt();
            this.datasetIndex[name] = {position: pos, size: size};
        }

        this.groupIndex = {};
        nEntries = binaryParser.getInt();
        while (nEntries-- > 0) {
            const name = binaryParser.getString();
            const pos = binaryParser.getLong();
            const size = binaryParser.getInt();
            this.groupIndex[name] = {position: pos, size: size};
        }

        return this
    }

    async readDataset(chr, windowFunction, zoom) {

        const key = chr + "_" + windowFunction + "_" + zoom;

        if (this.datasetCache[key]) {
            return this.datasetCache[key]

        } else {
            await this.readHeader();
            const wf = (this.version < 2) ? "" : "/" + windowFunction;
            const zoomString = (chr.toLowerCase() === "all" || zoom === undefined) ? "0" : zoom.toString();

            let dsName;
            if (windowFunction === "raw") {
                dsName = "/" + chr + "/raw";
            } else {
                dsName = "/" + chr + "/z" + zoomString + wf;
            }
            const indexEntry = this.datasetIndex[dsName];

            if (indexEntry === undefined) {
                return undefined
            }

            const data = await igvxhr$1.loadArrayBuffer(this.path, buildOptions(this.config, {
                range: {
                    start: indexEntry.position,
                    size: indexEntry.size
                }
            }));

            if (!data) {
                return undefined
            }

            const binaryParser = new BinaryParser$1(new DataView(data));
            let nAttributes = binaryParser.getInt();
            const attributes = {};
            while (nAttributes-- > 0) {
                attributes[binaryParser.getString()] = binaryParser.getString();
            }
            const dataType = binaryParser.getString();
            const tileWidth = binaryParser.getFloat();
            let nTiles = binaryParser.getInt();
            const tiles = [];
            while (nTiles-- > 0) {
                tiles.push({position: binaryParser.getLong(), size: binaryParser.getInt()});
            }

            const dataset = {
                name: dsName,
                attributes: attributes,
                dataType: dataType,
                tileWidth: tileWidth,
                tiles: tiles
            };

            this.datasetCache[key] = dataset;
            return dataset
        }
    }

    async readRootGroup() {

        const genome = this.genome;
        const rootGroup = this.groupCache["/"];
        if (rootGroup) {
            return rootGroup
        } else {

            const group = await this.readGroup("/");
            const names = group["chromosomes"];
            const maxZoomString = group["maxZoom"];

            // Now parse out interesting attributes.
            if (maxZoomString) {
                this.maxZoom = Number(maxZoomString);
            }

            const totalCountString = group["totalCount"];
            if (totalCountString) {
                group.totalCount = Number(totalCountString);
            }

            // Chromosome names
            const chrAliasTable = {};
            if (names) {
                names.split(",").forEach(function (chr) {
                    const canonicalName = genome.getChromosomeName(chr);
                    chrAliasTable[canonicalName] = chr;
                });
            }
            this.chrAliasTable = chrAliasTable;

            this.groupCache["/"] = group;
            return group
        }
    }

    async readGroup(name) {

        const group = this.groupCache[name];
        if (group) {
            return group
        } else {

            await this.readHeader();
            const indexEntry = this.groupIndex[name];
            if (indexEntry === undefined) {
                return undefined
            }

            const data = await igvxhr$1.loadArrayBuffer(this.path, buildOptions(this.config, {
                range: {
                    start: indexEntry.position,
                    size: indexEntry.size
                }
            }));

            if (!data) {
                return undefined
            }

            const binaryParser = new BinaryParser$1(new DataView(data));
            const group = {name: name};
            let nAttributes = binaryParser.getInt();
            while (nAttributes-- > 0) {
                const key = binaryParser.getString();
                const value = binaryParser.getString();
                group[key] = value;
            }
            this.groupCache[name] = group;
            return group
        }
    }

    async readTiles(tileIndeces, nTracks) {

        tileIndeces.sort(function (a, b) {
            return a.position - b.position
        });

        tileIndeces = tileIndeces.filter(function (idx) {
            return idx.size > 0
        });

        if (tileIndeces.length === 0) {
            return []
        }

        const tiles = [];

        for (let indexEntry of tileIndeces) {

            const data = await igvxhr$1.loadArrayBuffer(this.path, buildOptions(this.config, {
                range: {
                    start: indexEntry.position,
                    size: indexEntry.size
                }
            }));

            let tileData;
            try {
                tileData = this.compressed ? inflate_1$3(data).buffer : data;
            } catch (e) {
                console.error(e);
                continue
            }

            const binaryParser = new BinaryParser$1(new DataView(tileData));
            const type = binaryParser.getString();
            let tile;
            switch (type) {
                case "fixedStep":
                    tile = createFixedStep(binaryParser, nTracks);
                    break
                case "variableStep":
                    tile = createVariableStep(binaryParser, nTracks);
                    break
                case "bed":
                case "bedWithName":
                    tile = createBed(binaryParser, nTracks, type);
                    break
                default:
                    throw "Unknown tile type: " + type
            }
            tiles.push(tile);
        }
        return tiles
    }

    async readTile(indexEntry, nTracks) {

        let data = await igvxhr$1.loadArrayBuffer(this.path, buildOptions(this.config, {
            range: {
                start: indexEntry.position,
                size: indexEntry.size
            }
        }));

        if (this.compressed) {
            const plain = inflate_1$3(data);
            data = plain.buffer;
        }

        const binaryParser = new BinaryParser$1(new DataView(data));
        const type = binaryParser.getString();
        switch (type) {
            case "fixedStep":
                return createFixedStep(binaryParser, nTracks)
            case "variableStep":
                return createVariableStep(binaryParser, nTracks)
            case "bed":
            case "bedWithName":
                return createBed(binaryParser, nTracks, type)
            default:
                throw "Unknown tile type: " + type
        }
    }

}

function createFixedStep(binaryParser, nTracks) {
    const nPositions = binaryParser.getInt();
    const start = binaryParser.getInt();
    const span = binaryParser.getFloat();

    const data = [];
    let nt = nTracks;
    while (nt-- > 0) {
        let np = nPositions;
        const dtrack = [];
        while (np-- > 0) {
            dtrack.push(binaryParser.getFloat());
        }
        data.push(dtrack);
    }

    return {
        type: "fixedStep",
        start: start,
        span: span,
        data: data,
        nTracks: nTracks,
        nPositions: nPositions
    }
}

function createVariableStep(binaryParser, nTracks) {

    const tileStart = binaryParser.getInt();
    const span = binaryParser.getFloat();
    const nPositions = binaryParser.getInt();
    const start = [];

    let np = nPositions;
    while (np-- > 0) {
        start.push(binaryParser.getInt());
    }
    binaryParser.getInt();  // # of samples, ignored but should === nTracks

    const data = [];
    let nt = nTracks;
    while (nt-- > 0) {
        np = nPositions;
        const dtrack = [];
        while (np-- > 0) {
            dtrack.push(binaryParser.getFloat());
        }
        data.push(dtrack);
    }

    return {
        type: "variableStep",
        tileStart: tileStart,
        span: span,
        start: start,
        data: data,
        nTracks: nTracks,
        nPositions: nPositions
    }
}

function createBed(binaryParser, nTracks, type) {

    const nPositions = binaryParser.getInt();

    let n = nPositions;
    const start = [];
    while (n-- > 0) {
        start.push(binaryParser.getInt());
    }

    n = nPositions;
    const end = [];
    while (n-- > 0) {
        end.push(binaryParser.getInt());
    }

    binaryParser.getInt();  // # of samples, ignored but should === nTracks
    const data = [];
    let nt = nTracks;
    while (nt-- > 0) {
        let np = nPositions;
        const dtrack = [];
        while (np-- > 0) {
            dtrack.push(binaryParser.getFloat());
        }
        data.push(dtrack);
    }

    if (type === "bedWithName") {
        n = nPositions;
        const name = [];
        while (n-- > 0) {
            name.push(binaryParser.getString());
        }
    }

    return {
        type: type,
        start: start,
        end: end,
        data: data,
        nTracks: nTracks,
        nPositions: nPositions
    }
}

class TDFSource extends BaseFeatureSource {

    #wgValues = {}
    searchable = false


    constructor(config, genome) {
        super(genome);
        this.genome = genome;
        this.reader = new TDFReader(config, genome);
        this.queryable = true;
    }

    async getFeatures({chr, start, end, bpPerPixel, windowFunction = "mean"}) {

        if (chr.toLowerCase() === "all") {
            return this.getWGValues(windowFunction, bpPerPixel)
        } else {
            return this._getFeatures(chr, start, end, bpPerPixel, windowFunction)
        }
    }
    async _getFeatures(chr, start, end, bpPerPixel, windowFunction) {
        const genomicInterval = new GenomicInterval(chr, start, end);
        const genome = this.genome;


        if (!this.rootGroup) {
            this.rootGroup = await this.reader.readRootGroup();
            if (!this.normalizationFactor) {
                const totalCount = this.rootGroup.totalCount;
                if (totalCount) {
                    this.normalizationFactor = 1.0e6 / totalCount;
                }
            }
        }

        genomicInterval.bpPerPixel = bpPerPixel;
        const zoom = zoomLevelForScale$1(chr, bpPerPixel, genome);
        let queryChr = this.reader.chrAliasTable[chr];
        let maxZoom = this.reader.maxZoom;
        if (queryChr === undefined) queryChr = chr;
        if (maxZoom === undefined) maxZoom = -1;

        const wf = zoom > maxZoom ? "raw" : windowFunction;
        const dataset = await this.reader.readDataset(queryChr, wf, zoom);
        if (dataset == null) {
            return []
        }

        const tileWidth = dataset.tileWidth;
        const startTile = Math.floor(start / tileWidth);
        const endTile = Math.floor(end / tileWidth);
        const NTRACKS = 1;   // TODO read this
        const tiles = await this.reader.readTiles(dataset.tiles.slice(startTile, endTile + 1), NTRACKS);
        const features = [];
        for (let tile of tiles) {
            switch (tile.type) {
                case "bed":
                    decodeBedTile(tile, chr, start, end, bpPerPixel, features);
                    break
                case "variableStep":
                    decodeVaryTile(tile, chr, start, end, bpPerPixel, features);
                    break
                case "fixedStep":
                    decodeFixedTile(tile, chr, start, end, bpPerPixel, features);
                    break
                default:
                    throw ("Unknown tile type: " + tile.type)
            }
        }
        features.sort(function (a, b) {
            return a.start - b.start
        });

        return features
    }

    get supportsWholeGenome() {
        return true
    }

    get windowFunctions() {
        return this.reader.windowFunctions
    }

    async getWGValues(windowFunction, bpPerPixel) {

        const cached = this.#wgValues[windowFunction];
        if (cached && cached.bpPerPixel > 0.8 * bpPerPixel && cached.bpPerPixel < 1.2 * bpPerPixel) {
            return cached.values
        } else {
            const wgFeatures = [];
            const genome = this.genome;
            const chrNames = this.genome.wgChromosomeNames;
            if (chrNames) {
                for (let c of genome.wgChromosomeNames) {
                    const len = genome.getChromosome(c).bpLength;
                    bpPerPixel = len / 1000;
                    const chrFeatures = await this._getFeatures(c, 0, len, bpPerPixel, windowFunction);
                    if (chrFeatures) {
                        for (let f of chrFeatures) {
                            const wg = Object.assign({}, f);
                            wg.chr = "all";
                            wg.start = genome.getGenomeCoordinate(f.chr, f.start);
                            wg.end = genome.getGenomeCoordinate(f.chr, f.end);
                            wg._f = f;
                            wgFeatures.push(wg);
                        }
                    }
                }
            }
            this.#wgValues[windowFunction] = {values: wgFeatures, bpPerPixel};
            return wgFeatures
        }
    }

}

function decodeBedTile(tile, chr, bpStart, bpEnd, bpPerPixel, features) {

    const nPositions = tile.nPositions;
    const starts = tile.start;
    const ends = tile.end;
    const data = tile.data[0];   // Single track for now
    for (let i = 0; i < nPositions; i++) {
        const s = starts[i];
        const e = ends[i];
        if (e < bpStart) continue
        if (s > bpEnd) break
        features.push({
            chr: chr,
            start: s,
            end: e,
            value: data[i]
        });
    }
}

function decodeVaryTile(tile, chr, bpStart, bpEnd, bpPerPixel, features) {

    const nPositions = tile.nPositions;
    const starts = tile.start;
    const span = tile.span;
    const data = tile.data[0];   // Single track for now
    for (let i = 0; i < nPositions; i++) {
        const s = starts[i];
        const e = s + span;
        if (e < bpStart) continue
        if (s > bpEnd) break
        features.push({
            chr: chr,
            start: s,
            end: e,
            value: data[i]
        });
    }
}

function decodeFixedTile(tile, chr, bpStart, bpEnd, bpPerPixel, features) {

    const nPositions = tile.nPositions;
    let s = tile.start;
    const span = tile.span;
    const data = tile.data[0];   // Single track for now

    for (let i = 0; i < nPositions; i++) {
        const e = s + span;
        if (s > bpEnd) break
        if (e >= bpStart) {
            if (!Number.isNaN(data[i])) {
                features.push({
                    chr: chr,
                    start: s,
                    end: e,
                    value: data[i]
                });
            }
        }
        s = e;
    }
}


var log2 = Math.log(2);

function zoomLevelForScale$1(chr, bpPerPixel, genome) {

    // Convert bpPerPixel to IGV "zoom" level.   This is a bit convoluted,  TDF is computed zoom levels assuming
    // display in a 700 pixel window.  The fully zoomed out view of a chromosome is zoom level "0".
    // Zoom level 1 is magnified 2X,  and so forth

    var chrSize = genome.getChromosome(chr).bpLength;

    return Math.ceil(Math.log(Math.max(0, (chrSize / (bpPerPixel * 700)))) / log2)
}

const RPTREE_HEADER_SIZE = 48;
const RPTREE_NODE_LEAF_ITEM_SIZE = 32;   // leaf item size
const RPTREE_NODE_CHILD_ITEM_SIZE = 24;  // child item size

class RPTree {

    static magic = 610839776
    littleEndian = true
    nodeCache = new Map()

    constructor(path, config, startOffset, loader) {

        this.path = path;
        this.config = config;
        this.startOffset = startOffset;
        this.loader = loader || igvxhr$1;
    }


    async init() {
        const binaryParser = await this.#getParserFor(this.startOffset, RPTREE_HEADER_SIZE);
        let magic = binaryParser.getInt();
        if (magic !== RPTree.magic) {
            binaryParser.setPosition(0);
            this.littleEndian = !this.littleEndian;
            binaryParser.littleEndian = this.littleEndian;
            magic = binaryParser.getInt();
            if (magic !== RPTree.magic) {
                throw Error(`Bad magic number ${magic}`)
            }
        }

        const blockSize = binaryParser.getUInt();
        const itemCount = binaryParser.getLong();
        const startChromIx = binaryParser.getUInt();
        const startBase = binaryParser.getUInt();
        const endChromIx = binaryParser.getUInt();
        const endBase = binaryParser.getUInt();
        const endFileOffset = binaryParser.getLong();
        const itemsPerSlot = binaryParser.getUInt();
        const reserved = binaryParser.getUInt();
        const rootNodeOffset = this.startOffset + RPTREE_HEADER_SIZE;
        this.header = {
            magic,
            blockSize,
            itemCount,
            startChromIx,
            startBase,
            endChromIx,
            endBase,
            endFileOffset,
            itemsPerSlot,
            reserved,
            rootNodeOffset
        };
        return this
    }

    async #getParserFor(start, size) {
        const data = await this.loader.loadArrayBuffer(this.path, buildOptions(this.config, {range: {start, size}}));
        return new BinaryParser$1(new DataView(data), this.littleEndian)
    }


    async findLeafItemsOverlapping(chrIdx1, startBase, chrIdx2, endBase) {

        const leafItems = [];
        const walkTreeNode = async (offset) => {
            const node = await this.readNode(offset);
            for (let item of node.items) {
                if (overlaps(item, chrIdx1, startBase, chrIdx2, endBase)) {
                    if (node.type === 1) {   // Leaf node
                        leafItems.push(item);
                    } else { // Non leaf node
                        await walkTreeNode(item.childOffset);
                    }
                }
            }
        };

        await walkTreeNode(this.header.rootNodeOffset);
        return leafItems
    }


    async readNode(offset) {

        const nodeKey = offset;
        if (this.nodeCache.has(nodeKey)) {
            return this.nodeCache.get(nodeKey)
        }

        let binaryParser = await this.#getParserFor(offset, 4);
        const type = binaryParser.getByte();
        const isLeaf = (type === 1);
        binaryParser.getByte();
        const count = binaryParser.getUShort();
        let bytesRequired = count * (isLeaf ? RPTREE_NODE_LEAF_ITEM_SIZE : RPTREE_NODE_CHILD_ITEM_SIZE);
        binaryParser = await this.#getParserFor(offset + 4, bytesRequired);

        const items = [];
        for (let i = 0; i < count; i++) {
            let item = {
                isLeaf: isLeaf,
                startChrom: binaryParser.getInt(),
                startBase: binaryParser.getInt(),
                endChrom: binaryParser.getInt(),
                endBase: binaryParser.getInt(),
                childOffset: binaryParser.getLong()
            };
            if (isLeaf) {
                item.dataSize =  binaryParser.getLong();
                item.dataOffset = item.childOffset;
            }
            items.push(item);
        }

        const node = {type, items};
        this.nodeCache.set(nodeKey, node);
        return node
    }

}

/**
 * Return true if {chrIdx1:startBase-chrIdx2:endBase} overlaps item's interval
 * @returns {boolean}
 */
function overlaps(item, chrIdx1, startBase, chrIdx2, endBase) {

    if (!item) {
        return false
    }

    return ((chrIdx2 > item.startChrom) || (chrIdx2 === item.startChrom && endBase >= item.startBase)) &&
        ((chrIdx1 < item.endChrom) || (chrIdx1 === item.endChrom && startBase <= item.endBase))


}

function getDecoder(definedFieldCount, fieldCount, autoSql, format) {
//biggenepred
    if ("biginteract" === format || (autoSql && ('chromatinInteract' === autoSql.table || 'interact' === autoSql.table))) {
        return decodeInteract
    } else {
        const standardFieldCount = definedFieldCount - 3;
        return function (feature, tokens) {

            if (standardFieldCount > 0) {
                feature.name = tokens[0];
            }
            if (standardFieldCount > 1) {
                feature.score = Number(tokens[1]);
            }
            if (standardFieldCount > 2) {
                feature.strand = tokens[2];
            }
            if (standardFieldCount > 3) {
                feature.cdStart = parseInt(tokens[3]);
            }
            if (standardFieldCount > 4) {
                feature.cdEnd = parseInt(tokens[4]);
            }
            if (standardFieldCount > 5) {
                if (tokens[5] !== "." && tokens[5] !== "0" && tokens[5] !== "-1") {
                    const c = IGVColor.createColorString(tokens[5]);
                    feature.color = c.startsWith("rgb") ? c : undefined;
                }
            }
            if (standardFieldCount > 8) {
                const exonCount = parseInt(tokens[6]);
                const exonSizes = tokens[7].split(',');
                const exonStarts = tokens[8].split(',');
                const exons = [];
                for (let i = 0; i < exonCount; i++) {
                    const eStart = feature.start + parseInt(exonStarts[i]);
                    const eEnd = eStart + parseInt(exonSizes[i]);
                    exons.push({start: eStart, end: eEnd});
                }
                findUTRs(exons, feature.cdStart, feature.cdEnd);
                feature.exons = exons;
            }

            if (autoSql) {
                // TODO -- these should be equal, validate?  fieldCount-definedFieldCount, as.fields.length, tokens.length-3
                const extraStart = definedFieldCount;
                for (let i = extraStart; i < fieldCount; i++) {
                    if (i < autoSql.fields.length) {

                        const name = autoSql.fields[i].name;

                        if (name === "exonFrames") {
                            const frameOffsets = tokens[i - 3].replace(/,$/, '').split(',');
                            for (let i = 0; i < feature.exons.length; i++) {
                                const exon = feature.exons[i];
                                const fo = parseInt(frameOffsets[i]);
                                if (fo != -1) exon.readingFrame = fo;
                            }
                        } else {
                            const value = tokens[i - 3];
                            feature[name] = value;
                        }
                    }
                }
            }
        }
    }

    //table chromatinInteract
// "Chromatin interaction between two regions"
//     (
//     string chrom;      "Chromosome (or contig, scaffold, etc.). For interchromosomal, use 2 records"
//     uint chromStart;   "Start position of lower region. For interchromosomal, set to chromStart of this region"
//     uint chromEnd;     "End position of upper region. For interchromosomal, set to chromEnd of this region"
//     string name;       "Name of item, for display"
//     uint score;        "Score from 0-1000"
//     double value;      "Strength of interaction or other data value. Typically basis for score"
//     string exp;        "Experiment name (metadata for filtering). Use . if not applicable"
//     string color;      "Item color.  Specified as r,g,b or hexadecimal #RRGGBB or html color name, as in //www.w3.org/TR/css3-color/#html4."
//     string region1Chrom;  "Chromosome of lower region. For non-directional interchromosomal, chrom of this region."
//     uint region1Start;  "Start position of lower/this region"
//     uint region1End;    "End position in chromosome of lower/this region"
//     string region1Name;  "Identifier of lower/this region"
//     string region1Strand; "Orientation of lower/this region: + or -.  Use . if not applicable"
//     string region2Chrom; "Chromosome of upper region. For non-directional interchromosomal, chrom of other region"
//     uint region2Start;  "Start position in chromosome of upper/this region"
//     uint region2End;    "End position in chromosome of upper/this region"
//     string region2Name; "Identifier of upper/this region"
//     string region2Strand; "Orientation of upper/this region: + or -.  Use . if not applicable"
//     )
    function decodeInteract(feature, tokens) {

        feature.chr1 = tokens[5];
        feature.start1 = Number.parseInt(tokens[6]);
        feature.end1 = Number.parseInt(tokens[7]);

        feature.chr2 = tokens[10];
        feature.start2 = Number.parseInt(tokens[11]);
        feature.end2 = Number.parseInt(tokens[12]);

        feature.name = tokens[0];
        feature.score = Number(tokens[1]);
        feature.value = Number(tokens[2]);
        feature.color = tokens[4] === '.' ? undefined : tokens[4] === "0" ? "rgb(0,0,0)" : tokens[4];

        return feature
    }

}

function findUTRs(exons, cdStart, cdEnd) {

    for (let exon of exons) {
        const end = exon.end;
        const start = exon.start;
        if (end < cdStart || start > cdEnd) {
            exon.utr = true;
        } else {
            if (cdStart >= start && cdStart <= end) {
                exon.cdStart = cdStart;
            }
            if (cdEnd >= start && cdEnd <= end) {
                exon.cdEnd = cdEnd;
            }
        }
    }
}

function scoreShade(score, color) {
    const alpha = Math.min(1, 0.11 + 0.89 * (score / 779));
    return alpha.toString()
}

function parseAutoSQL(str) {

    let table;
    const fields = [];
    let startDecoding = false;
    const lines = str.trim().split(/\s*[\r\n]+\s*/g);
    for (let line of lines) {
        line = line.trim();
        if (line.length > 0) {
            if (line.startsWith('#')) {
                continue
            } else if (line.startsWith('table')) {
                table = line.split(/\s+/)[1].trim();
            } else if (line.startsWith('(')) {
                startDecoding = true;
            } else if (line.startsWith(')')) ; else if (startDecoding) {
                const idx = line.indexOf(';');
                if (idx > 0) {
                    const tokens = line.substr(0, idx).split(/\s+/);
                    const description = line.substr(idx + 1).replace(/"/g, '').trim();
                    fields.push({
                        type: tokens[0],
                        name: tokens[1],
                        description: description
                    });
                }
            }
        }
    }
    return {
        table: table,
        fields: fields
    }
}

// This is a port of trix-js from the GMOD repository:  https://github.com/GMOD/trix-js,


// this is the number of hex characters to use for the address in ixixx, see
// https://github.com/GMOD/ixixx-js/blob/master/src/index.ts#L182
const ADDRESS_SIZE = 10;

class Trix {

    ixFile  // URL to the ix file
    ixxFile  // URL to the ixx file
    bufferCache = new Map()

    constructor(ixxFile, ixFile) {
        this.ixFile = ixFile;
        this.ixxFile = ixxFile;
    }

    /**
     * @param searchString
     * @param opts
     * @returns {Promise<Map<any, any>|undefined|*[]>}
     */
    async search(searchString, opts) {

        const searchWords = searchString.split(' ');

        // we only support a single search term
        const searchWord = searchWords[0].toLowerCase();
        const str = await this._getBuffer(searchWord, opts);
        if (!str) {
            return undefined
        }

        const lines = str
            .slice(0, str.lastIndexOf('\n'))
            .split('\n')
            .filter(f => !!f);

        const matches = [];
        for (let line of lines) {
            const word = line.split(' ')[0];
            const match = word.startsWith(searchWord);
            if (match) {
                matches.push(line);
            }
            // we are done scanning if we are lexicographically greater than the search string
            if (word.slice(0, searchWord.length) > searchWord) {
                break
            }
        }

        if(matches.length === 0) {
            return undefined
        } else {
            const results = new Map();
            for(let m of matches) {
                const [term, ...parts] = m.split(' ');
                results.set(term, parts.map(p => p.split(',')[0]));
            }
            return results
        }
    }

    async getIndex(opts) {
        if (!this.index) {
            this.index = await this._readIndex();
        }
        return this.index
    }

    async _readIndex(opts) {

        const file = await igvxhr$1.loadString(this.ixxFile);

        return file
            .split('\n')
            .filter(f => !!f)
            .map(line => {
                const p = line.length - ADDRESS_SIZE;
                const prefix = line.slice(0, p);
                const posStr = line.slice(p);
                const pos = Number.parseInt(posStr, 16);
                return [prefix, pos]          //as [string, number]
            })
    }

    async _getBuffer(searchWord, opts) {

        let start = 0;
        let end = 65536;
        const indexes = await this.getIndex(opts);
        for (let i = 0; i < indexes.length; i++) {
            const [key, value] = indexes[i];
            const trimmedEnd = Math.min(key.length, searchWord.length);
            const trimmedKey = key.slice(0, trimmedEnd);
            if (trimmedKey < searchWord) {
                start = value;
                end = value + 65536;
            }
        }

        // Return the buffer and its end position in the file.
        const len = end - start;
        if (len < 0) {
            return undefined
        }

        if(this.bufferCache.has(start)) {
            return this.bufferCache.get(start)
        } else {
            const buffer = await igvxhr$1.loadString(this.ixFile, {range: {start, size: len}});
            this.bufferCache.set(start, buffer);
            return buffer
        }

    }
}

class ChromTree {

    nameToId = new Map()
    idToName = new Map()

    constructor(path, config, startOffset, loader) {
        this.path = path;
        this.config = config;
        this.startOffset = startOffset;

        this.bpTree = new BPTree(path, config, startOffset, 'BPChromTree', loader);
    }

    async init() {
        return this.bpTree.init()
    }

    getItemCount() {
        return this.bpTree.getItemCount()
    }

    /**
     * Return the chromosome ID for the given name. This is the internal chromosome ID for the parent BB file only.
     * @param {string} chr - The chromosome name.
     * @returns {number|null} - The chromosome ID or null if not found.
     */
    async getIdForName(chr) {
        if (this.nameToId.has(chr)) {
            return this.nameToId.get(chr)
        } else {
            try {
                const result = await this.bpTree.search(chr);
                if (result) {
                    const id = result.id;
                    this.nameToId.set(chr, id);
                    return id
                } else {
                    return
                }
            } catch (error) {
                throw new Error(error)
            }
        }
    }

    /**
     * Return the chromosome name for the given ID. This is a potentially expensive operation as it involves
     * walking the tree until the leaf item for the given name is found. Currently it is used in only 2
     * situations:
     * (1) decoding features from a bigbed search-by-name query
     * (2) decoding bigwig data from the whole genome view
     * @param {number} id
     * @return {string|null}
     */
    async getNameForId(id) {
        if (this.idToName.has(id)) {
            return this.idToName.get(id)
        } else {
            const name = await this.searchForName(id);
            if (name !== null) {
                this.idToName.set(id, name);
                return name
            }
        }
        return null
    }

    /**
     * Perform a reverse search by traversing the tree starting at the given offset. This is potentially expensive
     * as it traverses the tree to find the name corresponding to the given ID.  It shoud not be used for large trees.
     *
     * @param {number} id - The ID to search for.
     * @returns {string|null} - The name corresponding to the ID, or null if not found.
     */
    async searchForName(id) {

        const reverseSearch = async (offset, id) => {

            const node = await this.bpTree.readTreeNode(offset);

            let found = null;

            if (node.type === 1) {
                // Leaf node
                for (const item of node.items) {
                    const key = item.key;
                    const itemId = item.value.id;
                    if (itemId === id) {
                        found = key;
                    }
                    // Cache the name and ID for future lookups
                    this.nameToId.set(key, itemId);
                    this.idToName.set(id, itemId);
                }
                return found
            } else {
                // Non-leaf node
                for (const item of node.items) {
                    found = await reverseSearch.call(this, item.offset, id);
                    if (found !== null) {
                        break
                    }
                }
            }
            return found
        };

        try {
            return reverseSearch.call(this, this.startOffset + 32, id)
        } catch (error) {
            throw new Error(error)
        }
    }

    /**
     * Return an estimated length of the genome, which might be the actual length if the number of contigs is small.
     * This is only used for calculating a default feature visibility window.
     *
     * @return {number}
     */
    async estimateGenomeSize() {
        try {
            const runningTotal = {total: 0, count: 0};
            await this.accumulateSize(this.startOffset + 32, runningTotal, 10000);
            const itemCount = this.getItemCount();
            return (itemCount / runningTotal.count) * runningTotal.total

        } catch (error) {
            console.error("Error estimating genome size", error);
            return -1
        }
    }

    async accumulateSize(offset, runningTotal, maxCount) {

        const node = await this.bpTree.readTreeNode(offset);

        if (node.type === 1) {
            // Leaf node
            for (const item of node.items) {
                const value = item.value;
                runningTotal.total += value.size;
                runningTotal.count += 1;
            }
        } else {
            // Non-leaf node.  Items are visited in random order to avoid biasing the estimate
            const shuffledItems = node.items.slice().sort(() => Math.random() - 0.5);
            for (const item of shuffledItems) {
                await this.accumulateSize(item.offset, runningTotal, maxCount);
                if (runningTotal.count > maxCount) {
                    break
                }
            }
        }
        return runningTotal
    }

}

const BIGWIG_MAGIC_LTH$1 = 0x888FFC26; // BigWig Magic Low to High
const BIGWIG_MAGIC_HTL = 0x26FC8F66; // BigWig Magic High to Low
const BIGBED_MAGIC_LTH$1 = 0x8789F2EB; // BigBed Magic Low to High
const BIGBED_MAGIC_HTL = 0xEBF28987; // BigBed Magic High to Low
const BBFILE_HEADER_SIZE = 64;
const BBFILE_EXTENDED_HEADER_HEADER_SIZE = 64;
const BUFFER_SIZE = 512000;     //  buffer

class BWReader {

    chrAliasTable = new Map()
    rpTreeCache = new Map()

    constructor(config, genome) {
        this.path = config.url;
        this.format = config.format || "bigwig";
        this.genome = genome;
        this.config = config;
        this.bufferSize = BUFFER_SIZE;
        this.loader = isDataURL(this.path) ?
            new DataBuffer(decodeDataURI$1(this.path).buffer) :
            igvxhr$1;

        const trixURL = config.trixURL || config.searchTrix;
        if (trixURL) {
            this._trix = new Trix(`${trixURL}x`, trixURL);
        }

    }

    /**
     * Preload all the data for this bb file
     * @returns {Promise<void>}
     */
    async preload() {
        const data = await igvxhr$1.loadArrayBuffer(this.path);
        this.loader = new DataBuffer(data);
        for (let rpTree of this.rpTreeCache.values()) {
            rpTree.loader = this.loader;
        }
        if (this._searchTrees) {
            for (let bpTree of this._searchTrees) {
                bpTree.loader = this.loader;
            }
        }
    }

    async readWGFeatures(wgChromosomeNames, bpPerPixel, windowFunction) {

        await this.loadHeader();
        // Convert the logic to JavaScript
        let minID = Number.MAX_SAFE_INTEGER;
        let maxID = -1;
        let chr1;
        let chr2;

        for (const chr of wgChromosomeNames) {
            const id = await this.getIdForChr(chr);
            if (id === null || id === undefined) {
                continue
            }
            if (id < minID) {
                minID = id;
                chr1 = chr;
            }
            if (id > maxID) {
                maxID = id;
                chr2 = chr;
            }
        }

        return this.readFeatures(chr1, 0, chr2, Number.MAX_VALUE, bpPerPixel, windowFunction)
    }

    async readFeatures(chr1, bpStart, chr2, bpEnd, bpPerPixel, windowFunction = "mean") {

        if (!bpStart) bpStart = 0;
        if (!bpEnd) bpEnd = Number.MAX_SAFE_INTEGER;

        await this.loadHeader();

        const chrIdx1 = await this.getIdForChr(chr1);
        const chrIdx2 = await this.getIdForChr(chr2);

        if (chrIdx1 === undefined || chrIdx2 === undefined) {
            return []
        }

        let treeOffset;
        let decodeFunction;
        if (this.type === "bigwig") {
            // Select a biwig "zoom level" appropriate for the current resolution.
            const zoomLevelHeaders = await this.getZoomHeaders();
            let zoomLevelHeader = bpPerPixel ? zoomLevelForScale(bpPerPixel, zoomLevelHeaders) : undefined;
            if (zoomLevelHeader && windowFunction != "none") {
                treeOffset = zoomLevelHeader.indexOffset;
                decodeFunction = decodeZoomData;
            } else {
                treeOffset = this.header.fullIndexOffset;
                decodeFunction = decodeWigData;
            }
        } else {
            // bigbed, zoom data is not currently used in igv for bed type features
            treeOffset = this.header.fullIndexOffset;
            decodeFunction = getBedDataDecoder.call(this);
        }


        // Load the R Tree and fine leaf items
        const rpTree = await this.loadRPTree(treeOffset);
        const leafItems = await rpTree.findLeafItemsOverlapping(chrIdx1, bpStart, chrIdx2, bpEnd);
        if (!leafItems || leafItems.length === 0) {
            return []
        } else {

            // Consolidate leaf items and get all data at once
            let start = Number.MAX_VALUE;
            let end = 0;
            for (let item of leafItems) {
                start = Math.min(start, item.dataOffset);
                end = Math.max(end, item.dataOffset + item.dataSize);
            }
            const size = end - start;
            const arrayBuffer = await this.loader.loadArrayBuffer(this.config.url, buildOptions(this.config, {
                range: {
                    start: start,
                    size: size
                }
            }));

            // Parse data and return features
            const features = [];
            for (let item of leafItems) {
                const uint8Array = new Uint8Array(arrayBuffer, item.dataOffset - start, item.dataSize);
                let plain;
                const isCompressed = this.header.uncompressBuffSize > 0;
                if (isCompressed) {
                    plain = inflate_1$3(uint8Array);
                } else {
                    plain = uint8Array;
                }
                await decodeFunction.call(this, new DataView(plain.buffer), chrIdx1, bpStart, chrIdx2, bpEnd, features, windowFunction);
            }

            features.sort(function (a, b) {
                return a.start - b.start
            });

            return features
        }
    }

    /**
     * Return the ID for the given chromosome name.  If there is no direct match, search for a chromosome alias.
     *
     * @param chr
     * @returns {Promise<*>}
     */
    async getIdForChr(chr) {

        if (this.chrAliasTable.has(chr)) {
            chr = this.chrAliasTable.get(chr);
            if (!chr) {
                return undefined
            }
        }

        let chrIdx = await this.chromTree.getIdForName(chr);

        // Try alias
        if (chrIdx === undefined && this.genome) {
            const aliasRecord = await this.genome.getAliasRecord(chr);
            let alias;
            if (aliasRecord) {
                for (let k of Object.keys(aliasRecord)) {
                    if (k === "start" || k === "end") continue
                    alias = aliasRecord[k];
                    if (alias === chr) continue   // Already tried this
                    chrIdx = await this.chromTree.getIdForName(alias);
                    if (chrIdx !== undefined) {
                        break
                    }
                }
            }
            this.chrAliasTable.set(chr, alias);  // alias may be undefined => no alias exists. Setting prevents repeated attempts
        }
        return chrIdx
    }


    /**
     * Potentially searchable if a bigbed source.  Bigwig files are not searchable.
     * @returns {boolean}
     */
    get searchable() {
        return "bigbed" === this.type
    }

    /**
     * Search the extended BP tree for the search term, and return any matching features.  This only works
     * for BB sources with an "extended" BP tree for searching
     * @param term
     * @returns {Promise<void>}
     */
    async search(term) {
        if (!this.header) {
            await this.loadHeader();
        }
        if (!(this.header && this.header.extraIndexCount)) {
            return undefined
        }

        const region = await this._searchForRegions(term);   // Either 1 or no (undefined) reginos returned for now
        if (region) {
            const features = await this._loadFeaturesForRange(region.offset, region.length);
            if (features) {
                // Collect all matching features and return the largest
                const matching = features.filter(f => {
                    // We could use the searchIndex parameter to pick an attribute (column),  but we don't know
                    // the names of all the columns and if they match IGV names
                    // TODO -- align all feature attribute names with UCSC, an use specific column
                    for (let key of Object.keys(f)) {
                        const v = f[key];
                        if (isString$3(v) && v.toLowerCase() === term.toLowerCase()) {
                            return true
                        }
                    }
                    return false
                });
                if (matching.length > 0) {
                    return matching.reduce((l, f) => (l.end - l.start) > (f.end - f.start) ? l : f, matching[0])
                } else {
                    return undefined
                }
            }
        }
    }

    async _searchForRegions(term) {
        const searchTrees = await this.#getSearchTrees();
        if (searchTrees) {

            // Use a trix index if we have one to map entered term to indexed value in bb file
            if (this._trix) {
                const termLower = term.toLowerCase();
                const trixResults = await this._trix.search(termLower);
                if (trixResults && trixResults.has(termLower)) {   // <= exact matches only for now
                    term = trixResults.get(termLower)[0];
                }
            }

            // For now take the first match, we don't support multiple results
            for (let bpTree of searchTrees) {
                const result = await bpTree.search(term);
                if (result) {
                    return result
                }
            }
        }
    }

    async #getSearchTrees() {

        if (this._searchTrees === undefined &&
            this.header.extraIndexOffsets &&
            this.header.extraIndexOffsets.length > 0) {
            this._searchTrees = [];
            for (let offset of this.header.extraIndexOffsets) {
                const type = undefined;
                const bpTree = await BPTree.loadBpTree(this.path, this.config, offset, type, this.loader);
                this._searchTrees.push(bpTree);
            }
        }
        return this._searchTrees

    }

    async getZoomHeaders() {
        if (this.zoomLevelHeaders) {
            return this.zoomLevelHeaders
        } else {
            await this.loadHeader();
            return this.zoomLevelHeaders
        }
    }

    /**
     * The BB header consists of
     *  (1) the common header
     *  (2) the zoom headers
     *  (3) autosql
     *  (4) total summary block (version 2 and later)
     *
     *  In addition, we read the chromomsome B+ tree
     * @returns {Promise<*>}
     */
    async loadHeader() {

        if (this.header) {
            return this.header
        } else {
            let data = await this.loader.loadArrayBuffer(this.path, buildOptions(this.config, {
                range: {
                    start: 0,
                    size: BBFILE_HEADER_SIZE
                }
            }));

            let header;

            // Assume low-to-high unless proven otherwise
            this.littleEndian = true;

            const binaryParser = new BinaryParser$1(new DataView(data), this.littleEndian);
            let magic = binaryParser.getUInt();
            if (magic === BIGWIG_MAGIC_LTH$1) {
                this.type = "bigwig";
            } else if (magic === BIGBED_MAGIC_LTH$1) {
                this.type = "bigbed";
            } else {
                //Try big endian order
                this.littleEndian = false;

                binaryParser.littleEndian = false;
                binaryParser.position = 0;
                let magic = binaryParser.getUInt();

                if (magic === BIGWIG_MAGIC_HTL) {
                    this.type = "bigwig";
                } else if (magic === BIGBED_MAGIC_HTL) {
                    this.type = "bigbed";
                } else ;
            }
            // Table 5  "Common header for bigwig and bigbed files"
            header = {
                bwVersion: binaryParser.getUShort(),
                nZoomLevels: binaryParser.getUShort(),
                chromTreeOffset: binaryParser.getLong(),
                fullDataOffset: binaryParser.getLong(),
                fullIndexOffset: binaryParser.getLong(),
                fieldCount: binaryParser.getUShort(),
                definedFieldCount: binaryParser.getUShort(),
                autoSqlOffset: binaryParser.getLong(),
                totalSummaryOffset: binaryParser.getLong(),
                uncompressBuffSize: binaryParser.getInt(),
                extensionOffset: binaryParser.getLong()
            };

            // Read the next chunk containing zoom headers, autosql, and total summary if present.  TotalSummary size = 40 bytes
            const startOffset = BBFILE_HEADER_SIZE;
            const size = header.totalSummaryOffset > 0 ?
                header.totalSummaryOffset - startOffset + 40 :
                Math.min(header.fullDataOffset, header.chromTreeOffset) - startOffset;
            let range = {
                start: startOffset,
                size: size
            };
            data = await this.loader.loadArrayBuffer(this.path, buildOptions(this.config, {range: range}));
            const extHeaderParser = new BinaryParser$1(new DataView(data), this.littleEndian);

            // Load zoom headers, store in order of decreasing reduction level (increasing resolution)
            const nZooms = header.nZoomLevels;
            this.zoomLevelHeaders = [];
            this.firstZoomDataOffset = Number.MAX_SAFE_INTEGER;
            for (let i = 1; i <= nZooms; i++) {
                const zoomNumber = nZooms - i;
                const zlh = new ZoomLevelHeader(zoomNumber, extHeaderParser);
                this.firstZoomDataOffset = Math.min(zlh.dataOffset, this.firstZoomDataOffset);
                this.zoomLevelHeaders[zoomNumber] = zlh;
            }

            // Autosql
            if (header.autoSqlOffset > 0) {
                extHeaderParser.position = header.autoSqlOffset - startOffset;
                const autoSqlString = extHeaderParser.getString();
                if (autoSqlString) {
                    this.autoSql = parseAutoSQL(autoSqlString);
                }
            }

            // Total summary
            if (header.totalSummaryOffset > 0) {
                extHeaderParser.position = header.totalSummaryOffset - startOffset;
                this.totalSummary = new BWTotalSummary(extHeaderParser);
            }

            this.chromTree = new ChromTree(this.path, this.config, header.chromTreeOffset, this.loader);
            await this.chromTree.init();

            // Estimate feature density from dataCount (bigbed only)
            if ("bigbed" === this.type) {
                const dataCount = await this.#readDataCount(header.fullDataOffset);
                this.featureDensity = dataCount / await this.chromTree.estimateGenomeSize();
            }

            this.header = header;

            //extension
            if (header.extensionOffset > 0) {
                await this.loadExtendedHeader(header.extensionOffset);
            }
            return this.header
        }
    }

    async #readDataCount(offset) {
        const data = await this.loader.loadArrayBuffer(this.path, buildOptions(this.config, {
            range: {
                start: offset,
                size: 4
            }
        }));
        const binaryParser = new BinaryParser$1(new DataView(data), this.littleEndian);
        return binaryParser.getInt()
    }


    async loadExtendedHeader(offset) {

        let data = await this.loader.loadArrayBuffer(this.path, buildOptions(this.config, {
            range: {
                start: offset,
                size: BBFILE_EXTENDED_HEADER_HEADER_SIZE
            }
        }));
        let binaryParser = new BinaryParser$1(new DataView(data), this.littleEndian);
        binaryParser.getUShort();
        const extraIndexCount = binaryParser.getUShort();
        const extraIndexListOffset = binaryParser.getLong();
        if (extraIndexCount === 0) return

        let sz = extraIndexCount * (2 + 2 + 8 + 4 + 10 * (2 + 2));
        data = await this.loader.loadArrayBuffer(this.path, buildOptions(this.config, {
            range: {
                start: extraIndexListOffset,
                size: sz
            }
        }));
        binaryParser = new BinaryParser$1(new DataView(data), this.littleEndian);

        const type = [];
        const reserved = [];
        const indexOffset = [];
        for (let i = 0; i < extraIndexCount; i++) {

            type.push(binaryParser.getUShort());

            const fc = binaryParser.getUShort();

            indexOffset.push(binaryParser.getLong());
            reserved.push(binaryParser.getInt());

            for (let j = 0; j < fc; j++) {
                binaryParser.getUShort();

                //const field = this.autoSql.fields[fieldId]
                //console.log(field)

                reserved.push(binaryParser.getUShort());
            }
        }
        this.header.extraIndexCount = extraIndexCount;
        this.header.extraIndexOffsets = indexOffset;
    }

    async loadRPTree(offset) {

        let rpTree = this.rpTreeCache.get(offset);
        if (rpTree) {
            return rpTree
        } else {
            rpTree = new RPTree(this.path, this.config, offset, this.loader);
            await rpTree.init();
            this.rpTreeCache.set(offset, rpTree);
            return rpTree
        }
    }

    async getType() {
        await this.loadHeader();
        return this.type
    }

    async getTrackType() {
        await this.loadHeader();
        if (this.type === "bigwig") {
            return "wig"
        } else {
            return this.autoSql && ("interact" === this.autoSql.table || "chromatinInteract" === this.autoSql.table) ? "interact" : "annotation"
        }
    }

    /**
     * Directly load features given a file offset and size.  Added to support search index.
     * @param offset
     * @param size
     * @private
     */
    async _loadFeaturesForRange(offset, size) {

        const arrayBuffer = await this.loader.loadArrayBuffer(this.config.url, buildOptions(this.config, {
            range: {
                start: offset,
                size: size
            }
        }));

        const uint8Array = new Uint8Array(arrayBuffer);
        const plain = (this.header.uncompressBuffSize > 0) ? inflate_1$3(uint8Array) : uint8Array;
        const decodeFunction = getBedDataDecoder.call(this);
        const features = [];
        await decodeFunction.call(this, new DataView(plain.buffer), 0, 0, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, features);
        return features

    }
}


class ZoomLevelHeader {
    constructor(index, byteBuffer) {
        this.index = index;
        this.reductionLevel = byteBuffer.getUInt();
        this.reserved = byteBuffer.getInt();
        this.dataOffset = byteBuffer.getLong();
        this.indexOffset = byteBuffer.getLong();
    }
}

class BWTotalSummary {

    constructor(byteBuffer) {
        if (byteBuffer) {
            this.basesCovered = byteBuffer.getLong();
            this.minVal = byteBuffer.getDouble();
            this.maxVal = byteBuffer.getDouble();
            this.sumData = byteBuffer.getDouble();
            this.sumSquares = byteBuffer.getDouble();
            computeStats.call(this);
        } else {
            this.basesCovered = 0;
            this.minVal = 0;
            this.maxVal = 0;
            this.sumData = 0;
            this.sumSquares = 0;
            this.mean = 0;
            this.stddev = 0;
        }
    }
}

function computeStats() {
    let n = this.basesCovered;
    if (n > 0) {
        this.mean = this.sumData / n;
        this.stddev = Math.sqrt(this.sumSquares / (n - 1));

        let min = this.minVal < 0 ? this.mean - 2 * this.stddev : 0,
            max = this.maxVal > 0 ? this.mean + 2 * this.stddev : 0;

        this.defaultRange = {
            min: min,
            max: max
        };
    }
}

function zoomLevelForScale(bpPerPixel, zoomLevelHeaders) {
    let level;
    for (let i = 0; i < zoomLevelHeaders.length; i++) {
        const zl = zoomLevelHeaders[i];
        if (zl.reductionLevel < bpPerPixel) {
            level = zl;
            break
        }
    }
    return level
}


async function decodeWigData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, windowFunction, littleEndian) {

    const binaryParser = new BinaryParser$1(data, littleEndian);
    const chromId = binaryParser.getInt();
    const blockStart = binaryParser.getInt();
    let chromStart = blockStart;
    let chromEnd = binaryParser.getInt();
    const itemStep = binaryParser.getInt();
    const itemSpan = binaryParser.getInt();
    const type = binaryParser.getByte();
    binaryParser.getByte();
    let itemCount = binaryParser.getUShort();

    if (chromId >= chrIdx1 && chromId <= chrIdx2) {

        let idx = 0;
        while (itemCount-- > 0) {
            let value;
            switch (type) {
                case 1:
                    chromStart = binaryParser.getInt();
                    chromEnd = binaryParser.getInt();
                    value = binaryParser.getFloat();
                    break
                case 2:
                    chromStart = binaryParser.getInt();
                    value = binaryParser.getFloat();
                    chromEnd = chromStart + itemSpan;
                    break
                case 3:  // Fixed step
                    value = binaryParser.getFloat();
                    chromStart = blockStart + idx * itemStep;
                    chromEnd = chromStart + itemSpan;
                    idx++;
                    break
            }

            if (chromId < chrIdx1 || (chromId === chrIdx1 && chromEnd < bpStart)) continue
            else if (chromId > chrIdx2 || (chromId === chrIdx2 && chromStart >= bpEnd)) break

            if (Number.isFinite(value)) {
                const chr = await this.chromTree.getNameForId(chromId);
                featureArray.push({chr: chr, start: chromStart, end: chromEnd, value: value});
            }
        }
    }
}

function getBedDataDecoder() {

    const minSize = 3 * 4 + 1;   // Minimum # of bytes required for a bed record
    const decoder = getDecoder(this.header.definedFieldCount, this.header.fieldCount, this.autoSql, this.format);
    return async function (data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray) {

        const binaryParser = new BinaryParser$1(data, this.littleEndian);
        while (binaryParser.remLength() >= minSize) {

            const chromId = binaryParser.getInt();
            const chr = await this.chromTree.getNameForId(chromId);
            const chromStart = binaryParser.getInt();
            const chromEnd = binaryParser.getInt();
            const rest = binaryParser.getString();
            if (chromId < chrIdx1 || (chromId === chrIdx1 && chromEnd < bpStart)) continue
            else if (chromId > chrIdx2 || (chromId === chrIdx2 && chromStart >= bpEnd)) break

            if (chromEnd > 0) {
                const feature = {chr: chr, start: chromStart, end: chromEnd};
                featureArray.push(feature);
                const tokens = rest.split("\t");
                decoder(feature, tokens);
            }
        }
    }
}

async function decodeZoomData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, windowFunction, littleEndian) {

    const binaryParser = new BinaryParser$1(data, littleEndian);
    const minSize = 8 * 4;  // Minimum # of bytes required for a zoom record


    while (binaryParser.remLength() >= minSize) {
        const chromId = binaryParser.getInt();
        const chromStart = binaryParser.getInt();
        const chromEnd = binaryParser.getInt();
        const validCount = binaryParser.getInt();
        const minVal = binaryParser.getFloat();
        const maxVal = binaryParser.getFloat();
        const sumData = binaryParser.getFloat();
        binaryParser.getFloat();
        let value;
        switch (windowFunction) {
            case "min":
                value = minVal;
                break
            case "max":
                value = maxVal;
                break
            default:
                value = validCount === 0 ? 0 : sumData / validCount;
        }

        if (chromId < chrIdx1 || (chromId === chrIdx1 && chromEnd < bpStart)) continue
        else if (chromId > chrIdx2 || (chromId === chrIdx2 && chromStart >= bpEnd)) break


        if (Number.isFinite(value)) {
            const chr = await this.chromTree.getNameForId(chromId);
            featureArray.push({chr: chr, start: chromStart, end: chromEnd, value: value});


        }
    }
}

class DataBuffer {

    constructor(data) {
        this.data = data;
    }

    /**
     * igvxhr interface
     * @param ignore
     * @param options
     * @returns {any}
     */
    loadArrayBuffer(ignore, options) {
        const range = options.range;
        return range ? this.data.slice(range.start, range.start + range.size) : this.data
    }

    /**
     * BufferedReader interface
     *
     * @param requestedRange - byte rangeas {start, size}
     * @param fulfill - function to receive result
     * @param asUint8 - optional flag to return result as an UInt8Array
     */
    async dataViewForRange(requestedRange, asUint8) {
        const len = Math.min(this.data.byteLength - requestedRange.start, requestedRange.size);
        return asUint8 ?
            new Uint8Array(this.data, requestedRange.start, len) :
            new DataView(this.data, requestedRange.start, len)
    }
}

class BWSource extends BaseFeatureSource {

    queryable = true
    #wgValues = {}
    windowFunctions = ["mean", "min", "max", "none"]

    constructor(config, genome) {
        super(genome);
        this.reader = new BWReader(config, genome);
        this.genome = genome;
        this.format = config.format || "bigwig";
    }

    async getFeatures({chr, start, end, bpPerPixel, windowFunction}) {

        await this.reader.loadHeader();
        const isBigWig = this.reader.type === "bigwig";

        let features;
        if ("all" === chr.toLowerCase()) {
            const wgChromosomeNames = this.genome.wgChromosomeNames;
            features = isBigWig && wgChromosomeNames? await this.getWGValues(wgChromosomeNames, windowFunction, bpPerPixel) : [];
        } else {
            features = await this.reader.readFeatures(chr, start, chr, end, bpPerPixel, windowFunction);
        }

        if (!isBigWig) {
            pack(features);
        }
        return features
    }

    async getHeader() {
        return this.reader.loadHeader()
    }

    async defaultVisibilityWindow() {
        if (this.reader.type === "bigwig") {
            return -1
        } else {
            return this.reader.featureDensity ? Math.floor(10000 / this.reader.featureDensity) : -1
        }

    }

    async getWGValues(wgChromosomeNames, windowFunction, bpPerPixel) {

        const genome = this.genome;
        const cached = this.#wgValues[windowFunction];
        if (cached && cached.bpPerPixel > 0.8 * bpPerPixel && cached.bpPerPixel < 1.2 * bpPerPixel) {
            return cached.values
        } else {
            const features = await this.reader.readWGFeatures(wgChromosomeNames, bpPerPixel, windowFunction);
            let wgValues = [];
            for (let f of features) {
                const chr = f.chr;
                const offset = genome.getCumulativeOffset(chr);
                if (undefined === offset) continue
                const wgFeature = Object.assign({}, f);
                wgFeature.chr = "all";
                wgFeature.start = offset + f.start;
                wgFeature.end = offset + f.end;
                wgFeature._f = f;
                wgValues.push(wgFeature);
            }
            wgValues.sort((a, b) => a.start - b.start);
            this.#wgValues[windowFunction] = {values: wgValues, bpPerPixel};
            return wgValues
        }
    }

    supportsWholeGenome() {
        return this.reader.type === "bigwig"
    }

    async trackType() {
        return this.reader.getTrackType()
    }

    get searchable() {
        return this.reader.searchable
    }

    async search(term) {
        return this.reader.search(term)
    }
}

const shim = .01;
const colorStripWidth = 4;
const axesXOffset = colorStripWidth + 1;

function paintAxis(ctx, width, height, colorOrUndefined) {

    let axisMin = this.axisMin; //|| this.dataRange ? this.dataRange.min : 0
    let axisMax = this.axisMax; //|| this.dataRange ? this.dataRange.max : undefined
    if (undefined === axisMax && this.dataRange) {
        axisMin = this.dataRange.min || 0;
        axisMax = this.dataRange.max;
    }
    if (undefined === axisMax) {
        return
    }

    IGVGraphics.fillRect(ctx, 0, 0, width, height, {fillStyle: 'white'});
    if (colorOrUndefined) {
        IGVGraphics.fillRect(ctx, width - colorStripWidth - 2, 0, colorStripWidth, height, {fillStyle: colorOrUndefined});
    }

    const flipAxis = (undefined === this.flipAxis) ? false : this.flipAxis;

    const xTickStart = 0.95 * width - 8 - axesXOffset;
    const xTickEnd = 0.95 * width - axesXOffset;

    const properties =
        {
            font: 'normal 10px Arial',
            textAlign: 'right',
            fillStyle: 'black',
            strokeStyle: 'black',
        };

    // tick
    IGVGraphics.strokeLine(ctx, xTickStart, shim * height, xTickEnd, shim * height, properties);
    IGVGraphics.fillText(ctx, prettyPrint(flipAxis ? axisMin : axisMax), xTickStart + 4, shim * height + 12, properties);

    const y = (1.0 - shim) * height;

    // tick
    IGVGraphics.strokeLine(ctx, xTickStart, y, xTickEnd, y, properties);
    IGVGraphics.fillText(ctx, prettyPrint(flipAxis ? axisMax : axisMin), xTickStart + 4, y - 4, properties);

    // vertical axis
    IGVGraphics.strokeLine(ctx, xTickEnd, shim * height, xTickEnd, y, properties);

    function prettyPrint(number) {

        if (Number.isInteger(number)) {
            return number
        } else if (number % 1 === 0) {   // Number can be represented exactly as an integer
            return number
        } else if (Math.abs(number) >= 10) {
            return number.toFixed()
        } else if (Math.abs(number) >= 1) {
            return number.toFixed(1)
        } else if (Math.abs(number) >= 0.1) {
            return number.toFixed(2)
        } else {
            return number.toExponential(1)
        }
    }
}

const ColorScaleFactory = {

    fromJson: (obj) => {
        switch (obj.type) {
            case 'gradient':
                return new GradientColorScale(obj)
            case 'doubleGradient':
            case 'diverging':
                return new DivergingGradientScale(obj)
            default:
                throw Error("Unknown color scale type: " + obj)
        }
    },

    defaultGradientScale: function (min, max) {

        return new GradientColorScale({
            "type": "doubleGradient",
            "min": min,
            "max": max,
            "minColor": "rgb(46,56,183)",
            "maxColor": "rgb(164,0,30)"
        })
    },

    defaultDivergingScale: function (min, mid, max) {
        return new DivergingGradientScale({
            "type": "doubleGradient",
            "min": 0,
            "mid": 0.25,
            "max": 0.5,
            "minColor": "rgb(46,56,183)",
            "midColor": "white",
            "maxColor": "rgb(164,0,30)"
        })
    }
};

/**
 *
 * @param cs - object containing
 * 1) array of threshold values defining bin boundaries in ascending order
 * 2) array of colors for bins  (length == thresholds.length + 1)
 * @constructor
 */
class BinnedColorScale {
    constructor(cs) {
        this.thresholds = cs.thresholds;
        this.colors = cs.colors;
    }

    getColor(value) {

        for (let threshold of this.thresholds) {
            if (value < threshold) {
                return this.colors[this.thresholds.indexOf(threshold)]
            }
        }

        return this.colors[this.colors.length - 1]
    }
}


class GradientColorScale {
    constructor(config) {
        this.type = 'gradient';
        const fixed = {
            min: config.min !== undefined ? config.min : config.low,
            max: config.max !== undefined ? config.max : config.high,
            minColor: config.minColor || config.lowColor,
            maxColor: config.maxColor || config.highColor
        };
        this.setProperties(fixed);
    }

    setProperties({min, max, minColor, maxColor}) {
        this.type = 'gradient';
        this.min = min;
        this.max = max;
        this._lowColor = minColor;
        this._highColor = maxColor;
        this.lowComponents = IGVColor.rgbComponents(minColor);
        this.highComponents = IGVColor.rgbComponents(maxColor);
    }

    get minColor() {
        return this._lowColor
    }

    set minColor(c) {
        this._lowColor = c;
        this.lowComponents = IGVColor.rgbComponents(c);
    }

    get maxColor() {
        return this._highColor
    }

    set maxColor(c) {
        this._highColor = c;
        this.highComponents = IGVColor.rgbComponents(c);
    }

    getColor(value) {

        if (value <= this.min) return this.minColor
        else if (value >= this.max) return this.maxColor

        const frac = (value - this.min) / (this.max - this.min);
        const r = Math.floor(this.lowComponents[0] + frac * (this.highComponents[0] - this.lowComponents[0]));
        const g = Math.floor(this.lowComponents[1] + frac * (this.highComponents[1] - this.lowComponents[1]));
        const b = Math.floor(this.lowComponents[2] + frac * (this.highComponents[2] - this.lowComponents[2]));

        return "rgb(" + r + "," + g + "," + b + ")"
    }

    /**
     * Return a simple json-like object, not a literaly json string
     * @returns {{max, min, maxColor, minColor}}
     */
    toJson() {
        return {
            type: this.type,
            min: this.min,
            max: this.max,
            minColor: this.minColor,
            maxColor: this.maxColor
        }
    }

    clone() {
        return new GradientColorScale(this.toJson())
    }

}

class DivergingGradientScale {

    constructor(json) {
        this.type = 'diverging';
        this.lowGradientScale = new GradientColorScale({
            minColor: json.minColor || json.lowColor,
            maxColor: json.midColor,
            min: json.min !== undefined ? json.min : json.low,
            max: json.mid
        });
        this.highGradientScale = new GradientColorScale({
            minColor: json.midColor,
            maxColor: json.maxColor || json.highColor,
            min: json.mid,
            max: json.max !== undefined ? json.max : json.high
        });
    }

    getColor(value) {
        if (value < this.mid) {
            return this.lowGradientScale.getColor(value)
        } else {
            return this.highGradientScale.getColor(value)
        }
    }

    get min() {
        return this.lowGradientScale.min
    }

    set min(v) {
        this.lowGradientScale.min = v;
    }

    get max() {
        return this.highGradientScale.max
    }

    set max(v) {
        this.highGradientScale.max = v;
    }

    get mid() {
        return this.lowGradientScale.max
    }

    set mid(v) {
        this.lowGradientScale.max = v;
        this.highGradientScale.min = v;
    }

    get minColor() {
        return this.lowGradientScale.minColor
    }

    set minColor(c) {
        this.lowGradientScale.minColor = c;
    }

    get maxColor() {
        return this.highGradientScale.maxColor
    }

    set maxColor(c) {
        this.highGradientScale.maxColor = c;
    }

    get midColor() {
        return this.lowGradientScale.maxColor
    }

    set midColor(c) {
        this.lowGradientScale.maxColor = c;
        this.highGradientScale.minColor = c;
    }


    /**
     * Return a simple json-like object, not a literaly json string
     * @returns {{max, mid, min, maxColor, midColor, minColor}}
     */
    toJson() {
        return {
            type: this.type,
            min: this.min,
            mid: this.mid,
            max: this.max,
            minColor: this.minColor,
            midColor: this.midColor,
            maxColor: this.maxColor
        }
    }

    clone() {
        const json = this.toJson();
        return new DivergingGradientScale(json)
    }
}

class ConstantColorScale {
    constructor(color) {
        this.color = color;
    }

    getColor() {
        return this.color
    }
}

/*!
 * vanilla-picker v2.12.3
 * https://vanilla-picker.js.org
 *
 * Copyright 2017-2024 Andreas Borgen (https://github.com/Sphinxxxx), Adam Brooks (https://github.com/dissimulate)
 * Released under the ISC license.
 */
var classCallCheck$1 = function (instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
};

var createClass$1 = function () {
  function defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i];
      descriptor.enumerable = descriptor.enumerable || false;
      descriptor.configurable = true;
      if ("value" in descriptor) descriptor.writable = true;
      Object.defineProperty(target, descriptor.key, descriptor);
    }
  }

  return function (Constructor, protoProps, staticProps) {
    if (protoProps) defineProperties(Constructor.prototype, protoProps);
    if (staticProps) defineProperties(Constructor, staticProps);
    return Constructor;
  };
}();

var slicedToArray$1 = function () {
  function sliceIterator(arr, i) {
    var _arr = [];
    var _n = true;
    var _d = false;
    var _e = undefined;

    try {
      for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
        _arr.push(_s.value);

        if (i && _arr.length === i) break;
      }
    } catch (err) {
      _d = true;
      _e = err;
    } finally {
      try {
        if (!_n && _i["return"]) _i["return"]();
      } finally {
        if (_d) throw _e;
      }
    }

    return _arr;
  }

  return function (arr, i) {
    if (Array.isArray(arr)) {
      return arr;
    } else if (Symbol.iterator in Object(arr)) {
      return sliceIterator(arr, i);
    } else {
      throw new TypeError("Invalid attempt to destructure non-iterable instance");
    }
  };
}();

String.prototype.startsWith = String.prototype.startsWith || function (needle) {
    return this.indexOf(needle) === 0;
};
String.prototype.padStart = String.prototype.padStart || function (len, pad) {
    var str = this;while (str.length < len) {
        str = pad + str;
    }return str;
};

var colorNames$1 = { cb: '0f8ff', tqw: 'aebd7', q: '-ffff', qmrn: '7fffd4', zr: '0ffff', bg: '5f5dc', bsq: 'e4c4', bck: '---', nch: 'ebcd', b: '--ff', bvt: '8a2be2', brwn: 'a52a2a', brw: 'deb887', ctb: '5f9ea0', hrt: '7fff-', chcT: 'd2691e', cr: '7f50', rnw: '6495ed', crns: '8dc', crms: 'dc143c', cn: '-ffff', Db: '--8b', Dcn: '-8b8b', Dgnr: 'b8860b', Dgr: 'a9a9a9', Dgrn: '-64-', Dkhk: 'bdb76b', Dmgn: '8b-8b', Dvgr: '556b2f', Drng: '8c-', Drch: '9932cc', Dr: '8b--', Dsmn: 'e9967a', Dsgr: '8fbc8f', DsTb: '483d8b', DsTg: '2f4f4f', Dtrq: '-ced1', Dvt: '94-d3', ppnk: '1493', pskb: '-bfff', mgr: '696969', grb: '1e90ff', rbrc: 'b22222', rwht: 'af0', stg: '228b22', chs: '-ff', gnsb: 'dcdcdc', st: '8f8ff', g: 'd7-', gnr: 'daa520', gr: '808080', grn: '-8-0', grnw: 'adff2f', hnw: '0fff0', htpn: '69b4', nnr: 'cd5c5c', ng: '4b-82', vr: '0', khk: '0e68c', vnr: 'e6e6fa', nrb: '0f5', wngr: '7cfc-', mnch: 'acd', Lb: 'add8e6', Lcr: '08080', Lcn: 'e0ffff', Lgnr: 'afad2', Lgr: 'd3d3d3', Lgrn: '90ee90', Lpnk: 'b6c1', Lsmn: 'a07a', Lsgr: '20b2aa', Lskb: '87cefa', LsTg: '778899', Lstb: 'b0c4de', Lw: 'e0', m: '-ff-', mgrn: '32cd32', nn: 'af0e6', mgnt: '-ff', mrn: '8--0', mqm: '66cdaa', mmb: '--cd', mmrc: 'ba55d3', mmpr: '9370db', msg: '3cb371', mmsT: '7b68ee', '': '-fa9a', mtr: '48d1cc', mmvt: 'c71585', mnLb: '191970', ntc: '5fffa', mstr: 'e4e1', mccs: 'e4b5', vjw: 'dead', nv: '--80', c: 'df5e6', v: '808-0', vrb: '6b8e23', rng: 'a5-', rngr: '45-', rch: 'da70d6', pgnr: 'eee8aa', pgrn: '98fb98', ptrq: 'afeeee', pvtr: 'db7093', ppwh: 'efd5', pchp: 'dab9', pr: 'cd853f', pnk: 'c0cb', pm: 'dda0dd', pwrb: 'b0e0e6', prp: '8-080', cc: '663399', r: '--', sbr: 'bc8f8f', rb: '4169e1', sbrw: '8b4513', smn: 'a8072', nbr: '4a460', sgrn: '2e8b57', ssh: '5ee', snn: 'a0522d', svr: 'c0c0c0', skb: '87ceeb', sTb: '6a5acd', sTgr: '708090', snw: 'afa', n: '-ff7f', stb: '4682b4', tn: 'd2b48c', t: '-8080', thst: 'd8bfd8', tmT: '6347', trqs: '40e0d0', vt: 'ee82ee', whT: '5deb3', wht: '', hts: '5f5f5', w: '-', wgrn: '9acd32' };

function printNum$1(num) {
    var decs = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;

    var str = decs > 0 ? num.toFixed(decs).replace(/0+$/, '').replace(/\.$/, '') : num.toString();
    return str || '0';
}

var Color$1 = function () {
    function Color(r, g, b, a) {
        classCallCheck$1(this, Color);


        var that = this;
        function parseString(input) {

            if (input.startsWith('hsl')) {
                var _input$match$map = input.match(/([\-\d\.e]+)/g).map(Number),
                    _input$match$map2 = slicedToArray$1(_input$match$map, 4),
                    h = _input$match$map2[0],
                    s = _input$match$map2[1],
                    l = _input$match$map2[2],
                    _a = _input$match$map2[3];

                if (_a === undefined) {
                    _a = 1;
                }

                h /= 360;
                s /= 100;
                l /= 100;
                that.hsla = [h, s, l, _a];
            } else if (input.startsWith('rgb')) {
                var _input$match$map3 = input.match(/([\-\d\.e]+)/g).map(Number),
                    _input$match$map4 = slicedToArray$1(_input$match$map3, 4),
                    _r = _input$match$map4[0],
                    _g = _input$match$map4[1],
                    _b = _input$match$map4[2],
                    _a2 = _input$match$map4[3];

                if (_a2 === undefined) {
                    _a2 = 1;
                }

                that.rgba = [_r, _g, _b, _a2];
            } else {
                if (input.startsWith('#')) {
                    that.rgba = Color.hexToRgb(input);
                } else {
                    that.rgba = Color.nameToRgb(input) || Color.hexToRgb(input);
                }
            }
        }

        if (r === undefined) ; else if (Array.isArray(r)) {
            this.rgba = r;
        } else if (b === undefined) {
            var color = r && '' + r;
            if (color) {
                parseString(color.toLowerCase());
            }
        } else {
            this.rgba = [r, g, b, a === undefined ? 1 : a];
        }
    }

    createClass$1(Color, [{
        key: 'printRGB',
        value: function printRGB(alpha) {
            var rgb = alpha ? this.rgba : this.rgba.slice(0, 3),
                vals = rgb.map(function (x, i) {
                return printNum$1(x, i === 3 ? 3 : 0);
            });

            return alpha ? 'rgba(' + vals + ')' : 'rgb(' + vals + ')';
        }
    }, {
        key: 'printHSL',
        value: function printHSL(alpha) {
            var mults = [360, 100, 100, 1],
                suff = ['', '%', '%', ''];

            var hsl = alpha ? this.hsla : this.hsla.slice(0, 3),
                vals = hsl.map(function (x, i) {
                return printNum$1(x * mults[i], i === 3 ? 3 : 1) + suff[i];
            });

            return alpha ? 'hsla(' + vals + ')' : 'hsl(' + vals + ')';
        }
    }, {
        key: 'printHex',
        value: function printHex(alpha) {
            var hex = this.hex;
            return alpha ? hex : hex.substring(0, 7);
        }
    }, {
        key: 'rgba',
        get: function get() {
            if (this._rgba) {
                return this._rgba;
            }
            if (!this._hsla) {
                throw new Error('No color is set');
            }

            return this._rgba = Color.hslToRgb(this._hsla);
        },
        set: function set(rgb) {
            if (rgb.length === 3) {
                rgb[3] = 1;
            }

            this._rgba = rgb;
            this._hsla = null;
        }
    }, {
        key: 'rgbString',
        get: function get() {
            return this.printRGB();
        }
    }, {
        key: 'rgbaString',
        get: function get() {
            return this.printRGB(true);
        }
    }, {
        key: 'hsla',
        get: function get() {
            if (this._hsla) {
                return this._hsla;
            }
            if (!this._rgba) {
                throw new Error('No color is set');
            }

            return this._hsla = Color.rgbToHsl(this._rgba);
        },
        set: function set(hsl) {
            if (hsl.length === 3) {
                hsl[3] = 1;
            }

            this._hsla = hsl;
            this._rgba = null;
        }
    }, {
        key: 'hslString',
        get: function get() {
            return this.printHSL();
        }
    }, {
        key: 'hslaString',
        get: function get() {
            return this.printHSL(true);
        }
    }, {
        key: 'hex',
        get: function get() {
            var rgb = this.rgba,
                hex = rgb.map(function (x, i) {
                return i < 3 ? x.toString(16) : Math.round(x * 255).toString(16);
            });

            return '#' + hex.map(function (x) {
                return x.padStart(2, '0');
            }).join('');
        },
        set: function set(hex) {
            this.rgba = Color.hexToRgb(hex);
        }
    }], [{
        key: 'hexToRgb',
        value: function hexToRgb(input) {

            var hex = (input.startsWith('#') ? input.slice(1) : input).replace(/^(\w{3})$/, '$1F').replace(/^(\w)(\w)(\w)(\w)$/, '$1$1$2$2$3$3$4$4').replace(/^(\w{6})$/, '$1FF');

            if (!hex.match(/^([0-9a-fA-F]{8})$/)) {
                throw new Error('Unknown hex color; ' + input);
            }

            var rgba = hex.match(/^(\w\w)(\w\w)(\w\w)(\w\w)$/).slice(1).map(function (x) {
                return parseInt(x, 16);
            });

            rgba[3] = rgba[3] / 255;
            return rgba;
        }
    }, {
        key: 'nameToRgb',
        value: function nameToRgb(input) {

            var hash = input.toLowerCase().replace('at', 'T').replace(/[aeiouyldf]/g, '').replace('ght', 'L').replace('rk', 'D').slice(-5, 4),
                hex = colorNames$1[hash];
            return hex === undefined ? hex : Color.hexToRgb(hex.replace(/\-/g, '00').padStart(6, 'f'));
        }
    }, {
        key: 'rgbToHsl',
        value: function rgbToHsl(_ref) {
            var _ref2 = slicedToArray$1(_ref, 4),
                r = _ref2[0],
                g = _ref2[1],
                b = _ref2[2],
                a = _ref2[3];

            r /= 255;
            g /= 255;
            b /= 255;

            var max = Math.max(r, g, b),
                min = Math.min(r, g, b);
            var h = void 0,
                s = void 0,
                l = (max + min) / 2;

            if (max === min) {
                h = s = 0;
            } else {
                var d = max - min;
                s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
                switch (max) {
                    case r:
                        h = (g - b) / d + (g < b ? 6 : 0);break;
                    case g:
                        h = (b - r) / d + 2;break;
                    case b:
                        h = (r - g) / d + 4;break;
                }

                h /= 6;
            }

            return [h, s, l, a];
        }
    }, {
        key: 'hslToRgb',
        value: function hslToRgb(_ref3) {
            var _ref4 = slicedToArray$1(_ref3, 4),
                h = _ref4[0],
                s = _ref4[1],
                l = _ref4[2],
                a = _ref4[3];

            var r = void 0,
                g = void 0,
                b = void 0;

            if (s === 0) {
                r = g = b = l;
            } else {
                var hue2rgb = function hue2rgb(p, q, t) {
                    if (t < 0) t += 1;
                    if (t > 1) t -= 1;
                    if (t < 1 / 6) return p + (q - p) * 6 * t;
                    if (t < 1 / 2) return q;
                    if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
                    return p;
                };

                var q = l < 0.5 ? l * (1 + s) : l + s - l * s,
                    p = 2 * l - q;

                r = hue2rgb(p, q, h + 1 / 3);
                g = hue2rgb(p, q, h);
                b = hue2rgb(p, q, h - 1 / 3);
            }

            var rgba = [r * 255, g * 255, b * 255].map(Math.round);
            rgba[3] = a;

            return rgba;
        }
    }]);
    return Color;
}();

var EventBucket$1 = function () {
    function EventBucket() {
        classCallCheck$1(this, EventBucket);

        this._events = [];
    }

    createClass$1(EventBucket, [{
        key: 'add',
        value: function add(target, type, handler) {
            target.addEventListener(type, handler, false);
            this._events.push({
                target: target,
                type: type,
                handler: handler
            });
        }
    }, {
        key: 'remove',
        value: function remove(target, type, handler) {
            this._events = this._events.filter(function (e) {
                var isMatch = true;
                if (target && target !== e.target) {
                    isMatch = false;
                }
                if (type && type !== e.type) {
                    isMatch = false;
                }
                if (handler && handler !== e.handler) {
                    isMatch = false;
                }

                if (isMatch) {
                    EventBucket._doRemove(e.target, e.type, e.handler);
                }
                return !isMatch;
            });
        }
    }, {
        key: 'destroy',
        value: function destroy() {
            this._events.forEach(function (e) {
                return EventBucket._doRemove(e.target, e.type, e.handler);
            });
            this._events = [];
        }
    }], [{
        key: '_doRemove',
        value: function _doRemove(target, type, handler) {
            target.removeEventListener(type, handler, false);
        }
    }]);
    return EventBucket;
}();

function parseHTML$1(htmlString) {

    var div = document.createElement('div');
    div.innerHTML = htmlString;
    return div.firstElementChild;
}

function dragTrack$1(eventBucket, area, callback) {
    var dragging = false;

    function clamp(val, min, max) {
        return Math.max(min, Math.min(val, max));
    }

    function onMove(e, info, starting) {
        if (starting) {
            dragging = true;
        }
        if (!dragging) {
            return;
        }

        e.preventDefault();

        var bounds = area.getBoundingClientRect(),
            w = bounds.width,
            h = bounds.height,
            x = info.clientX,
            y = info.clientY;

        var relX = clamp(x - bounds.left, 0, w),
            relY = clamp(y - bounds.top, 0, h);

        callback(relX / w, relY / h);
    }

    function onMouse(e, starting) {
        var button = e.buttons === undefined ? e.which : e.buttons;
        if (button === 1) {
            onMove(e, e, starting);
        } else {
            dragging = false;
        }
    }

    function onTouch(e, starting) {
        if (e.touches.length === 1) {
            onMove(e, e.touches[0], starting);
        } else {
            dragging = false;
        }
    }

    eventBucket.add(area, 'mousedown', function (e) {
        onMouse(e, true);
    });
    eventBucket.add(area, 'touchstart', function (e) {
        onTouch(e, true);
    });
    eventBucket.add(window, 'mousemove', onMouse);
    eventBucket.add(area, 'touchmove', onTouch);
    eventBucket.add(window, 'mouseup', function (e) {
        dragging = false;
    });
    eventBucket.add(area, 'touchend', function (e) {
        dragging = false;
    });
    eventBucket.add(area, 'touchcancel', function (e) {
        dragging = false;
    });
}

var BG_TRANSP$1 = 'linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%) 0 0 / 2em 2em,\n                   linear-gradient(45deg, lightgrey 25%,       white 25%,       white 75%, lightgrey 75%) 1em 1em / 2em 2em';
var HUES$1 = 360;

var EVENT_KEY$1 = 'keydown',
    EVENT_CLICK_OUTSIDE$1 = 'mousedown',
    EVENT_TAB_MOVE$1 = 'focusin';

function $$1(selector, context) {
    return (context || document).querySelector(selector);
}

function stopEvent$1(e) {

    e.preventDefault();
    e.stopPropagation();
}
function onKey$1(bucket, target, keys, handler, stop) {
    bucket.add(target, EVENT_KEY$1, function (e) {
        if (keys.indexOf(e.key) >= 0) {
            if (stop) {
                stopEvent$1(e);
            }
            handler(e);
        }
    });
}

var Picker$1 = function () {
    function Picker(options) {
        classCallCheck$1(this, Picker);


        this.settings = {

            popup: 'right',
            layout: 'default',
            alpha: true,
            editor: true,
            editorFormat: 'hex',
            cancelButton: false,
            defaultColor: '#0cf'
        };

        this._events = new EventBucket$1();

        this.onChange = null;

        this.onDone = null;

        this.onOpen = null;

        this.onClose = null;

        this.setOptions(options);
    }

    createClass$1(Picker, [{
        key: 'setOptions',
        value: function setOptions(options) {
            var _this = this;

            if (!options) {
                return;
            }
            var settings = this.settings;

            function transfer(source, target, skipKeys) {
                for (var key in source) {
                    if (skipKeys && skipKeys.indexOf(key) >= 0) {
                        continue;
                    }

                    target[key] = source[key];
                }
            }

            if (options instanceof HTMLElement) {
                settings.parent = options;
            } else {

                if (settings.parent && options.parent && settings.parent !== options.parent) {
                    this._events.remove(settings.parent);
                    this._popupInited = false;
                }

                transfer(options, settings);

                if (options.onChange) {
                    this.onChange = options.onChange;
                }
                if (options.onDone) {
                    this.onDone = options.onDone;
                }
                if (options.onOpen) {
                    this.onOpen = options.onOpen;
                }
                if (options.onClose) {
                    this.onClose = options.onClose;
                }

                var col = options.color || options.colour;
                if (col) {
                    this._setColor(col);
                }
            }

            var parent = settings.parent;
            if (parent && settings.popup && !this._popupInited) {

                var openProxy = function openProxy(e) {
                    return _this.openHandler(e);
                };

                this._events.add(parent, 'click', openProxy);

                onKey$1(this._events, parent, [' ', 'Spacebar', 'Enter'], openProxy);

                this._popupInited = true;
            } else if (options.parent && !settings.popup) {
                this.show();
            }
        }
    }, {
        key: 'openHandler',
        value: function openHandler(e) {
            if (this.show()) {

                e && e.preventDefault();

                this.settings.parent.style.pointerEvents = 'none';

                var toFocus = e && e.type === EVENT_KEY$1 ? this._domEdit : this.domElement;
                setTimeout(function () {
                    return toFocus.focus();
                }, 100);

                if (this.onOpen) {
                    this.onOpen(this.colour);
                }
            }
        }
    }, {
        key: 'closeHandler',
        value: function closeHandler(e) {
            var event = e && e.type;
            var doHide = false;

            if (!e) {
                doHide = true;
            } else if (event === EVENT_CLICK_OUTSIDE$1 || event === EVENT_TAB_MOVE$1) {

                var knownTime = (this.__containedEvent || 0) + 100;
                if (e.timeStamp > knownTime) {
                    doHide = true;
                }
            } else {

                stopEvent$1(e);

                doHide = true;
            }

            if (doHide && this.hide()) {
                this.settings.parent.style.pointerEvents = '';

                if (event !== EVENT_CLICK_OUTSIDE$1) {
                    this.settings.parent.focus();
                }

                if (this.onClose) {
                    this.onClose(this.colour);
                }
            }
        }
    }, {
        key: 'movePopup',
        value: function movePopup(options, open) {

            this.closeHandler();

            this.setOptions(options);
            if (open) {
                this.openHandler();
            }
        }
    }, {
        key: 'setColor',
        value: function setColor(color, silent) {
            this._setColor(color, { silent: silent });
        }
    }, {
        key: '_setColor',
        value: function _setColor(color, flags) {
            if (typeof color === 'string') {
                color = color.trim();
            }
            if (!color) {
                return;
            }

            flags = flags || {};
            var c = void 0;
            try {

                c = new Color$1(color);
            } catch (ex) {
                if (flags.failSilently) {
                    return;
                }
                throw ex;
            }

            if (!this.settings.alpha) {
                var hsla = c.hsla;
                hsla[3] = 1;
                c.hsla = hsla;
            }
            this.colour = this.color = c;
            this._setHSLA(null, null, null, null, flags);
        }
    }, {
        key: 'setColour',
        value: function setColour(colour, silent) {
            this.setColor(colour, silent);
        }
    }, {
        key: 'show',
        value: function show() {
            var parent = this.settings.parent;
            if (!parent) {
                return false;
            }

            if (this.domElement) {
                var toggled = this._toggleDOM(true);

                this._setPosition();

                return toggled;
            }

            var html = this.settings.template || '<div class="picker_wrapper" tabindex="-1"><div class="picker_arrow"></div><div class="picker_hue picker_slider"><div class="picker_selector"></div></div><div class="picker_sl"><div class="picker_selector"></div></div><div class="picker_alpha picker_slider"><div class="picker_selector"></div></div><div class="picker_editor"><input aria-label="Type a color name or hex value"/></div><div class="picker_sample"></div><div class="picker_done"><button>Ok</button></div><div class="picker_cancel"><button>Cancel</button></div></div>';
            var wrapper = parseHTML$1(html);

            this.domElement = wrapper;
            this._domH = $$1('.picker_hue', wrapper);
            this._domSL = $$1('.picker_sl', wrapper);
            this._domA = $$1('.picker_alpha', wrapper);
            this._domEdit = $$1('.picker_editor input', wrapper);
            this._domSample = $$1('.picker_sample', wrapper);
            this._domOkay = $$1('.picker_done button', wrapper);
            this._domCancel = $$1('.picker_cancel button', wrapper);

            wrapper.classList.add('layout_' + this.settings.layout);
            if (!this.settings.alpha) {
                wrapper.classList.add('no_alpha');
            }
            if (!this.settings.editor) {
                wrapper.classList.add('no_editor');
            }
            if (!this.settings.cancelButton) {
                wrapper.classList.add('no_cancel');
            }
            this._ifPopup(function () {
                return wrapper.classList.add('popup');
            });

            this._setPosition();

            if (this.colour) {
                this._updateUI();
            } else {
                this._setColor(this.settings.defaultColor);
            }
            this._bindEvents();

            return true;
        }
    }, {
        key: 'hide',
        value: function hide() {
            return this._toggleDOM(false);
        }
    }, {
        key: 'destroy',
        value: function destroy() {
            this._events.destroy();
            if (this.domElement) {
                this.settings.parent.removeChild(this.domElement);
            }
        }
    }, {
        key: '_bindEvents',
        value: function _bindEvents() {
            var _this2 = this;

            var that = this,
                dom = this.domElement,
                events = this._events;

            function addEvent(target, type, handler) {
                events.add(target, type, handler);
            }

            addEvent(dom, 'click', function (e) {
                return e.preventDefault();
            });

            dragTrack$1(events, this._domH, function (x, y) {
                return that._setHSLA(x);
            });

            dragTrack$1(events, this._domSL, function (x, y) {
                return that._setHSLA(null, x, 1 - y);
            });

            if (this.settings.alpha) {
                dragTrack$1(events, this._domA, function (x, y) {
                    return that._setHSLA(null, null, null, 1 - y);
                });
            }

            var editInput = this._domEdit;
            {
                addEvent(editInput, 'input', function (e) {
                    that._setColor(this.value, { fromEditor: true, failSilently: true });
                });

                addEvent(editInput, 'focus', function (e) {
                    var input = this;

                    if (input.selectionStart === input.selectionEnd) {
                        input.select();
                    }
                });
            }

            this._ifPopup(function () {

                var popupCloseProxy = function popupCloseProxy(e) {
                    return _this2.closeHandler(e);
                };

                addEvent(window, EVENT_CLICK_OUTSIDE$1, popupCloseProxy);
                addEvent(window, EVENT_TAB_MOVE$1, popupCloseProxy);
                onKey$1(events, dom, ['Esc', 'Escape'], popupCloseProxy);

                var timeKeeper = function timeKeeper(e) {
                    _this2.__containedEvent = e.timeStamp;
                };
                addEvent(dom, EVENT_CLICK_OUTSIDE$1, timeKeeper);

                addEvent(dom, EVENT_TAB_MOVE$1, timeKeeper);

                addEvent(_this2._domCancel, 'click', popupCloseProxy);
            });

            var onDoneProxy = function onDoneProxy(e) {
                _this2._ifPopup(function () {
                    return _this2.closeHandler(e);
                });
                if (_this2.onDone) {
                    _this2.onDone(_this2.colour);
                }
            };
            addEvent(this._domOkay, 'click', onDoneProxy);
            onKey$1(events, dom, ['Enter'], onDoneProxy);
        }
    }, {
        key: '_setPosition',
        value: function _setPosition() {
            var parent = this.settings.parent,
                elm = this.domElement;

            if (parent !== elm.parentNode) {
                parent.appendChild(elm);
            }

            this._ifPopup(function (popup) {

                if (getComputedStyle(parent).position === 'static') {
                    parent.style.position = 'relative';
                }

                var cssClass = popup === true ? 'popup_right' : 'popup_' + popup;

                ['popup_top', 'popup_bottom', 'popup_left', 'popup_right'].forEach(function (c) {

                    if (c === cssClass) {
                        elm.classList.add(c);
                    } else {
                        elm.classList.remove(c);
                    }
                });

                elm.classList.add(cssClass);
            });
        }
    }, {
        key: '_setHSLA',
        value: function _setHSLA(h, s, l, a, flags) {
            flags = flags || {};

            var col = this.colour,
                hsla = col.hsla;

            [h, s, l, a].forEach(function (x, i) {
                if (x || x === 0) {
                    hsla[i] = x;
                }
            });
            col.hsla = hsla;

            this._updateUI(flags);

            if (this.onChange && !flags.silent) {
                this.onChange(col);
            }
        }
    }, {
        key: '_updateUI',
        value: function _updateUI(flags) {
            if (!this.domElement) {
                return;
            }
            flags = flags || {};

            var col = this.colour,
                hsl = col.hsla,
                cssHue = 'hsl(' + hsl[0] * HUES$1 + ', 100%, 50%)',
                cssHSL = col.hslString,
                cssHSLA = col.hslaString;

            var uiH = this._domH,
                uiSL = this._domSL,
                uiA = this._domA,
                thumbH = $$1('.picker_selector', uiH),
                thumbSL = $$1('.picker_selector', uiSL),
                thumbA = $$1('.picker_selector', uiA);

            function posX(parent, child, relX) {
                child.style.left = relX * 100 + '%';
            }
            function posY(parent, child, relY) {
                child.style.top = relY * 100 + '%';
            }

            posX(uiH, thumbH, hsl[0]);

            this._domSL.style.backgroundColor = this._domH.style.color = cssHue;

            posX(uiSL, thumbSL, hsl[1]);
            posY(uiSL, thumbSL, 1 - hsl[2]);

            uiSL.style.color = cssHSL;

            posY(uiA, thumbA, 1 - hsl[3]);

            var opaque = cssHSL,
                transp = opaque.replace('hsl', 'hsla').replace(')', ', 0)'),
                bg = 'linear-gradient(' + [opaque, transp] + ')';

            this._domA.style.background = bg + ', ' + BG_TRANSP$1;

            if (!flags.fromEditor) {
                var format = this.settings.editorFormat,
                    alpha = this.settings.alpha;

                var value = void 0;
                switch (format) {
                    case 'rgb':
                        value = col.printRGB(alpha);break;
                    case 'hsl':
                        value = col.printHSL(alpha);break;
                    default:
                        value = col.printHex(alpha);
                }
                this._domEdit.value = value;
            }

            this._domSample.style.color = cssHSLA;
        }
    }, {
        key: '_ifPopup',
        value: function _ifPopup(actionIf, actionElse) {
            if (this.settings.parent && this.settings.popup) {
                actionIf && actionIf(this.settings.popup);
            } else {
                actionElse && actionElse();
            }
        }
    }, {
        key: '_toggleDOM',
        value: function _toggleDOM(toVisible) {
            var dom = this.domElement;
            if (!dom) {
                return false;
            }

            var displayStyle = toVisible ? '' : 'none',
                toggle = dom.style.display !== displayStyle;

            if (toggle) {
                dom.style.display = displayStyle;
            }
            return toggle;
        }
    }]);
    return Picker;
}();

const style$1 = {
    display: 'flex',
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: 'transparent',
};

class Checkbox {

    constructor({selected, label, onchange}) {

        this.state = selected;
        this.onchange = onchange;
        this.elem = div$2({style: style$1});

        const svgDiv = div$2({
            style: {
                width: '14px',
                height: '14px',
                borderColor: 'gray',
                borderWidth: '1px',
                borderStyle: 'solid'
            }
        });
        this.svg = createIcon$1('check', (true === selected ? '#444' : 'transparent'));
        this.svg.style.width = '12px';
        this.svg.style.height = '12px';
        svgDiv.appendChild(this.svg);
        this.elem.appendChild(svgDiv);

        if (label) {
            const d = div$2({style: {marginLeft: '5px'}}); //{ class: 'igv-some-label-class' });
            d.textContent = label;
            this.elem.appendChild(d);
        }

        const handleClick = (e) => {
            e.preventDefault();
            e.stopPropagation();
            const newState = !this.state;
            this.selected = newState;
            if (typeof this.onchange === 'function') {
                this.onchange(newState);
            }
        };
        this.elem.addEventListener('click', handleClick);
        this.elem.addEventListener('touchend', handleClick);
    }

    set selected(selected) {
        this.state = selected;
        const p = this.svg.querySelector('path');
        p.setAttributeNS(null, 'fill', (true === selected ? '#444' : 'transparent'));
    }

    get selected() {
        return this.state;
    }

    onchange(handler) {
        this.onchange = handler;
    }


}

function paintLegend(legend, newColorScale) {

    const ctx = legend.getContext("2d");
    const w = legend.width;
    const step = (newColorScale.max - newColorScale.min) / w;
    for (let i = 0; i < w; i++) {
        const v = newColorScale.min + i * step;
        const color = newColorScale.getColor(v);
        ctx.fillStyle = color;
        ctx.fillRect(i, 0, 1, legend.height);
    }
}

/**
 *   Editor for color scales.  Supported types:
 *
 *   'gradient': {min, max, minColor, maxColor}
 *
 *   'diverging': {mid, midColor, lowGradientScale, highGradientScale}
 *
 *
 */
class ColorScaleEditor {

    static open(colorScale, parent, callback) {

        let newColorScale = colorScale.clone();

        const table = document.createElement('table');

        const legend = document.createElement('canvas');
        legend.style.height = "20px";
        legend.style.width = "100%";
        legend.style.marginTop = "10px";
        legend.style.border = "1px solid black";

        const minTextbox = new TextBoxRow({
            label: "Min value",
            value: newColorScale.min.toString(),
            onchange: (v) => {
                newColorScale.min = Number.parseFloat(v);
                paintLegend(legend, newColorScale);
            }
        });
        table.appendChild(minTextbox.row);

        const midTextbox = new TextBoxRow({
            label: "Mid value",
            value: (newColorScale.mid || newColorScale.min).toString(),
            onchange: (v) => {
                newColorScale.mid = Number.parseFloat(v);
                paintLegend(legend, newColorScale);
            }
        });
        table.appendChild(midTextbox.row);

        const maxTextbox = new TextBoxRow({
            label: "Max value",
            value: newColorScale.max.toString(),
            onchange: (v) => {
                newColorScale.max = Number.parseFloat(v);
                paintLegend(legend, newColorScale);
            }
        });
        table.appendChild(maxTextbox.row);


        const colorElem = new ColorPickerRow({
            label: "Min color",
            value: newColorScale.minColor,
            onchange: (v) => {
                newColorScale.minColor = v;
                paintLegend(legend, newColorScale);
            }
        });
        table.appendChild(colorElem.row);

        const midColorElem = new ColorPickerRow({
            label: "Mid color",
            value: newColorScale.midColor || newColorScale.minColor,
            onchange: (v) => {
                newColorScale.midColor = v;
                paintLegend(legend, newColorScale);
            }
        });
        table.appendChild(midColorElem.row);

        const highColorElem = new ColorPickerRow({
            label: "Max color",
            value: newColorScale.maxColor,
            onchange: (v) => {
                newColorScale.maxColor = v;
                paintLegend(legend, newColorScale);
            }
        });
        table.appendChild(highColorElem.row);

        const divergingCheckbox = new Checkbox({
            selected: "diverging" === colorScale.type,
            label: "Diverging Scale",
            onchange: (diverging) => {
                if (diverging) {
                    // Converting from gradient to diverting
                    newColorScale.mid = newColorScale.min < 0 && newColorScale.max > 0 ? 0 : (newColorScale.min + newColorScale.max) / 2;
                    newColorScale.midColor = "rgb(255,255,255)";
                    newColorScale = new DivergingGradientScale(newColorScale);

                    midTextbox.value = newColorScale.mid;
                    midTextbox.show();

                    midColorElem.value = newColorScale.midColor;
                    midColorElem.show();

                    paintLegend(legend, newColorScale);
                } else {

                    // Converting from diverging to gradient
                    newColorScale = new GradientColorScale(newColorScale);
                    midTextbox.hide();
                    midColorElem.hide();
                    paintLegend(legend, newColorScale);
                }
            }
        });
        divergingCheckbox.elem.style.marginBottom = "20px";

        if('diverging' !== colorScale.type) {
            midTextbox.hide();
            midColorElem.hide();
        }

        const panel = document.createElement('div');
        panel.appendChild(divergingCheckbox.elem);
        panel.appendChild(table);
        panel.appendChild(legend);

        const okHandler = () => {
            if (callback) {
                callback(newColorScale);
            }

        };

        const config = {
            parent, // label: 'Multi-select',
            content: {elem: panel}, okHandler
        };
        const dialog = new Dialog(config);
        parent.appendChild(dialog.elem);
        show$1(dialog.elem);

        paintLegend(legend, newColorScale);

    }

}

class LabeledButtonRow {
    constructor({label, value, onchange}) {

        this.row = document.createElement('tr');
        const cell = document.createElement('td');
        this.row.appendChild(cell);

        const div = document.createElement('div');
        div.innerHTML = label;
        cell.appendChild(div);
    }

    hide() {
        this.row.style.display = 'none';
    }

    show() {
        this.row.style.display = 'table-row';
    }
}

class TextBoxRow extends LabeledButtonRow {

    constructor({label, value, onchange}) {
        super({label, value, onchange});

        const cell2 = document.createElement('td');
        this.row.appendChild(cell2);
        this.input = document.createElement('input');

        value = value || "0";
        this.input.value = purify.sanitize(value);

        cell2.appendChild(this.input);

        if (onchange) {
            this.input.addEventListener('change', (e) => onchange(this.input.value));
        }
    }

    get value() {
        return this.input.value
    }

    set value(v) {
        this.input.value = v;
    }
}

class ColorPickerRow extends LabeledButtonRow {

    constructor({label, value, onchange}) {
        super({label, value, onchange});

        const cell2 = document.createElement('td');
        this.row.appendChild(cell2);
        const colorButton = document.createElement('div');
        cell2.appendChild(colorButton);
        colorButton.style.width = "20px";
        colorButton.style.height = "20px";
        colorButton.style.border = "1px solid black";
        this.colorButton = colorButton;

        value = value || "white";
        colorButton.style.background = value;

        const picker = new Picker$1(colorButton);
        picker.setOptions({
            alpha: false, color: value
        });

        picker.onDone =  (color) => {
            colorButton.style.background = color.rgbString;
            if (onchange) {
                onchange(color.rgbString);
            }
        };
    }

    set value(c) {
        this.colorButton.style.background = c;
    }
}

class WigTrack extends TrackBase {

    static defaultColor = 'rgb(150, 150, 150)'

    static defaults = {
        height: 50,
        flipAxis: false,
        logScale: false,
        windowFunction: 'mean',
        graphType: 'bar',
        normalize: undefined,
        scaleFactor: undefined,
        overflowColor: `rgb(255, 32, 255)`,
        baselineColor: 'lightGray',
        summarize: true,
        visibilityWindow: undefined
    }

    constructor(config, browser) {
        super(config, browser);
    }

    init(config) {

        super.init(config);

        this.type = "wig";
        this.featureType = 'numeric';
        this.resolutionAware = true;
        this._paintAxis = paintAxis.bind(this);

        const format = config.format ? config.format.toLowerCase() : config.format;
        if (config.featureSource) {
            this.featureSource = config.featureSource;
            delete config.featureSource;
        } else if ("bigwig" === format) {
            this.featureSource = new BWSource(config, this.browser.genome);
        } else if ("tdf" === format) {
            this.featureSource = new TDFSource(config, this.browser.genome);
        } else {
            this.featureSource = FeatureSource(config, this.browser.genome);
        }

        // Override autoscale default
        if (config.max === undefined || config.autoscale === true) {
            this.autoscale = true;
        } else {
            this.dataRange = {
                min: config.min || 0,
                max: config.max
            };
        }

        if (config.colorScale) {
            this._colorScale = ColorScaleFactory.fromJson(config.colorScale);
        }

        // Override default height for heatmaps
        if ("heatmap" === config.graphType && !config.height) {
            this.height = 20;
        }
    }

    async postInit() {
        const header = await this.getHeader();
        if (this.disposed) return   // This track was removed during async load
        if (header) this.setTrackProperties(header);

        this._initialColor = this.color || this.constructor.defaultColor;
        this._initialAltColor = this.altColor || this.constructor.defaultColor;

    }

    get supportsWholeGenome() {
        return !this.config.indexURL && this.config.supportsWholeGenome !== false
    }

    get paintAxis() {
        // Supply do-nothing implementation for heatmaps
        return "heatmap" === this.graphType ? () => {
        } : this._paintAxis
    }

    get colorScale() {
        return this._colorScale
    }

    async getFeatures(chr, start, end, bpPerPixel) {

        const windowFunction = this.windowFunction;

        const features = await this.featureSource.getFeatures({
            chr,
            start,
            end,
            bpPerPixel,
            visibilityWindow: this.visibilityWindow,
            windowFunction
        });
        if (this.normalize && this.featureSource.normalizationFactor) {
            const scaleFactor = this.featureSource.normalizationFactor;
            for (let f of features) {
                f.value *= scaleFactor;
            }
        }
        if (this.scaleFactor) {
            const scaleFactor = this.scaleFactor;
            for (let f of features) {
                f.value *= scaleFactor;
            }
        }

        // For dynseq rendering, attach sequence data to features
        if (this.graphType === "dynseq" && features && features.length > 0) {
            for (let f of features) {
                try {
                    f.sequence = await this.browser.genome.getSequence(f.chr, Math.floor(f.start), Math.floor(f.end));
                } catch (error) {
                    console.warn(`Failed to get sequence for feature at chr: ${f.chr}, start: ${f.start}, end: ${f.end}. Error:`, error);
                    f.sequence = null;
                }
            }
        }

        // Summarize features to current resolution.  This needs to be done here, rather than in the "draw" function,
        // for group autoscale to work.
        if (this.summarize && ("mean" === windowFunction || "min" === windowFunction || "max" === windowFunction)) {
            return summarizeData(features, start, bpPerPixel, windowFunction)
        } else {
            return features
        }
    }

    menuItemList() {
        const items = [];

        if ('heatmap' === this.graphType) {
            items.push('<hr>');
            items.push({
                label: 'Set color scale', click: function () {
                    ColorScaleEditor.open(this.colorScale, this.browser.columnContainer, (colorScale) => {
                        this._colorScale = colorScale;
                        this.trackView.repaintViews();
                    });
                }
            });
        } else if (this.flipAxis !== undefined) {
            items.push('<hr>');
            items.push({
                label: 'Flip y-axis',
                click: function () {
                    this.flipAxis = !this.flipAxis;
                    this.trackView.repaintViews();
                }
            });
        }

        items.push(...this.graphTypeItems());

        if (this.featureSource.windowFunctions) {
            items.push(...this.wigSummarizationItems());
        }

        items.push(...this.numericDataMenuItems());

        return items
    }

    wigSummarizationItems() {

        const windowFunctions = this.featureSource.windowFunctions;

        const menuItems = [];
        menuItems.push('<hr/>');
        menuItems.push("<div>Windowing function</div>");
        for (const wf of windowFunctions) {

            function clickHandler() {
                this.windowFunction = wf;
                this.trackView.updateViews();
            }

            menuItems.push({element:createCheckbox(wf, this.windowFunction === wf), click: clickHandler});
        }

        return menuItems
    }

    graphTypeItems() {

        const graphType = ['bar', 'line', 'points', 'heatmap', 'dynseq'];

        const menuItems = [];
        menuItems.push('<hr/>');
        menuItems.push("<div>Graph type</div>");

        for (const gt of graphType) {

            function clickHandler() {
                this.graphType = gt;
                this.trackView.repaintViews();
            }

            menuItems.push({element:createCheckbox(gt, this.graphType === gt), click: clickHandler});
        }

        return menuItems
    }

    async getHeader() {

        if (typeof this.featureSource.getHeader === "function") {
            this.header = await this.featureSource.getHeader();
        }
        return this.header
    }

    // TODO: refactor to igvUtils.js
    getScaleFactor(min, max, height, logScale) {
        const minValue = (logScale === true) ? ((min < 0) ? -Math.log10(Math.abs(min) + 1) : Math.log10(Math.abs(min) + 1)) : min;
        const maxValue = (logScale === true) ? Math.log10(Math.abs(max) + 1) : max;
        const scale = height / (maxValue - minValue);
        return scale
    }

    computeYPixelValue(yValue, yScaleFactor) {
        return (this.flipAxis ? (yValue - this.dataRange.min) : (this.dataRange.max - yValue)) * yScaleFactor
    }

    computeYPixelValueInLogScale(yValue, yScaleFactor) {
        let maxValue = this.dataRange.max;
        let minValue = this.dataRange.min;
        minValue = (minValue < 0) ? -Math.log10(Math.abs(minValue) + 1) : Math.log10(Math.abs(minValue) + 1);
        maxValue = (maxValue < 0) ? -Math.log10(Math.abs(maxValue) + 1) : Math.log10(Math.abs(maxValue) + 1);

        yValue = (yValue < 0) ? -Math.log10(Math.abs(yValue) + 1) : Math.log10(yValue + 1);
        return ((this.flipAxis ? (yValue - minValue) : (maxValue - yValue)) * yScaleFactor)
    }

    draw(options) {

        const features = options.features;
        const ctx = options.context;
        const bpPerPixel = options.bpPerPixel;
        const bpStart = options.bpStart;
        const pixelWidth = options.pixelWidth;
        const pixelHeight = options.pixelHeight - 1;
        const bpEnd = bpStart + pixelWidth * bpPerPixel + 1;

        const scaleFactor = this.getScaleFactor(this.dataRange.min, this.dataRange.max, pixelHeight, this.logScale);
        const yScale = (yValue) => this.logScale
            ? this.computeYPixelValueInLogScale(yValue, scaleFactor)
            : this.computeYPixelValue(yValue, scaleFactor);

        if (features && features.length > 0) {

            if (this.dataRange.min === undefined) this.dataRange.min = 0;

            // Max can be less than min if config.min is set but max left to autoscale.   If that's the case there is
            // nothing to paint.
            if (this.dataRange.max > this.dataRange.min) {

                let lastPixelEnd = -1;
                let lastY;
                const y0 = yScale(0);

                for (let f of features) {

                    if (f.end < bpStart) continue
                    if (f.start > bpEnd) break

                    const x = (f.start - bpStart) / bpPerPixel;
                    if (Number.isNaN(x)) continue

                    let y = yScale(f.value);

                    const rectEnd = (f.end - bpStart) / bpPerPixel;
                    const width = rectEnd - x;

                    const color = options.alpha ? IGVColor.addAlpha(this.getColorForFeature(f), options.alpha) : this.getColorForFeature(f);

                    if (this.graphType === "line") {
                        if (lastY !== undefined) {
                            IGVGraphics.strokeLine(ctx, lastPixelEnd, lastY, x, y, {
                                "fillStyle": color,
                                "strokeStyle": color
                            });
                        }
                        IGVGraphics.strokeLine(ctx, x, y, x + width, y, {"fillStyle": color, "strokeStyle": color});
                    } else if (this.graphType === "points") {
                        const pointSize = this.config.pointSize || 3;
                        const px = x + width / 2;
                        IGVGraphics.fillCircle(ctx, px, y, pointSize / 2, {"fillStyle": color, "strokeStyle": color});

                        if (f.value > this.dataRange.max) {
                            IGVGraphics.fillCircle(ctx, px, pointSize / 2, pointSize / 2, 3, {fillStyle: this.overflowColor});
                        } else if (f.value < this.dataRange.min) {
                            IGVGraphics.fillCircle(ctx, px, pixelHeight - pointSize / 2, pointSize / 2, 3, {fillStyle: this.overflowColor});
                        }


                    } else if (this.graphType === "heatmap") {
                        if (!this._colorScale) {
                            // Create a default color scale.
                            this._colorScale = this.dataRange.min < 0 && this.dataRange.max > 0 ?
                                ColorScaleFactory.defaultDivergingScale(this.dataRange.min, 0, this.dataRange.max) :
                                ColorScaleFactory.defaultGradientScale(this.dataRange.min, this.dataRange.max);
                        }
                        const color = this._colorScale.getColor(f.value);
                        IGVGraphics.fillRect(ctx, x, 0, width, pixelHeight, {fillStyle: color});
                    } else if (this.graphType === "dynseq") {
                        // Dynamic sequence rendering - render bases as glyphs with heights based on wig values
                        this.renderDynSeq(ctx, f, x, width, y, y0, pixelHeight);
                    } else {
                        // Default graph type (bar)
                        const height = Math.min(pixelHeight, y - y0);
                        IGVGraphics.fillRect(ctx, x, y0, width, height, {fillStyle: color});
                        if (f.value > this.dataRange.max) {
                            IGVGraphics.fillRect(ctx, x, 0, width, 3, {fillStyle: this.overflowColor});
                        } else if (f.value < this.dataRange.min) {
                            IGVGraphics.fillRect(ctx, x, pixelHeight - 2, width, 3, {fillStyle: this.overflowColor});
                        }

                    }
                    lastPixelEnd = x + width;
                    lastY = y;
                }

                // If the track includes negative values draw a baseline
                if (this.dataRange.min < 0) {
                    let maxValue = this.dataRange.max;
                    let minValue = this.dataRange.min;
                    minValue = (this.logScale === true) ? ((minValue < 0) ? -Math.log10(Math.abs(minValue) + 1) : Math.log10(Math.abs(minValue) + 1)) : minValue;
                    maxValue = (this.logScale === true) ? ((maxValue < 0) ? -Math.log10(Math.abs(maxValue) + 1) : Math.log10(Math.abs(maxValue) + 1)) : maxValue;
                    const ratio = maxValue / (maxValue - minValue);
                    const basepx = this.flipAxis ? (1 - ratio) * pixelHeight : ratio * pixelHeight;
                    IGVGraphics.strokeLine(ctx, 0, basepx, options.pixelWidth, basepx, {strokeStyle: this.baselineColor});
                }
            }
        }

        // Draw guidelines
        if (this.config.hasOwnProperty('guideLines')) {
            for (let line of this.config.guideLines) {
                if (line.hasOwnProperty('color') && line.hasOwnProperty('y') && line.hasOwnProperty('dotted')) {
                    let y = yScale(line.y);
                    let props = {
                        'strokeStyle': line['color'],
                        'strokeWidth': 2
                    };
                    if (line['dotted']) IGVGraphics.dashedLine(options.context, 0, y, options.pixelWidth, y, 5, props);
                    else IGVGraphics.strokeLine(options.context, 0, y, options.pixelWidth, y, props);
                }
            }
        }
    }

    renderDynSeq(ctx, feature, x, width, y, y0, pixelHeight) {
        // Use pre-cached sequence data from the feature
        const sequence = feature.sequence;
        
        if (!sequence) {
            // Fall back to regular bar rendering if no sequence data
            const height = Math.min(pixelHeight, y - y0);
            const color = this.getColorForFeature(feature);
            IGVGraphics.fillRect(ctx, x, y0, width, height, {fillStyle: color});
            return
        }
        
        // Calculate rectangle position and height based on the wig value
        const rectY = Math.min(y, y0);
        const rectHeight = Math.max(1, Math.abs(y - y0)); // Ensure minimum height of 1px
        
        // Render each base in the sequence
        const baseWidth = width / sequence.length;
        const isNegative = feature.value < 0;
        
        for (let i = 0; i < sequence.length; i++) {
            const baseX = x + (i * baseWidth);
            const base = sequence[i].toUpperCase();
            
            // Get nucleotide color from browser's color scheme
            const nucleotideColor = this.browser.nucleotideColors[base] || 'gray';
            
            // Draw the base as a letter-shaped glyph
            this.drawLetterGlyph(ctx, base, baseX, rectY, baseWidth, rectHeight, nucleotideColor, isNegative);
        }
        
        // Draw overflow indicators if needed
        if (feature.value > this.dataRange.max) {
            IGVGraphics.fillRect(ctx, x, 0, width, 3, {fillStyle: this.overflowColor});
        } else if (feature.value < this.dataRange.min) {
            IGVGraphics.fillRect(ctx, x, pixelHeight - 2, width, 3, {fillStyle: this.overflowColor});
        }
    }

    drawLetterGlyph(ctx, base, x, y, width, height, color, flipVertical = false) {
        // Define letter path data as SVG path strings (100x100 coordinate system)
        const letterPaths = {
            'A': {
                main: `M 0 100 L 33 0 L 66 0 L 100 100 L 75 100 L 66 75 L 33 75 L 25 100 L 0 100`,
                overlay: `M 41 55 L 50 25 L 58 55 L 41 55`
            },
            'C': {
                main: `M 100 28 C 100 -13 0 -13 0 50 C 0 113 100 113 100 72 L 75 72 C 75 90 30 90 30 50 C 30 10 75 10 75 28 L 100 28`
            },
            'G': {
                main: `M 100 28 C 100 -13 0 -13 0 50 C 0 113 100 113 100 72 L 100 48 L 55 48 L 55 72 L 75 72 C 75 90 30 90 30 50 C 30 10 75 5 75 28 L 100 28`
            },
            'T': {
                main: `M 0 0 L 0 20 L 35 20 L 35 100 L 65 100 L 65 20 L 100 20 L 100 0 L 0 0`
            },
            'N': {
                main: `M 0 100 L 0 0 L 20 0 L 80 75 L 80 0 L 100 0 L 100 100 L 80 100 L 20 25 L 20 100 L 0 100`
            }
        };

        const pathData = letterPaths[base] || letterPaths['N'];
        
        ctx.save();
        ctx.fillStyle = color;
        
        // Apply vertical flip transformation if needed
        if (flipVertical) {
            ctx.translate(x + width/2, y + height/2);
            ctx.scale(1, -1);
            ctx.translate(-(x + width/2), -(y + height/2));
        }
        
        // Draw main path
        this.drawSVGPath(ctx, pathData.main, x, y, width, height);
        
        // Draw overlay path (if exists) - typically a cutout or highlight
        if (pathData.overlay) {
            ctx.fillStyle = '#ffffff';
            this.drawSVGPath(ctx, pathData.overlay, x, y, width, height);
        }
        
        ctx.restore();
    }

    drawSVGPath(ctx, pathString, x, y, width, height) {
        // Parse SVG path string and draw it scaled to the given dimensions
        // Path is defined in 100x100 coordinate system
        const scaleX = width / 100;
        const scaleY = height / 100;
        
        ctx.beginPath();
        
        // Enhanced SVG path parser for M (move), L (line), and C (cubic Bézier curve) commands
        const commands = pathString.match(/[MLC][^MLC]*/g) || [];
        
        for (let command of commands) {
            const type = command[0];
            const coords = command.slice(1).trim().split(/[\s,]+/).map(Number);
            
            if (type === 'M') {
                // Move to
                ctx.moveTo(x + coords[0] * scaleX, y + coords[1] * scaleY);
            } else if (type === 'L') {
                // Line to
                ctx.lineTo(x + coords[0] * scaleX, y + coords[1] * scaleY);
            } else if (type === 'C') {
                // Cubic Bézier curve
                // C x1 y1 x2 y2 x y (control point 1, control point 2, end point)
                if (coords.length >= 6) {
                    ctx.bezierCurveTo(
                        x + coords[0] * scaleX, y + coords[1] * scaleY, // control point 1
                        x + coords[2] * scaleX, y + coords[3] * scaleY, // control point 2
                        x + coords[4] * scaleX, y + coords[5] * scaleY  // end point
                    );
                }
            }
        }
        
        ctx.closePath();
        ctx.fill();
    }

    popupData(clickState, features) {

        if (features === undefined) features = this.clickedFeatures(clickState);

        if (features && features.length > 0) {

            const genomicLocation = clickState.genomicLocation;
            const popupData = [];

            // Sort features based on distance from click
            features.sort(function (a, b) {
                const distA = Math.abs((a.start + a.end) / 2 - genomicLocation);
                const distB = Math.abs((b.start + b.end) / 2 - genomicLocation);
                return distA - distB
            });

            // Display closest 10
            const displayFeatures = features.length > 10 ? features.slice(0, 10) : features;

            // Resort in ascending order
            displayFeatures.sort(function (a, b) {
                return a.start - b.start
            });

            for (let selectedFeature of displayFeatures) {
                if (selectedFeature) {
                    if (popupData.length > 0) {
                        popupData.push('<hr/>');
                    }
                    let posString = (selectedFeature.end - selectedFeature.start) === 1 ?
                        numberFormatter$1(Math.floor(selectedFeature.start) + 1)
                        : numberFormatter$1(Math.floor(selectedFeature.start) + 1) + "-" + numberFormatter$1(Math.floor(selectedFeature.end));
                    popupData.push({name: "Position:", value: posString});
                    popupData.push({
                        name: "Value:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;",
                        value: numberFormatter$1(selectedFeature.value.toFixed(4))
                    });
                }
            }
            if (displayFeatures.length < features.length) {
                popupData.push("<hr/>...");
            }

            return popupData

        } else {
            return []
        }
    }

    /**
     * Return color for feature.
     * @param feature
     * @returns {string}
     */

    getColorForFeature(f) {
        let c = (f.value < 0 && this.altColor) ? this.altColor : this.color || WigTrack.defaultColor;
        return (typeof c === "function") ? c(f.value) : c
    }


    getState() {
        const state = super.getState();
        if (this._colorScale) {
            state.colorScale = this._colorScale.toJson();
        }
        return state
    }

    /**
     * Called when the track is removed.  Do any needed cleanup here
     */
    dispose() {
        this.trackView = undefined;
    }

}

/**
 * Summarize wig data in bins of size "bpPerPixel" with the given window function.
 *
 * @param features  wig (numeric) data -- features cannot overlap, and are in ascending order by start position
 * @param startBP  bp start position for computing binned data
 * @param bpPerPixel  bp per pixel (bin)
 * @param windowFunction mean, min, or max
 * @returns {*|*[]}
 */
function summarizeData(features, startBP, bpPerPixel, windowFunction = "mean") {

    if (bpPerPixel <= 1 || !features || features.length === 0 || windowFunction === "none") {
        return features
    }

    // Assume features are sorted by position.  Wig features cannot overlap.  Note, UCSC "reductionLevel" == bpPerPixel
    const chr = features[0].chr;
    const binSize = bpPerPixel;
    const summaryFeatures = [];

    const finishBin = (bin) => {
        const start = startBP + bin.bin * binSize;
        const end = start + binSize;
        let value;
        switch (windowFunction) {
            case "mean":
                value = bin.sumData / bin.count;
                break
            case "max":
                value = bin.max;
                break
            case "min":
                value = bin.min;
                break
            default:
                throw Error(`Unknown window function: ${windowFunction}`)
        }
        const description = `${windowFunction} of ${bin.count} values`;
        summaryFeatures.push({chr, start, end, value, description});
    };

    let currentBinData;
    for (let f of features) {

        // Loop through bins this feature overlaps, updating the weighted sum for each bin or min/max,
        // depending on window function
        let startBin = Math.floor((f.start - startBP) / binSize);
        const endBin = Math.floor((f.end - startBP) / binSize);

        if (currentBinData && startBin === currentBinData.bin) {
            currentBinData.add(f);
            startBin++;
        }

        if (!currentBinData || endBin > currentBinData.bin) {

            if (currentBinData) {
                finishBin(currentBinData);
            }

            // Feature stretches across multiple bins.
            if (endBin > startBin) {
                const end = startBP + endBin * binSize;
                summaryFeatures.push({chr, start: f.start, end, value: f.value});
            }

            currentBinData = new SummaryBinData(endBin, f);
        }

    }
    if (currentBinData) {
        finishBin(currentBinData);
    }

    // Consolidate
    const c = [];
    let lastFeature = summaryFeatures[0];
    for (let f of summaryFeatures) {
        if (lastFeature.value === f.value && f.start <= lastFeature.end) {
            lastFeature.end = f.end;
        } else {
            c.push(lastFeature);
            lastFeature = f;
        }
    }
    c.push(lastFeature);

    return c

}

class SummaryBinData {
    constructor(bin, feature) {
        this.bin = bin;
        this.sumData = feature.value;
        this.count = 1;
        this.min = feature.value;
        this.max = feature.value;
    }

    add(feature) {
        this.sumData += feature.value;
        this.max = Math.max(feature.value, this.max);
        this.min = Math.min(feature.value, this.min);
        this.count++;
    }

    get mean() {
        return this.sumData / this.count
    }
}

const DEFAULT_MAX_WG_COUNT = 10000;

/**
 * feature source for "bed like" files (tab or whitespace delimited files with 1 feature per line: bed, gff, vcf, etc)
 *
 * @param config
 * @constructor
 */
class TextFeatureSource extends BaseFeatureSource {

    constructor(config, genome) {

        super(genome);

        this.config = config || {};
        this.genome = genome;
        this.sourceType = (config.sourceType === undefined ? "file" : config.sourceType);
        this.maxWGCount = config.maxWGCount || DEFAULT_MAX_WG_COUNT;
        this.windowFunctions = ["mean", "min", "max", "none"];

        const queryableFormats = new Set(["bigwig", "bw", "bigbed", "bb", "biginteract", "biggenepred", "bignarrowpeak", "tdf"]);

        this.queryable = config.indexURL || config.queryable === true;   // False by default, unless explicitly set
        if (config.reader) {
            // Explicit reader implementation
            this.reader = config.reader;
            this.queryable = config.queryable !== false;
        } else if (config.sourceType === "ga4gh") {
            throw Error("Unsupported source type 'ga4gh'")
        } else if ((config.type === "eqtl" || config.type === "qtl") && config.sourceType === "gtex-ws") {
            this.reader = new GtexReader(config);
            this.queryable = true;
        } else if ("htsget" === config.sourceType) {
            this.reader = new HtsgetVariantReader(config, genome);
            this.queryable = true;
            this.supportsWholeGenome = () => false;   // htsget sources do not support whole genome view
        } else if (config.sourceType === 'ucscservice') {
            this.reader = new UCSCServiceReader(config.source);
            this.queryable = true;
        } else if (config.sourceType === 'custom') {
            this.reader = new CustomServiceReader(config.source);
            this.queryable = false !== config.source.queryable;
        } else if ('service' === config.sourceType) {
            this.reader = new FeatureFileReader(config, genome);
            this.queryable = true;
        } else {
            // File of some type (i.e. not a webservice)
            this.reader = new FeatureFileReader(config, genome);
            if (config.queryable !== undefined) {
                this.queryable = config.queryable;
            } else if (queryableFormats.has(config.format) || this.reader.indexed) {
                this.queryable = true;
            } else ;
        }

        // Flag indicating if features loaded by this source can be searched for by name or attribute, true by default
        this.searchable = config.searchable !== false;

    }

    async defaultVisibilityWindow() {
        if (this.reader && typeof this.reader.defaultVisibilityWindow === 'function') {
            return this.reader.defaultVisibilityWindow()
        }
    }

    async trackType() {
        const header = await this.getHeader();
        if (header) {
            return header.type
        } else {
            return undefined    // Convention for unknown or unspecified
        }
    }

    async getHeader() {
        if (!this.header) {

            if (this.reader && typeof this.reader.readHeader === "function") {
                const header = await this.reader.readHeader();
                if (header) {
                    this.header = header;
                    if (header.format) {
                        this.config.format = header.format;
                    }
                } else {
                    this.header = {};
                }
            } else {
                this.header = {};
            }
        }
        return this.header
    }

    /**
     * Required function for all data source objects.  Fetches features for the
     * range requested.
     *
     * This function is quite complex due to the variety of reader types backing it, some indexed, some queryable,
     * some not.
     *
     * @param chr
     * @param start
     * @param end
     * @param bpPerPixel
     */
    async getFeatures({chr, start, end, bpPerPixel, visibilityWindow, windowFunction}) {

        const isWholeGenome = ("all" === chr.toLowerCase());

        start = start || 0;
        end = end || Number.MAX_SAFE_INTEGER;

        // Various conditions that can require a feature load
        //   * view is "whole genome" but no features are loaded
        //   * cache is disabled
        //   * cache does not contain requested range
        // const containsRange = this.featureCache.containsRange(new GenomicInterval(queryChr, start, end))
        if ((isWholeGenome && !this.wgFeatures && this.supportsWholeGenome()) ||
            this.config.disableCache ||
            !this.featureCache ||
            !this.featureCache.containsRange(new GenomicInterval(chr, start, end))) {
            await this.loadFeatures(chr, start, end, visibilityWindow);
        }

        if (isWholeGenome) {
            if (!this.wgFeatures) {
                if (this.supportsWholeGenome()) {
                    if("wig" === this.config.type) {
                        const allWgFeatures = await computeWGFeatures(this.featureCache.getAllFeatures(), this.genome, this.chromAliasManager, 1000000);
                        this.wgFeatures = summarizeData(allWgFeatures, 0, bpPerPixel, windowFunction);
                    } else {
                        this.wgFeatures = await computeWGFeatures(this.featureCache.getAllFeatures(), this.genome, this.chromAliasManager, this.maxWGCount);
                    }
                } else {
                    this.wgFeatures = [];
                }
            }
            return this.wgFeatures
        } else {
            const queryChr = this.chromAliasManager ?  await this.chromAliasManager.getAliasName(chr) : chr;
            return this.featureCache.queryFeatures(queryChr, start, end)
        }
    }

    async findFeatures(fn) {
        return this.featureCache ? this.featureCache.findFeatures(fn) : []
    }

    supportsWholeGenome() {
        return !this.queryable   // queryable (indexed, web services) sources don't support whole genome view
    }

    // TODO -- experimental, will only work for non-indexed sources
    getAllFeatures() {
        if (this.queryable || !this.featureCache) {   // queryable sources don't support all features
            return []
        } else {
            return this.featureCache.getAllFeatures()
        }
    }


    async loadFeatures(chr, start, end, visibilityWindow) {

        await this.getHeader();

        const reader = this.reader;
        let intervalStart = start;
        let intervalEnd = end;

        // chr aliasing
        let queryChr = chr;
        if (!this.chromAliasManager && this.reader && this.reader.sequenceNames && this.reader.sequenceNames.size > 0) {
            this.chromAliasManager = new ChromAliasManager(this.reader.sequenceNames, this.genome);
        }
        if (this.chromAliasManager) {
            queryChr = await this.chromAliasManager.getAliasName(chr);
        }

        // Use visibility window to potentially expand query interval.
        // This can save re-queries as we zoom out.  Visibility window <= 0 is a special case
        // indicating whole chromosome should be read at once.
        if ((!visibilityWindow || visibilityWindow <= 0) && this.config.expandQuery !== false) {
            // Whole chromosome
            const chromosome = this.genome ? this.genome.getChromosome(chr) : undefined;
            intervalStart = 0;
            intervalEnd = Math.max(chromosome ? chromosome.bpLength : Number.MAX_SAFE_INTEGER, end);
        } else if (visibilityWindow > (end - start) && this.config.expandQuery !== false) {
            let expansionWindow = Math.min(4.1 * (end - start), visibilityWindow);
            if(this.config.minQuerySize && expansionWindow < this.config.minQuerySize) {
                expansionWindow = this.config.minQuerySize;
            }
            intervalStart = Math.max(0, (start + end - expansionWindow) / 2);
            intervalEnd = intervalStart + expansionWindow;
        }

        let features = await reader.readFeatures(queryChr, intervalStart, intervalEnd);
        if (this.queryable === undefined) {
            this.queryable = reader.indexed;
        }

        const genomicInterval = this.queryable ?
            new GenomicInterval(queryChr, intervalStart, intervalEnd) :
            undefined;

        if (features) {

            // Assign overlapping features to rows
            if (this.config.format !== "wig" && this.config.type !== "junctions") {
                const maxRows = this.config.maxRows || Number.MAX_SAFE_INTEGER;
                packFeatures(features, maxRows);
            }

            // Note - replacing previous cache with new one.  genomicInterval is optional (might be undefined => includes all features)
            this.featureCache = new FeatureCache$1(features, genomicInterval);

            // If track is marked "searchable"< cache features by name -- use this with caution, memory intensive
            if (this.searchable) {
                this.addFeaturesToDB(features, this.config);
            }
        } else {
            this.featureCache = new FeatureCache$1([], genomicInterval);     // Empty cache
        }
    }

    addFeaturesToDB(featureList, config) {
        if (!this.featureMap) {
            this.featureMap = new Map();
        }
        const searchableFields = config.searchableFields || ["name", "transcript_id", "gene_id", "gene_name", "id"];
        for (let feature of featureList) {
            for (let field of searchableFields) {
                let key;
                if(feature.hasOwnProperty(field)) {
                    key = feature[field];
                }
                else if (typeof feature.getAttributeValue === 'function') {
                    key = feature.getAttributeValue(field);
                }
                if (key) {
                    key = key.replaceAll(' ', '+').toUpperCase();
                    // If feature is already present keep largest one
                    if (this.featureMap.has(key)) {
                        const f2 = this.featureMap.get(key);
                        if (feature.end - feature.start < f2.end - f2.start) {
                            continue
                        }
                    }
                    this.featureMap.set(key, feature);
                }
            }
        }
    }

    search(term) {
        if (this.featureMap) {
            return this.featureMap.get(term.toUpperCase())
        }

    }
}

/**
 * feature source for features supplied directly, as opposed to reading and parsing from a file or webservice
 *
 * @param config
 * @constructor
 */
class StaticFeatureSource extends BaseFeatureSource {

    constructor(config, genome) {

        super(genome);
        this.config = config;
        this.genome = genome;
        this.queryable = false;
        this.searchable = config.searchable !== false;  // searchable by default
        this.updateFeatures(config.features);
    }

    updateFeatures(features) {
        features = fixFeatures$1(features, this.genome);
        packFeatures(features);
        if (this.config.mappings) {
            mapProperties(features, this.config.mappings);
        }

        this.chromAliasManager = this.genome ? new ChromAliasManager(features.map(f => f.chr), this.genome) : null;

        this.featureCache = new FeatureCache$1(features);

        if (this.searchable || this.config.searchableFields) {
            this.addFeaturesToDB(features, this.config);
        }
    }

    /**
     * Required function for all data source objects.  Fetches features for the
     * range requested.
     *
     * This function is complex due to the variety of reader types backing it, some indexed, some queryable,
     * some not.
     *
     * @param chr
     * @param start
     * @param end
     * @param bpPerPixel
     */
    async getFeatures({chr, start, end, bpPerPixel, visibilityWindow}) {

        const queryChr = this.chromAliasManager ? await this.chromAliasManager.getAliasName(chr) : chr;
        const isWholeGenome = ("all" === queryChr.toLowerCase());

        // Various conditions that can require a feature load
        //   * view is "whole genome" but no features are loaded
        //   * cache is disabled
        //   * cache does not contain requested range
        if (isWholeGenome) {
            return await computeWGFeatures(this.featureCache.getAllFeatures(), this.genome, this.chromAliasManager, this.maxWGCount)
        } else {
            return this.featureCache.queryFeatures(queryChr, start, end)
        }
    }

    //
    // supportsWholeGenome() {
    //    return true
    // }

    getAllFeatures() {
        return this.featureCache.getAllFeatures()
    }

    supportsWholeGenome() {
        return true
    }

    addFeaturesToDB(featureList, config) {
        if (!this.featureMap) {
            this.featureMap = new Map();
        }
        const searchableFields = config.searchableFields || ["name"];
        for (let feature of featureList) {
            for (let field of searchableFields) {
                let key;

                if (typeof feature.getAttributeValue === 'function') {
                    key = feature.getAttributeValue(field);
                }
                if (!key) {
                    key = feature[field];
                }
                if (key) {
                    key = key.replaceAll(' ', '+');
                    const current = this.featureMap.get(key.toUpperCase());
                    if (current && ((current.end - current.start) > (feature.end - feature.start))) continue
                    this.featureMap.set(key.toUpperCase(), feature);
                }
            }
        }
    }

    search(term) {
        if (this.featureMap) {
            return this.featureMap.get(term.toUpperCase())
        }
    }
}


/**
 * This function is used to apply properties normally added during parsing to  features supplied directly in the
 * config as an array of objects.   At the moment the only application is bedpe type features.
 * @param features
 */
function fixFeatures$1(features, genome) {

    if (genome) {
        for (let feature of features) {
            feature.chr = genome.getChromosomeName(feature.chr);
        }
    }

    return features
}


function mapProperties(features, mappings) {
    let mappingKeys = Object.keys(mappings);
    features.forEach(function (f) {
        mappingKeys.forEach(function (key) {
            f[key] = f[mappings[key]];
        });
    });
}

/**
 * Represents a Genbank file, which combines both annotations (features) and sequence.   The format combines both
 * sequence and annotations.
 *
 * Implements the Genome interface
 */
class Genbank {

    constructor({chr, locus, accession, aliases, features, sequence}) {
        this.chr = chr;
        this.locus = locus;
        this.accession = accession;
        this.aliases = aliases;
        this.features = features;
        this.sequence = sequence;
        this.bpLength = sequence.length;
    }


    toJSON() {
        return {
            gbkURL: this.url
        }
    }


    // Genome interface follows

    getSequenceRecord(chr) {
        //chr, 0, sequenceRecord.bpLength
        return {chr: this.chr, bpLength: this.bpLength}
    }

    get chromosomeNames() {
        return [this.chr]
    }

    getFirstChromosomeName() {
        return this.chr
    }

    get id() {
        return this.accession
    }
    get name() {
        return this.locus
    }

    get initialLocus() {
        return this.chr
    }

    // Genome interface follows
    get description() {
        return this.locus
    }

    get infoURL() {
        return this.url
    }

    showWholeGenomeView() {
        return false
    }

    getHomeChromosomeName() {
        return this.chr
    }

    getChromosomeName(chr) {
        return chr
    }

    getChromosomeDisplayName(str) {
        return this.chr
    }

    getChromosome(chr) {
        if (chr === this.chr) {
            return {
                name: this.chr,
                bpLength: this.bpLength
            }
        }
    }

    async loadChromosome(chr) {
        return this.getChromosome(chr)
    }

    async getAliasRecord(chr) {
        return undefined
    }

    getCytobands(chr) {
        return []
    }

    getChromosomes() {
        return [this.getChromosome(this.chr)]
    }

    get wgChromosomeNames() {
        return undefined
    }

    /**
     * Return the genome coordinate in kb for the give chromosome and position.
     * NOTE: This might return undefined if the chr is filtered from whole genome view.
     */
    getGenomeCoordinate(chr, bp) {
        if (chr === this.chr)
            return bp
    }

    /**
     * Return the chromosome and coordinate in bp for the given genome coordinate
     */
    getChromosomeCoordinate(genomeCoordinate) {
        return {chr: this.chr, position: genomeCoordinate}
    }


    /**
     * Return the offset in genome coordinates (kb) of the start of the given chromosome
     * NOTE:  This might return undefined if the chromosome is filtered from whole genome view.
     */
    getCumulativeOffset(chr) {
        return 0
    }

    /**
     * Return the nominal genome length, this is the length of the main chromosomes (no scaffolds, etc).
     */
    getGenomeLength() {
        return this.bpLength
    }


    async getSequence(chr, start, end) {
        if (chr === this.chr) {
            return this.sequence.substring(start, end)
        } else {
            return undefined
        }
    }

    /**
     * Return the first cached interval containing the specified region, or undefined if no interval is found.
     *
     * @param chr
     * @param start
     * @param end
     * @returns a SequenceInterval or undefined
     */
    getSequenceInterval(chr, start, end) {
        if (chr === this.chr) {
            return new SequenceInterval(this.chr, 0, this.sequence.length, this.sequence)
        } else {
            return undefined
        }
    }
}

const wsRegex = /\s+/;

const genbankCache = new Map();

async function loadGenbank(url) {
    let genbank = genbankCache.get(url);

    if (!genbank) {
        const data = await igvxhr$1.loadString(url, {});
        genbank = parseGenbank(data);
        genbank.url = url;
        genbankCache.set(url, genbank);
    }

    return genbank;
}


function parseGenbank(data) {


    if (!data) return null

    const dataWrapper = getDataWrapper$1(data);

    // Read locus
    let line = dataWrapper.nextLine();
    const tokens = line.split(/\s+/);
    if (tokens[0].toUpperCase() !== "LOCUS") {
        throw Error("Expected `LOCUS` line.  Found: " + line)
    }
    const locus = tokens[1].trim();

    // Loop until FEATURES section
    let accession, aliases;
    do {
        line = dataWrapper.nextLine();
        if (line.startsWith("ACCESSION")) {
            const tokens = line.split(wsRegex);
            if (tokens.length < 2) {
                throw Error("Genbank file missing ACCESSION number.")
            } else {
                accession = tokens[1].trim();
            }
        } else if (line.startsWith("ALIASES")) {
            // NOTE - this is an IGV extension
            const tokens = line.split(wsRegex);
            if (tokens.length > 1) {
                aliases = tokens[1].split(",");
            }

        }
    }
    while (line && !line.startsWith("FEATURES"))

    const chr = accession || locus;
    const features = parseFeatures(chr, dataWrapper);
    const sequence = parseSequence(dataWrapper);

    return new Genbank({chr, locus, accession, aliases, features, sequence})
}


/**
 * Read the origin section.   Example...
 * <p/>
 * ORIGIN
 * 1 gatcctccat atacaacggt atctccacct caggtttaga tctcaacaac ggaaccattg
 * 61 ccgacatgag acagttaggt atcgtcgaga gttacaagct aaaacgagca gtagtcagct
 * 121 ctgcatctga agccgctgaa gttctactaa gggtggataa catcatccgt gcaagaccaa
 *
 * @param reader
 */
function parseSequence(dataWrapper) {

    let nextLine;
    let sequence = "";

    while ((nextLine = dataWrapper.nextLine()) && !nextLine.startsWith("//")) {
        nextLine = nextLine.trim();
        const tokens = nextLine.split(/\s+/);
        for (let i = 1; i < tokens.length; i++) {
            sequence += tokens[i];
        }
    }
    return sequence
}

/**
 * FEATURES             Location/Qualifiers
 * source          1..105338
 * /organism="Homo sapiens"
 * /mol_type="genomic DNA"
 * /db_xref="taxon:9606"
 * /chromosome="10"
 * gene            1..105338
 * /gene="PTEN"
 * /note="Derived by automated computational analysis using
 * gene prediction method: BestRefseq."
 * /db_xref="GeneID:5728"
 * /db_xref="HGNC:9588"
 * /db_xref="HPRD:03431"
 * /db_xref="MIM:601728"
 * <p/>
 * CDS             join(1033..1111,30588..30672,62076..62120,67609..67652,
 * 69576..69814,88681..88822,94416..94582,97457..97681,
 * 101850..102035)
 * /gene="PTEN"
 *
 * @param reader
 * @throws IOException
 */
function parseFeatures(chr, dataWrapper) {

    //Process features until "ORIGIN" or end of file
    const features = [];
    let currentLocQualifier;
    let nextLine;
    let errorCount = 0;
    let f;

    do {
        nextLine = dataWrapper.nextLine();

        if (nextLine === "") {
            continue  // Not sure this is legal in a gbk file
        }

        if (!nextLine || nextLine.startsWith("ORIGIN")) {
            break
        }

        if (nextLine.length < 6) {
            if (errorCount < 10) {
                console("Unexpected line in genbank file (skipping): " + nextLine);
            }
            errorCount++;
            continue
        }

        if (nextLine.charAt(5) !== ' ') {

            let featureType = nextLine.substring(5, 21).trim();
            f = {
                chr: chr,
                type: featureType,
                attributes: {},
                getAttributeValue: function(key) {return this.attributes[key]}
            };
            currentLocQualifier = nextLine.substring(21);

            if (featureType.toLowerCase() !== "source") {
                features.push(f);
            }

        } else {
            let tmp = nextLine.substring(21).trim();
            if (tmp.length > 0)

                if (tmp.charCodeAt(0) === 47) {   // 47 == '/'
                    if (currentLocQualifier.charCodeAt(0) === 47) {
                        let tokens = currentLocQualifier.split("=", 2);
                        if (tokens.length > 1) {
                            let keyName = tokens[0].length > 1 ? tokens[0].substring(1) : "";
                            let value = stripQuotes(tokens[1]);
                            f.attributes[keyName] = value;

                        }
                    } else {
                        // Assumed to be a continuation of the location string.  There are many forms of this string,
                        // igv only supports "join()"

                        // Crude test for strand
                        const strand = currentLocQualifier.includes("complement") ? "-" : "+";
                        f.strand = strand;

                        let joinString = currentLocQualifier.replace("join", "")
                            .replace("order", "")
                            .replace("complement", "")
                            .replace("(", "")
                            .replace(")", "");

                        if (joinString.includes("..")) {
                            joinString = joinString.replace("<", "")
                                .replace(">", "");

                            const exons = createExons(joinString, chr, strand);
                            const firstExon = exons[0];
                            f.start = firstExon.start;
                            const lastExon = exons[exons.length - 1];
                            f.end = lastExon.end;
                            if (exons.length > 1) {
                                f.exons = exons;
                            }
                        } else {
                            // TODO Single locus for now, other forms possible
                            //  const start = parseInt(joinString) - 1;const end = start + 1;
                            f.start = parseInt(joinString) - 1;
                            f.end = f.start + 1;
                        }
                    }
                    currentLocQualifier = tmp;
                } else {
                    currentLocQualifier = currentLocQualifier + tmp;
                }
        }
    }
    while (true)

    return features
}

/**
 * Create a list of Exon objects from the Embl join string.  Apparently exons in embl
 * format are represented by a single CDS record.
 *
 * @param joinString
 */
function createExons(joinString, chr, strand) {

    const lociArray = joinString.split(",");
    const exons = [];
    joinString.includes("complement");

    for (const loci of lociArray) {
        const tmp = loci.split("..");
        let exonStart = 0; // - (isNegative ? 0 : 1);

        try {
            exonStart = parseInt(tmp[0]) - 1;
        } catch (e) {
            console.error(e); // Handle error appropriately
        }

        let exonEnd = exonStart + 1;
        if (tmp.length > 1) {
            exonEnd = parseInt(tmp[1]);
        }

        exons.push({
            chr: chr,
            start: exonStart,
            end: exonEnd,
            strand: strand
        });
    }
    exons.sort(function (a, b) {
        return a.start - b.start
    });

    return exons

}

function stripQuotes(value) {
    if (value.startsWith('"') && value.endsWith('"')) {
        value = value.substring(1, value.length - 2);
    }
    return value
}

class GenbankFeatureSource extends BaseFeatureSource {

    constructor(config, genome) {
        super(genome);
        this.config = config;
        this.searchable = true;
    }

    // Feature source interface
    async getFeatures({chr, start, end, bpPerPixel, visibilityWindow}) {
        if(!this.featureSource) {
            const gbk = await loadGenbank(this.config.url);
            this.featureSource = new StaticFeatureSource({
                genome: this.config.genome,
                features: gbk.features,
                searchableFields: ['gene', 'db_xref', 'locus_tag', 'transcript_id']
            });

        }
        return this.featureSource.getFeatures({chr, start, end})
    }
    supportsWholeGenome() {
        return false
    }

    search(term) {
        return this.featureSource.search(term)
    }
}

const bbFormats = new Set(['bigwig', 'bw', 'bigbed', 'bb', 'biginteract', 'biggenepred', 'bignarrowpeak']);

function FeatureSource(config, genome) {

    const format = config.format ? config.format.toLowerCase() : undefined;

    if (config.features) {
        return new StaticFeatureSource(config, genome)
    } else if (bbFormats.has(format)) {
        return new BWSource(config, genome)
    } else if ("tdf" === format) {
        return new TDFSource(config, genome)
    } else if ("gbk" === format) {
        return new GenbankFeatureSource(config, genome)
    } else {
        return new TextFeatureSource(config, genome)
    }
}

function getExonPhase(exon) {
    return (3 - exon.readingFrame) % 3
}

function getEonStart(exon) {
    return exon.cdStart || exon.start
}

function getExonEnd(exon) {
    return exon.cdEnd || exon.end
}

const aminoAcidSequenceRenderThreshold = 0.25;

/**
 * @param feature
 * @param bpStart  genomic location of the left edge of the current canvas
 * @param xScale  scale in base-pairs per pixel
 * @returns {{px: number, px1: number, pw: number, h: number, py: number}}
 */
function calculateFeatureCoordinates(feature, bpStart, xScale) {
    let px = (feature.start - bpStart) / xScale;
    let px1 = (feature.end - bpStart) / xScale;
    //px = Math.round((feature.start - bpStart) / xScale),
    //px1 = Math.round((feature.end - bpStart) / xScale),
    let pw = px1 - px;

    if (pw < 3) {
        pw = 3;
        px -= 1.5;
    }

    return {
        px: px,
        px1: px1,
        pw: pw
    }
}

/**
 *
 * @param feature
 * @param bpStart  genomic location of the left edge of the current canvas
 * @param xScale  scale in base-pairs per pixel
 * @param pixelHeight  pixel height of the current canvas
 * @param ctx  the canvas 2d context
 * @param options  genomic state
 */
function renderFeature(feature, bpStart, xScale, pixelHeight, ctx, options) {

    try {
        ctx.save();

        // Set ctx color to a known valid color.  If getColorForFeature returns an invalid color string it is ignored, and
        // this default will be used.
        ctx.fillStyle = this.color;
        ctx.strokeStyle = this.color;

        const color = this.getColorForFeature(feature);
        // const color = '+' === feature.strand ? 'rgba(135,206,235,0.5)' : 'rgba(255,20,147,0.5)'

        ctx.fillStyle = color;
        ctx.strokeStyle = color;

        let h;
        let py;
        if (this.displayMode === "SQUISHED" && feature.row !== undefined) {
            h = this.featureHeight / 2;
            py = this.margin + this.squishedRowHeight * feature.row;
        } else if (this.displayMode === "EXPANDED" && feature.row !== undefined) {
            h = this.featureHeight;
            py = this.margin + this.expandedRowHeight * feature.row;
        } else {  // collapsed
            h = this.featureHeight;
            py = this.margin;
        }

        const pixelWidth = options.pixelWidth;   // typical 3*viewportWidth

        const cy = py + h / 2;
        const h2 = h / 2;
        const py2 = cy - h2 / 2;

        const exonCount = feature.exons ? feature.exons.length : 0;
        const coord = calculateFeatureCoordinates(feature, bpStart, xScale);
        const step = this.arrowSpacing;
        const direction = feature.strand === '+' ? 1 : feature.strand === '-' ? -1 : 0;

        if (exonCount === 0) {
            // single-exon transcript
            const xLeft = Math.max(0, coord.px);
            const xRight = Math.min(pixelWidth, coord.px1);
            const width = xRight - xLeft;

            ctx.fillRect(xLeft, py, width, h);

            if (direction !== 0) {
                ctx.fillStyle = "white";
                ctx.strokeStyle = "white";
                for (let x = xLeft + step / 2; x < xRight; x += step) {
                    // draw arrowheads along central line indicating transcribed orientation
                    IGVGraphics.strokeLine(ctx, x - direction * 2, cy - 2, x, cy);
                    IGVGraphics.strokeLine(ctx, x - direction * 2, cy + 2, x, cy);
                }
                ctx.fillStyle = color;
                ctx.strokeStyle = color;
            }
        } else {

            // multi-exon transcript
            IGVGraphics.strokeLine(ctx, coord.px + 1, cy, coord.px1 - 1, cy); // center line for introns
            const xLeft = Math.max(0, coord.px) + step / 2;
            const xRight = Math.min(pixelWidth, coord.px1);
            for (let x = xLeft; x < xRight; x += step) {
                // draw arrowheads along central line indicating transcribed orientation
                IGVGraphics.strokeLine(ctx, x - direction * 2, cy - 2, x, cy);
                IGVGraphics.strokeLine(ctx, x - direction * 2, cy + 2, x, cy);
            }

            for (let i = 0; i < feature.exons.length; i++) {

                const exon = feature.exons[i];

                // draw the exons
                let ePx = Math.round((exon.start - bpStart) / xScale);
                let ePx1 = Math.round((exon.end - bpStart) / xScale);
                let ePw = Math.max(1, ePx1 - ePx);
                let ePxU;

                if (ePx + ePw < 0) {
                    continue  // Off the left edge
                }
                if (ePx > pixelWidth) {
                    break // Off the right edge
                }

                if (exon.utr) {
                    ctx.fillRect(ePx, py2, ePw, h2); // Entire exon is UTR
                } else {
                    if (exon.cdStart) {
                        ePxU = Math.round((exon.cdStart - bpStart) / xScale);
                        ctx.fillRect(ePx, py2, ePxU - ePx, h2); // start is UTR
                        ePw -= (ePxU - ePx);
                        ePx = ePxU;
                    }
                    if (exon.cdEnd) {
                        ePxU = Math.round((exon.cdEnd - bpStart) / xScale);
                        ctx.fillRect(ePxU, py2, ePx1 - ePxU, h2); // start is UTR
                        ePw -= (ePx1 - ePxU);
                        ePx1 = ePxU;
                    }

                    ePw = Math.max(ePw, 1);

                    ctx.fillRect(ePx, py, ePw, h);

                    if (exon.readingFrame !== undefined) {

                        if (options.bpPerPixel < aminoAcidSequenceRenderThreshold &&
                            options.sequenceInterval) {

                            const leftExon = i > 0 && feature.exons[i - 1].readingFrame !== undefined ? feature.exons[i - 1] : undefined;
                            const riteExon = i < feature.exons.length - 1 && feature.exons[i + 1].readingFrame !== undefined ? feature.exons[i + 1] : undefined;

                            renderAminoAcidSequence.call(this, ctx, feature.strand, leftExon, exon, riteExon, bpStart, options.bpPerPixel, py, h, options.sequenceInterval);
                        }
                    }

                    // Arrows
                    if (ePw > step + 5 && direction !== 0 && options.bpPerPixel > aminoAcidSequenceRenderThreshold) {
                        ctx.fillStyle = "white";
                        ctx.strokeStyle = "white";
                        for (let x = ePx + step / 2; x < ePx1; x += step) {
                            // draw arrowheads along central line indicating transcribed orientation
                            IGVGraphics.strokeLine(ctx, x - direction * 2, cy - 2, x, cy);
                            IGVGraphics.strokeLine(ctx, x - direction * 2, cy + 2, x, cy);
                        }
                        ctx.fillStyle = color;
                        ctx.strokeStyle = color;

                    }
                }
            }
        }

        if (options.drawLabel && this.displayMode !== "SQUISHED") {
            renderFeatureLabel.call(this, ctx, feature, coord.px, coord.px1, py, options.referenceFrame, options);
        }
    } finally {
        ctx.restore();
    }
}

function renderAminoAcidSequence(ctx, strand, leftExon, exon, riteExon, bpStart, bpPerPixel, y, height, sequenceInterval) {

    const aaColors =
        [
            'rgb(124,124,204)',
            'rgb(12, 12, 120)'
        ];


    ctx.save();

    const renderAminoAcidLetter = (strand, width, xs, y, aminoAcidLetter) => {

        if ('STOP' === aminoAcidLetter) {
            aminoAcidLetter = '*';
        }

        const aminoAcidLetterWidth = ctx.measureText(aminoAcidLetter).width;
        IGVGraphics.fillText(ctx, aminoAcidLetter, xs + (width - aminoAcidLetterWidth) / 2, y - 4, {fillStyle: '#ffffff'});
    };

    const doPaint = (strand, start, end, aminoAcidLetter, colorToggle, index) => {

        const xs = Math.round((start - bpStart) / bpPerPixel);
        const xe = Math.round((end - bpStart) / bpPerPixel);

        const width = xe - xs;

        let aaLetter;
        if (undefined === aminoAcidLetter) {

            if (sequenceInterval.hasSequence(start, end)) {

                const sequence = sequenceInterval.getSequence(start, end);
                if (sequence && 3 === sequence.length) {
                    const key = '+' === strand ? sequence : complementSequence(sequence.split('').reverse().join(''));
                    aaLetter = translationDict[key];
                }
            }

        } else {
            aaLetter = aminoAcidLetter;
        }

        if ('M' === aminoAcidLetter) {
            ctx.fillStyle = '#83f902';
        } else if ('M' === aaLetter && 0 === index) {
            ctx.fillStyle = '#83f902';
        } else if ('STOP' === aaLetter) {
            ctx.fillStyle = '#ff2101';
        } else {
            ctx.fillStyle = aaColors[colorToggle];
        }

        ctx.fillRect(xs, y, width, height);

        if (aaLetter) {
            ctx.save();
            renderAminoAcidLetter(strand, width, xs, y + height, aaLetter);
            ctx.restore();
        }

        const widthBP = end - start;
        return widthBP > 0 && widthBP < 3 ? {start, end} : undefined
    };

    const phase = getExonPhase(exon);
    let ss = getEonStart(exon);
    let ee = getExonEnd(exon);

    let bpTripletStart;
    let bpTripletEnd;

    let remainder;
    let aminoAcidBackdropColorCounter = 1;
    let colorToggle;
    let index;
    if ('+' === strand) {

        if (phase > 0) {
            ss += phase;
        }

        aminoAcidBackdropColorCounter = 1;
        for (index = 0, bpTripletStart = ss; bpTripletStart < ee; index++, bpTripletStart += 3) {
            colorToggle = aminoAcidBackdropColorCounter % 2;
            bpTripletEnd = Math.min(ee, bpTripletStart + 3);
            remainder = doPaint(strand, bpTripletStart, bpTripletEnd, undefined, aminoAcidBackdropColorCounter % 2, index);
            ++aminoAcidBackdropColorCounter;
        }

        if (phase > 0 || remainder) {

            const result = phase > 0
                ? getAminoAcidLetterWithExonGap.call(this, strand, phase, ss - phase, ss, remainder, leftExon, exon, riteExon, sequenceInterval)
                : getAminoAcidLetterWithExonGap.call(this, strand, undefined, undefined, undefined, remainder, leftExon, exon, riteExon, sequenceInterval);

            if (result) {
                const {left, rite} = result;

                if (left) {
                    doPaint(strand, ss - phase, ss, left.aminoAcidLetter, 0, undefined);
                }

                if (rite) {
                    doPaint(strand, remainder.start, remainder.end, rite.aminoAcidLetter, colorToggle, undefined);
                }

            }

        }

    } else {

        if (phase > 0) {
            ee -= phase;
        }

        aminoAcidBackdropColorCounter = 1;
        index = 0;
        for (index = 0, bpTripletEnd = ee; bpTripletEnd > ss; index++, bpTripletEnd -= 3) {
            colorToggle = aminoAcidBackdropColorCounter % 2;
            bpTripletStart = Math.max(ss, bpTripletEnd - 3);
            remainder = doPaint(strand, bpTripletStart, bpTripletEnd, undefined, aminoAcidBackdropColorCounter % 2, index);
            ++aminoAcidBackdropColorCounter;
        }

        if (phase > 0 || remainder) {

            const result = phase > 0
                ? getAminoAcidLetterWithExonGap.call(this, strand, phase, ee, ee + phase, remainder, leftExon, exon, riteExon, sequenceInterval)
                : getAminoAcidLetterWithExonGap.call(this, strand, undefined, undefined, undefined, remainder, leftExon, exon, riteExon, sequenceInterval);

            if (result) {
                const {left, rite} = result;

                if (rite) {
                    doPaint(strand, ee, ee + phase, rite.aminoAcidLetter, 0, undefined);
                }

                if (left) {
                    doPaint(strand, remainder.start, remainder.end, left.aminoAcidLetter, colorToggle, undefined);
                }

            }

        }

    }

    ctx.restore();
}

/**
 * @param ctx       the canvas 2d context
 * @param feature
 * @param featureX  feature start in pixel coordinates
 * @param featureX1 feature end in pixel coordinates
 * @param featureY  feature y-coordinate
 * @param referenceFrame  genomic state
 * @param options  options
 */
function renderFeatureLabel(ctx, feature, featureX, featureX1, featureY, referenceFrame, options) {

    try {
        ctx.save();

        const labelField = this.config.labelField ? this.config.labelField : 'name';
        let name = feature[labelField];
        if (name === undefined && feature.gene) name = feature.gene.name;
        if (name === undefined) name = feature.id || feature.ID;
        if (!name || name === '.') return

        let pixelXOffset = options.pixelXOffset || 0;
        const t1 = Math.max(featureX, -pixelXOffset);
        const t2 = Math.min(featureX1, -pixelXOffset + options.viewportWidth);
        let centerX = (t1 + t2) / 2;
        //let centerX = (featureX + featureX1) / 2

        let transform;
        if (this.displayMode === "COLLAPSED" && this.labelDisplayMode === "SLANT") {
            transform = {rotate: {angle: 45}};
        }
        const labelY = getFeatureLabelY(featureY, transform);

        let color = this.getColorForFeature(feature);
        let selected = this.browser.qtlSelections.hasPhenotype(feature.name);

        const geneFontStyle = {
            textAlign: "SLANT" === this.labelDisplayMode ? undefined : 'center',
            fillStyle: color,
            strokeStyle: color
        };

        const textMetrics = ctx.measureText(name);
        const xleft = centerX - textMetrics.width / 2;
        const xright = centerX + textMetrics.width / 2;
        const lastLabelX = options.rowLastLabelX[feature.row] || -Number.MAX_SAFE_INTEGER;
        if (options.labelAllFeatures || xleft > lastLabelX || selected) {
            options.rowLastLabelX[feature.row] = xright;

            ctx.clearRect(
                centerX - textMetrics.width / 2 - 1,
                labelY - textMetrics.actualBoundingBoxAscent - 1,
                textMetrics.width + 2,
                textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent + 2);
            IGVGraphics.fillText(ctx, name, centerX, labelY, geneFontStyle, transform);

        }

    } finally {
        ctx.restore();
    }
}

function getFeatureLabelY(featureY, transform) {
    return transform ? featureY + 20 : featureY + 25
}

function getAminoAcidLetterWithExonGap(strand, phase, phaseExtentStart, phaseExtentEnd, remainder, leftExon, exon, riteExon, sequenceInterval) {

    let ss;
    let ee;
    let stringA = '';
    let stringB = '';
    let triplet = '';

    const aminoAcidLetters = {left: undefined, rite: undefined};
    if ('+' === strand) {

        if (phase) {
            stringB = sequenceInterval.getSequence(phaseExtentStart, phaseExtentEnd);

            if (!stringB) {
                return undefined
            }

            [ss, ee] = [getExonEnd(leftExon) - (3 - phase), getExonEnd(leftExon)];
            stringA = sequenceInterval.getSequence(ss, ee);

            if (!stringA) {
                return undefined
            }

            triplet = stringA + stringB;
            aminoAcidLetters.left = {triplet, aminoAcidLetter: translationDict[triplet]};
        }

        if (remainder) {
            stringA = sequenceInterval.getSequence(remainder.start, remainder.end);

            if (!stringA) {
                return undefined
            }

            const ritePhase = getExonPhase(riteExon);
            const riteStart = getEonStart(riteExon);
            stringB = sequenceInterval.getSequence(riteStart, riteStart + ritePhase);

            if (!stringB) {
                return undefined
            }

            triplet = stringA + stringB;
            aminoAcidLetters.rite = {triplet, aminoAcidLetter: translationDict[triplet]};
        }

    } else {

        if (phase) {
            stringA = sequenceInterval.getSequence(phaseExtentStart, phaseExtentEnd);

            if (undefined === stringA) {
                return undefined
            }

            [ss, ee] = [getEonStart(riteExon), getEonStart(riteExon) + (3 - phase)];
            stringB = sequenceInterval.getSequence(ss, ee);

            if (undefined === stringB) {
                return undefined
            }

            triplet = stringA + stringB;
            triplet = complementSequence(triplet.split('').reverse().join(''));
            aminoAcidLetters.rite = {triplet, aminoAcidLetter: translationDict[triplet]};
        }

        if (remainder) {
            stringB = sequenceInterval.getSequence(remainder.start, remainder.end);

            if (undefined === stringB) {
                return undefined
            }

            const leftPhase = getExonPhase(leftExon);
            const leftEnd = getExonEnd(leftExon);
            stringA = sequenceInterval.getSequence(leftEnd - leftPhase, leftEnd);

            if (undefined === stringA) {
                return undefined
            }

            triplet = stringA + stringB;
            triplet = complementSequence(triplet.split('').reverse().join(''));
            aminoAcidLetters.left = {triplet, aminoAcidLetter: translationDict[triplet]};
        }
    }

    return aminoAcidLetters
}

// SNP constants

const codingNonSynonSet = new Set(['nonsense', 'missense', 'stop-loss', 'frameshift', 'cds-indel']);
const codingSynonSet = new Set(['coding-synon']);
const spliceSiteSet = new Set(['splice-3', 'splice-5']);
const untranslatedSet = new Set(['untranslated-5', 'untranslated-3']);


/**
 * Renderer for a UCSC snp track
 *
 * @param snp
 * @param bpStart  genomic location of the left edge of the current canvas
 * @param xScale  scale in base-pairs per pixel
 * @param pixelHeight  pixel height of the current canvas
 * @param ctx  the canvas 2d context
 */
function renderSnp(snp, bpStart, xScale, pixelHeight, ctx) {

    var coord = calculateFeatureCoordinates(snp, bpStart, xScale),
        py = this.margin,
        h,
        colorArrLength = this.snpColors.length,
        colorPriority;

    h = this.displayMode === "squished" ? this.squishedRowHeight : this.expandedRowHeight;

    switch (this.colorBy) {
        case 'function':
            colorPriority = colorByFunc(snp.func);
            break
        case 'class':
            colorPriority = colorByClass(snp['class']);
    }

    ctx.fillStyle = this.snpColors[colorPriority];
    ctx.fillRect(coord.px, py, coord.pw, h);

    // Coloring functions, convert a value to a priority

    function colorByFunc(theFunc) {
        var priorities;
        var funcArray = theFunc.split(',');
        // possible func values


        priorities = funcArray.map(function (func) {
            if (codingNonSynonSet.has(func) || spliceSiteSet.has(func)) {
                return colorArrLength - 1
            } else if (codingSynonSet.has(func)) {
                return colorArrLength - 2
            } else if (untranslatedSet.has(func)) {
                return colorArrLength - 3
            } else { // locusSet.has(func) || intronSet.has(func)
                return 0
            }
        });

        return priorities.reduce(function (a, b) {
            return Math.max(a, b)
        })
    }

    function colorByClass(cls) {
        if (cls === 'deletion') {
            return colorArrLength - 1
        } else if (cls === 'mnp') {
            return colorArrLength - 2
        } else if (cls === 'microsatellite' || cls === 'named') {
            return colorArrLength - 3
        } else { // cls === 'single' || cls === 'in-del' || cls === 'insertion'
            return 0
        }
    }
}

/**
 *
 * @param feature
 * @param bpStart  genomic location of the left edge of the current canvas
 * @param xScale  scale in base-pairs per pixel
 * @param pixelHeight  pixel height of the current canvas
 * @param ctx  the canvas 2d context
 */
function renderFusionJuncSpan(feature, bpStart, xScale, pixelHeight, ctx) {

    const rowHeight = (this.displayMode === "EXPANDED") ? this.expandedRowHeight : this.squishedRowHeight;
    let py = this.margin;
    if (this.displayMode !== "COLLAPSED" && feature.row !== undefined) {
        py += feature.row * rowHeight;
    }

    const cy = py + 0.5 * rowHeight;
    const topY = cy - 0.5 * rowHeight;
    const bottomY = cy + 0.5 * rowHeight;

    // draw the junction arc
    const junctionLeftPx = Math.round((feature.junction_left - bpStart) / xScale);
    const junctionRightPx = Math.round((feature.junction_right - bpStart) / xScale);

    ctx.beginPath();
    ctx.moveTo(junctionLeftPx, cy);
    ctx.bezierCurveTo(junctionLeftPx, topY, junctionRightPx, topY, junctionRightPx, cy);

    ctx.lineWidth = 1 + Math.log(feature.num_junction_reads) / Math.log(2);
    ctx.strokeStyle = 'blue';
    ctx.stroke();

    // draw the spanning arcs
    const spanningCoords = feature.spanning_frag_coords;
    for (let i = 0; i < spanningCoords.length; i++) {

        const spanningInfo = spanningCoords[i];
        const spanLeftPx = Math.round((spanningInfo.left - bpStart) / xScale);
        const spanRightPx = Math.round((spanningInfo.right - bpStart) / xScale);

        ctx.beginPath();
        ctx.moveTo(spanLeftPx, cy);
        ctx.bezierCurveTo(spanLeftPx, bottomY, spanRightPx, bottomY, spanRightPx, cy);

        ctx.lineWidth = 1;
        ctx.strokeStyle = 'purple';
        ctx.stroke();
    }
}

class FeatureTrack extends TrackBase {

    static defaultColor = 'rgb(0,0,150)'

    static defaults = {
        type: "annotation",
        maxRows: 1000, // protects against pathological feature packing cases (# of rows of overlapping feaures)
        displayMode: "EXPANDED", // COLLAPSED | EXPANDED | SQUISHED
        margin: 10,
        featureHeight: 14,
        useScore: false
    }

    constructor(config, browser) {
        super(config, browser);
    }

    init(config) {
        super.init(config);

        // Obscure option, not common or supoorted, included for backward compatibility
        this.labelDisplayMode = config.labelDisplayMode;

        if (config._featureSource) {
            this.featureSource = config._featureSource;
            delete config._featureSource;
        } else if ('blat' !== config.type) {
            this.featureSource = config.featureSource ?
                config.featureSource :
                FeatureSource(config, this.browser.genome);
        }

        if ("FusionJuncSpan" === config.type) {
            this.render = config.render || renderFusionJuncSpan;
            this.squishedRowHeight = config.squishedRowHeight || 50;
            this.expandedRowHeight = config.expandedRowHeight || 50;
            this.height = config.height || this.margin + 2 * this.expandedRowHeight;
        } else if ('snp' === config.type) {
            this.render = config.render || renderSnp;
            // colors ordered based on priority least to greatest
            this.snpColors = ['rgb(0,0,0)', 'rgb(0,0,255)', 'rgb(0,255,0)', 'rgb(255,0,0)'];
            this.colorBy = 'function';
            this.expandedRowHeight = config.expandedRowHeight || 10;
            this.squishedRowHeight = config.squishedRowHeight || 5;
            this.height = config.height || 30;
        } else {
            this.render = config.render || renderFeature;
            this.arrowSpacing = 30;
            // adjust label positions to make sure they're always visible
            monitorTrackDrag(this);
            this.squishedRowHeight = config.squishedRowHeight || 15;
            this.expandedRowHeight = config.expandedRowHeight || 30;
            this.height = config.height || this.margin + 2 * this.expandedRowHeight;

            // Set colorBy fields considering legacy options for backward compatibility
            if (config.colorBy) {
                if (config.colorBy.field) {
                    config.colorTable = config.colorBy.pallete || config.colorBy.palette;
                    config.colorBy = config.colorBy.field;
                }
                this.colorBy = config.colorBy;   // Can be undefined => default
            }
            if (config.colorTable) {
                this.colorTable = new ColorTable(config.colorTable);
            } else {
                this.colorTable = new PaletteColorTable("Set1");
            }
        }
    }

    async postInit() {

        if (typeof this.featureSource.getHeader === "function") {
            this.header = await this.featureSource.getHeader();
            if (this.disposed) return   // This track was removed during async load
        }

        // Set properties from track line
        if (this.header) {
            this.setTrackProperties(this.header);
        }

        if (this.visibilityWindow === undefined && typeof this.featureSource.defaultVisibilityWindow === 'function') {
            this.visibilityWindow = await this.featureSource.defaultVisibilityWindow();
        }

        this._initialColor = this.color || this.constructor.defaultColor;
        this._initialAltColor = this.altColor || this.constructor.defaultColor;

        return this

    }

    /**
     * Return true if this track can be searched for genome location by feature property.
     * This track is searchable if its featureSource is searchable.
     * @returns {boolean}
     */
    get searchable() {
        return this.featureSource.searchable
    }

    async search(locus) {
        if (this.featureSource && this.featureSource.searchable) {
            return this.featureSource.search(locus)
        } else {
            return undefined
        }
    }

    /**
     * Return boolean indicating if this track supports the whole genome view.  Generally this is non-indexed feature
     * tracks.
     *
     * @returns {*|boolean}
     */
    get supportsWholeGenome() {
        if (this.config.supportsWholeGenome !== undefined) {
            return this.config.supportsWholeGenome
        } else if (this.featureSource && typeof this.featureSource.supportsWholeGenome === 'function') {
            return this.featureSource.supportsWholeGenome()
        } else {
            if (this.visibilityWindow === undefined && (this.config.indexed === false || !this.config.indexURL)) {
                return true
            }
        }
    }

    async getFeatures(chr, start, end, bpPerPixel) {
        const visibilityWindow = this.visibilityWindow;
        return this.featureSource.getFeatures({chr, start, end, bpPerPixel, visibilityWindow})
    }

    /**
     * The required height in pixels required for the track content.   This is not the visible track height, which
     * can be smaller (with a scrollbar) or larger.
     *
     * @param features
     * @returns {*}
     */
    computePixelHeight(features) {

        if (this.displayMode === "COLLAPSED") {
            return this.margin + this.expandedRowHeight
        } else {
            let maxRow = 0;
            if (features && (typeof features.forEach === "function")) {
                for (let feature of features) {
                    if (feature.row && feature.row > maxRow) {
                        maxRow = feature.row;
                    }
                }
            }

            const height = this.margin + (maxRow + 1) * ("SQUISHED" === this.displayMode ? this.squishedRowHeight : this.expandedRowHeight);
            return height

        }
    };

    /**
     *                 context: ctx,
     *                 pixelXOffset,
     *                 pixelWidth,
     *                 pixelHeight,
     *                 pixelTop,
     *                 bpStart,
     *                 bpEnd: bpEnd,
     *                 bpPerPixel,
     *                 windowFunction: this.windowFunction,
     *                 referenceFrame: this.referenceFrame,
     *                 selection: this.selection,
     *                 viewport: this,
     *                 viewportWidth: this.$viewport.width()
     * @param options
     */
    draw(options) {

        const {features, context, bpPerPixel, bpStart, bpEnd, pixelWidth, pixelHeight, referenceFrame} = options;

        // If drawing amino acids fetch cached sequence interval.  It is not needed if track does not support AA, but
        // costs nothing since only a reference to a cached object is fetched.
        if (bpPerPixel < aminoAcidSequenceRenderThreshold) {
            // Restrict the range requested to the limits: 1-chromosome.bpLength
            const chromosome = this.browser.genome.getChromosome(referenceFrame.chr);
            const chromosomeEnd = chromosome.bpLength;
            options.sequenceInterval = this.browser.genome.getSequenceInterval(referenceFrame.chr,
                bpStart > 0 ? bpStart : 0, bpEnd > chromosomeEnd ? chromosomeEnd : bpEnd);
        }


        if (!this.isMergedTrack) {
            IGVGraphics.fillRect(context, 0, options.pixelTop, pixelWidth, pixelHeight, {'fillStyle': "rgb(255, 255, 255)"});
        }

        if (features) {

            const rowFeatureCount = [];
            options.rowLastX = [];
            options.rowLastLabelX = [];
            for (let feature of features) {
                if (this._filter && !this._filter(feature)) continue
                if (feature.start > bpStart && feature.end < bpEnd) {
                    const row = this.displayMode === "COLLAPSED" ? 0 : feature.row || 0;
                    if (!rowFeatureCount[row]) {
                        rowFeatureCount[row] = 1;
                    } else {
                        rowFeatureCount[row]++;
                    }
                    options.rowLastX[row] = -Number.MAX_SAFE_INTEGER;
                    options.rowLastLabelX[row] = -Number.MAX_SAFE_INTEGER;
                }
            }
            const maxFeatureCount = Math.max(1, Math.max(...(rowFeatureCount.filter(c => !isNaN(c)))));
            const pixelsPerFeature = pixelWidth / maxFeatureCount;

            let lastPxEnd = [];
            const selectedFeatures = [];
            for (let feature of features) {

                if (this._filter && !this._filter(feature)) continue
                if (feature.end < bpStart) continue
                if (feature.start > bpEnd) break

                if (this.displayMode === 'COLLAPSED' && this.browser.qtlSelections.hasPhenotype(feature.name)) {
                    selectedFeatures.push(feature);
                }

                const row = this.displayMode === 'COLLAPSED' ? 0 : feature.row;
                options.drawLabel = options.labelAllFeatures || pixelsPerFeature > 10;
                const pxEnd = Math.ceil((feature.end - bpStart) / bpPerPixel);
                const last = lastPxEnd[row];
                if (!last || pxEnd > last) {

                    this.render.call(this, feature, bpStart, bpPerPixel, pixelHeight, context, options);

                    // Ensure a visible gap between features
                    const pxStart = Math.floor((feature.start - bpStart) / bpPerPixel);
                    if (last && pxStart - last <= 0) {
                        context.globalAlpha = 0.5;
                        IGVGraphics.strokeLine(context, pxStart, 0, pxStart, pixelHeight, {'strokeStyle': "rgb(255, 255, 255)"});
                        context.globalAlpha = 1.0;
                    }
                    lastPxEnd[row] = pxEnd;
                }
            }

            // If any features are selected redraw them here.  This insures selected features are visible in collapsed mode
            for (let feature of selectedFeatures) {
                options.drawLabel = true;
                this.render.call(this, feature, bpStart, bpPerPixel, pixelHeight, context, options);
            }

        }

    };

    clickedFeatures(clickState) {

        const y = clickState.y - this.margin;
        const allFeatures = super.clickedFeatures(clickState);

        let row;
        switch (this.displayMode) {
            case 'SQUISHED':
                row = Math.floor(y / this.squishedRowHeight);
                break
            case 'EXPANDED':
                row = Math.floor(y / this.expandedRowHeight);
                break
            default:
                row = undefined;
        }

        return allFeatures.filter(function (feature) {
            return (row === undefined || feature.row === undefined || row === feature.row)
        })
    }

    /**
     * Return "popup data" for feature @ genomic location.  Data is an array of key-value pairs
     */
    popupData(clickState, features) {

        if (features === undefined) features = this.clickedFeatures(clickState);
        const genomicLocation = clickState.genomicLocation;
        const data = [];
        for (let feature of features) {

            // Whole genome hack, whole-genome psuedo features store the "real" feature in an _f field
            const f = feature._f || feature;

            const featureData = (typeof f.popupData === "function") ?
                f.popupData(genomicLocation) :
                this.extractPopupData(f);

            if (featureData) {

                if (data.length > 0) {
                    data.push("<hr/><hr/>");
                }

                // If we have an infoURL, find the name property and create the link.  We do this at this level
                // to catch name properties in both custom popupData functions and the generic extractPopupData function

                const infoURL = this.infoURL || this.config.infoURL;
                for (let fd of featureData) {
                    data.push(fd);
                    if (infoURL &&
                        fd.name &&
                        fd.name.toLowerCase() === "name" &&
                        fd.value &&
                        isString$3(fd.value) &&
                        !fd.value.startsWith("<")) {
                        const href = infoURL.replace("$$", feature.name);
                        fd.value = `<a target=_blank href=${href}>${fd.value}</a>`;
                    }
                }


                //Array.prototype.push.apply(data, featureData);

                // If we have clicked over an exon number it.
                // Disabled for GFF and GTF files if the visibility window is < the feature length since we don't know if we have all exons
                const isGFF = "gff" === this.config.format || "gff3" === this.config.format || "gtf" === this.config.format;
                if (f.exons && f.exons.length > 1) {
                    for (let i = 0; i < f.exons.length; i++) {
                        const exon = f.exons[i];
                        if (genomicLocation >= exon.start && genomicLocation <= exon.end) {
                            const exonNumber = isGFF ?
                                exon.number :
                                f.strand === "-" ? f.exons.length - i : i + 1;
                            if (exonNumber) {
                                data.push('<hr/>');
                                data.push({name: "Exon Number", value: exonNumber});
                            }
                            break
                        }
                    }
                }
            }
        }

        return data

    }

    menuItemList() {

        const menuItems = [];

        if (this.render === renderSnp) {
            menuItems.push('<hr/>');

            for (const colorScheme of ["function", "class"]) {

                function colorSchemeHandler() {
                    this.colorBy = colorScheme;
                    this.trackView.repaintViews();
                }

                menuItems.push({
                    element: createCheckbox(`Color by ${colorScheme}`, colorScheme === this.colorBy),
                    click: colorSchemeHandler
                });
            }
        }

        menuItems.push('<hr/>');

        const lut =
            {
                "COLLAPSED": "Collapse",
                "SQUISHED": "Squish",
                "EXPANDED": "Expand"
            };

        for (const displayMode of ["COLLAPSED", "SQUISHED", "EXPANDED"]) {

            function displayModeHandler() {
                this.displayMode = displayMode;
                this.config.displayMode = displayMode;
                this.trackView.checkContentHeight();
                this.trackView.repaintViews();
            }

            menuItems.push({
                element: createCheckbox(lut[displayMode], displayMode === this.displayMode),
                click: displayModeHandler
            });
        }

        return menuItems

    };


    contextMenuItemList(clickState) {

        const features = this.clickedFeatures(clickState);

        if (undefined === features || 0 === features.length) {
            return undefined
        }

        if (features.length > 1) {
            features.sort((a, b) => (b.end - b.start) - (a.end - a.start));
        }
        const f = features[0];   // The shortest clicked feature

        if ((f.end - f.start) <= 1000000) {
            const list = [{
                label: 'View feature sequence',
                click: async () => {
                    let seq = await this.browser.genome.getSequence(f.chr, f.start, f.end);
                    if (!seq) {
                        seq = "Unknown sequence";
                    } else if (f.strand === '-') {
                        seq = reverseComplementSequence(seq);
                    }
                    this.browser.alert.present(seq);

                }
            }];

            if (isSecureContext() && navigator.clipboard !== undefined) {
                list.push(
                    {
                        label: 'Copy feature sequence',
                        click: async () => {
                            let seq = await this.browser.genome.getSequence(f.chr, f.start, f.end);
                            if (!seq) {
                                seq = "Unknown sequence";
                            } else if (f.strand === '-') {
                                seq = reverseComplementSequence(seq);
                            }
                            try {
                                await navigator.clipboard.writeText(seq);
                            } catch (e) {
                                console.error(e);
                                this.browser.alert.present(`error copying sequence to clipboard ${e}`);
                            }
                        }
                    }
                );
            }
            list.push('<hr/>');
            return list
        } else {
            return undefined
        }
    }

    description() {

        // if('snp' === this.type) {
        if (renderSnp === this.render) {
            let desc = "<html>" + this.name + '<hr/>';
            desc += '<em>Color By Function:</em><br>';
            desc += '<span style="color:red">Red</span>: Coding-Non-Synonymous, Splice Site<br>';
            desc += '<span style="color:green">Green</span>: Coding-Synonymous<br>';
            desc += '<span style="color:blue">Blue</span>: Untranslated<br>';
            desc += '<span style="color:black">Black</span>: Intron, Locus, Unknown<br><br>';
            desc += '<em>Color By Class:</em><br>';
            desc += '<span style="color:red">Red</span>: Deletion<br>';
            desc += '<span style="color:green">Green</span>: MNP<br>';
            desc += '<span style="color:blue">Blue</span>: Microsatellite, Named<br>';
            desc += '<span style="color:black">Black</span>: Indel, Insertion, SNP';
            desc += "</html>";
            return desc
        } else {
            return super.description()
        }

    };

    /**
     * Return color for feature.
     * @param feature
     * @returns {string}
     */

    getColorForFeature(f) {

        const feature = f._f || f;    // f might be a "whole genome" wrapper

        let color;

        if (f.name && this.browser.qtlSelections.hasPhenotype(f.name)) {
            color = this.browser.qtlSelections.colorForGene(f.name);
        } else if (this.altColor && "-" === feature.strand) {
            color = (typeof this.altColor === "function") ? this.altColor(feature) : this.altColor;
        } else if (this.color) {
            color = (typeof this.color === "function") ? this.color(feature) : this.color;  // Explicit setting via menu, or possibly track line if !config.color
        } else if (this.colorBy) {
            const value = feature.getAttributeValue ?
                feature.getAttributeValue(this.colorBy) :
                feature[this.colorBy];
            color = this.colorTable.getColor(value);
        } else if (feature.color) {
            color = feature.color;   // Explicit color for feature
        }

        // If no explicit setting use the default
        if (!color) {
            color = FeatureTrack.defaultColor;   // Track default
        }

        if (feature.alpha && feature.alpha !== 1) {
            color = IGVColor.addAlpha(color, feature.alpha);
        } else if (this.useScore && feature.score && !Number.isNaN(feature.score)) {
            // UCSC useScore option, for scores between 0-1000.  See https://genome.ucsc.edu/goldenPath/help/customTrack.html#TRACK
            const min = this.config.min ? this.config.min : this.viewLimitMin ? this.viewLimitMin : 0;
            const max = this.config.max ? this.config.max : this.viewLimitMax ? this.viewLimitMax : 1000;
            const alpha = getAlpha(min, max, feature.score);
            feature.alpha = alpha;    // Avoid computing again
            color = IGVColor.addAlpha(color, alpha);
        }


        function getAlpha(min, max, score) {
            const binWidth = (max - min) / 9;
            const binNumber = Math.floor((score - min) / binWidth);
            return Math.min(1.0, 0.2 + (binNumber * 0.8) / 9)
        }

        return color
    }


    /**
     * Called when the track is removed.  Do any needed cleanup here
     */
    dispose() {
        this.trackView = undefined;
    }
}

/**
 * Monitors track drag events, updates label position to ensure that they're always visible.
 * @param track
 */
function monitorTrackDrag(track) {

    if (track.browser.on) {
        track.browser.on('trackdragend', onDragEnd);
        track.browser.on('trackremoved', unSubscribe);
    }

    function onDragEnd() {
        if (track.trackView && track.displayMode !== "SQUISHED") {
            // Repaint views to adjust feature name if center is moved out of view
            track.trackView.repaintViews();
        }
    }

    function unSubscribe(removedTrack) {
        if (track.browser.un && track === removedTrack) {
            track.browser.un('trackdragend', onDragEnd);
            track.browser.un('trackremoved', unSubscribe);
        }
    }

}

class RegionTableBase {
    constructor(config) {

        this.config = config;

        this.browser = config.browser;

        this.columnFormat = config.columnFormat;

        this.tableRowSelectionList = [];

        this.tableDOM = div$2({ class: 'igv-roi-table' });

        if(config.width) {
            let [ w ] = config.width.split('px');
            w = parseInt(w, 10);
            this.tableDOM.style.width = `${Math.min(w, 1600)}px`;
        }

        config.parent.appendChild(this.tableDOM);

        this.headerDOM = config;

        this.tableColumnTitles = this.tableDOM;

        this.tableRowContainer = this.tableDOM;

        this.footerDOM = config.gotoButtonHandler;

    }

    set headerDOM({ browser, parent, headerTitle, dismissHandler }) {

        // header
        const dom = div$2();
        this.tableDOM.appendChild(dom);

        // header title
        const div$1 = div$2();
        dom.appendChild(div$1);
        div$1.innerHTML = headerTitle;

        // table dismiss button
        const dismiss = div$2();
        dom.appendChild(dismiss);
        dismiss.appendChild(createIcon$1('times'));

        this.boundDismissHandler = mouseClickHandler.bind(this);

        dismiss.addEventListener('click', this.boundDismissHandler);

        function mouseClickHandler (event) {
            event.stopPropagation();
            dismissHandler();
        }

        const { y:y_root } = browser.root.getBoundingClientRect();
        const { y:y_parent } = parent.getBoundingClientRect();
        const constraint = -(y_parent - y_root);
        makeDraggable$1(this.tableDOM, dom, { minX:0, minY:constraint });

        this.tableDOM.style.display = 'none';

        this._headerDOM = dom;

    }

    set tableColumnTitles(tableDOM) {

        const tblColumnTitles = div$2({ class: 'igv-roi-table-column-titles' });
        tableDOM.appendChild(tblColumnTitles);

        for (const { label, width } of this.columnFormat) {
            const col = div$2();
            tblColumnTitles.appendChild(col);
            col.style.width = width;
            col.innerText = label;
        }

        this._tableColumnTitlesDOM = tblColumnTitles;

    }

    get tableColumnTitles() {
        return this._tableColumnTitlesDOM
    }

    set tableRowContainer(container) {

        const tblRowContainer = div$2({ class: 'igv-roi-table-row-container' });
        container.appendChild(tblRowContainer);

        this._tableRowContainerDOM = tblRowContainer;

    }

    get tableRowContainer() {
        return this._tableRowContainerDOM
    }

    set footerDOM(gotoButtonHandler) {

        const dom = div$2();
        this.tableDOM.appendChild(dom);

        // Go To Button
        const gotoButton = div$2({class: 'igv-roi-table-button'});
        dom.appendChild(gotoButton);

        gotoButton.id = 'igv-roi-table-view-button';
        gotoButton.textContent = 'Go To';
        gotoButton.style.pointerEvents = 'none';

        this._footerDOM = dom;

        this.gotoButton = gotoButton;

        this.boundGotoButtonHandler = gotoButtonHandler.bind(this);

        this.gotoButton.addEventListener('click', this.boundGotoButtonHandler);

    }

    tableRowDOMHelper(dom) {

        dom.addEventListener('mousedown', event => {
            event.stopPropagation();

            dom.classList.toggle('igv-roi-table-row-selected');
            dom.classList.contains('igv-roi-table-row-selected') ? dom.classList.remove('igv-roi-table-row-hover') : dom.classList.add('igv-roi-table-row-hover');

            this.setTableRowSelectionState(dom.classList.contains('igv-roi-table-row-selected'));
        });

        dom.addEventListener('mouseover', e => {
            dom.classList.contains('igv-roi-table-row-selected') ? dom.classList.remove('igv-roi-table-row-hover') : dom.classList.add('igv-roi-table-row-hover');
        });

        dom.addEventListener('mouseout', e => {
            dom.classList.remove('igv-roi-table-row-hover');
        });

    }

    clearTable() {
        const elements = this.tableRowContainer.querySelectorAll('.igv-roi-table-row');
        for (let el of elements) {
            el.remove();
        }
    }

    setTableRowSelectionState(isTableRowSelected) {
        isTableRowSelected ? this.tableRowSelectionList.push(1) : this.tableRowSelectionList.pop();
        this.gotoButton.style.pointerEvents = this.tableRowSelectionList.length > 0 ? 'auto' : 'none';
    }

    present() {
        this.tableDOM.style.left = `${ 0 }px`;

        const { y:y_root } = this.browser.root.getBoundingClientRect();
        const { y:y_parent } = this.config.parent.getBoundingClientRect();

        this.tableDOM.style.top  = `${ y_root - y_parent }px`;
        this.tableDOM.style.display = 'flex';
    }

    dismiss() {
        this.tableDOM.style.display = 'none';
    }

    isVisible() {
        return this.tableDOM.style.display !== 'none'
    }

    dispose() {

        this.tableDOM.innerHTML = '';
        this.tableDOM.remove();

        for (const key of Object.keys(this)) {
            this[key] = undefined;
        }

        document.removeEventListener('click', this.boundDismissHandler);

    }

}

class BlatTable extends RegionTableBase {

    constructor(config) {

        const cooked = Object.assign({ 'width':'1024px' }, config);
        super(cooked);

        this.descriptionDOM = config;

    }

    set descriptionDOM(config) {

        if (config.description) {

            let dom;

            // BLAT result for query sequence
            dom = div$2({ class: 'igv-roi-table-description' });
            this.tableDOM.insertBefore(dom, this.tableColumnTitles);
            dom.style.height = 'auto';
            dom.innerHTML = `BLAT result for query sequence:`;

            // CTAATCAtctacactggtttctactg ...
            dom = div$2({ class: 'igv-roi-table-description' });
            this.tableDOM.insertBefore(dom, this.tableColumnTitles);
            dom.style.height = 'auto';
            dom.style.maxHeight = '128px';
            dom.innerHTML = config.description;

            // Select one or more rows ...
            dom = div$2({ class: 'igv-roi-table-goto-explainer' });
            this.tableDOM.insertBefore(dom, this.tableColumnTitles);
            dom.innerHTML = `Select one or more rows and click Go To to view the regions`;

        }

    }

    tableRowDOM(record) {

        const dom = div$2({ class: 'igv-roi-table-row' });

        const pretty = record.map(item => isFinite(item) ? numberFormatter$1(item) : item);

        for (let i = 0; i < pretty.length; i++) {

            const el = div$2();
            dom.appendChild(el);

            const format = this.columnFormat[ i ];
            el.style.width = format.width || 'fit-content';
            el.innerText = pretty[ i ];
        }

        this.tableRowDOMHelper(dom);

        return dom
    }

    renderTable(records) {

        Array.from(this.tableRowContainer.querySelectorAll('.igv-roi-table-row')).forEach(el => el.remove());

        if (records.length > 0) {

            for (let record of records) {
                const row = this.tableRowDOM(record);
                this.tableRowContainer.appendChild(row);
            }

        }

    }

    static getColumnFormatConfiguration() {

        /*
        return [
            { label:         'chr', width: '60px' },
            { label:       'start', width: '100px' },
            { label:         'end', width: '100px' },
            { label:      'strand', width: '50px' },
            { label:       'score', width: '50px' },
            { label:       'match', width: '50px' },
            { label:   "mis-match", width: '70px' },
            { label:  "rep. match", width: '70px' },
            { label:         "N's", width: '32px' },
            { label: 'Q gap count', width: '90px' },
            { label: 'Q gap bases', width: '90px' },
            { label: 'T gap count', width: '90px' },
            { label: 'T gap bases', width: '90px' },
        ]
        */

        return [
            { label:         'chr', width: '7%' },
            { label:       'start', width: '12%' },
            { label:         'end', width: '12%' },
            { label:      'strand', width: '5%' },
            { label:       'score', width: '5%' },
            { label:       'match', width: '5%' },
            { label:   "mis-match", width: '7%' },
            { label:  "rep. match", width: '7%' },
            { label:         "N's", width: '3%' },
            { label: 'Q gap count', width: '9%' },
            { label: 'Q gap bases', width: '9%' },
            { label: 'T gap count', width: '9%' },
            { label: 'T gap bases', width: '9%' },
        ]
    }

    static gotoButtonHandler (event) {

        event.stopPropagation();

        const selectedRows = this.tableDOM.querySelectorAll('.igv-roi-table-row-selected');

        const loci = [];
        for (const row of selectedRows) {

            const record = [];
            row.querySelectorAll('div').forEach(el => record.push(el.innerText));

            const [ chr, start, end ] = record;
            loci.push(`${ chr }:${ start }-${ end }`);
        }

        for (const el of this.tableDOM.querySelectorAll('.igv-roi-table-row')) {
            el.classList.remove('igv-roi-table-row-selected');
        }

        this.setTableRowSelectionState(false);

        this.browser.search(loci.join(' '));

        // console.log(`browser search( ${loci.join(' ')} )`)

    }

}

/*
http://genome.ucsc.edu/cgi-bin/hgBlat
?userSeq=CTAATCAtctacactggtttctactgaaatgtctgttgtcatagacttaattgtgtcttcagatacagcagttctgttatttctgagttttacctggggcaagagaatctttagcaagtttaaaggcacctatatctggaatcacccctccctccagatgaatatcacagactctcccattaaaggtcttgccTTCCTTGATAGCATCATCACTCCA
&type=DNA
&db=hg38
&output=json
 */

//const blatServer = "https://genome.ucsc.edu/cgi-bin/hgBlat"
const defaultBlatServer$1 = "https://igv.org/services/blatUCSC.php";
//const blatServer = "http://localhost:8000/blatUCSC.php"


async function blat({url, userSeq, db}) {

    url = url || defaultBlatServer$1;

    if(!db) {
        throw Error("Blat database is not defined")
    }

    const results = await postData(url, userSeq, db);

    results.fields;

    const features = results.blat.map(decodePSL);

    return features
}

async function postData(url = "", userSeq, db) {

    const data = new URLSearchParams();
    data.append("userSeq", userSeq);
    data.append("db", db);

    const response = await fetch(url, { method: "post", body: data });
    return response.json(); // parses JSON response into native JavaScript objects
}

const maxSequenceSize$1 = 25000;
//const blatServer = "https://genome.ucsc.edu/cgi-bin/hgBlat"
const defaultBlatServer = "https://igv.org/services/blatUCSC.php";
//const blatServer = "http://localhost:8000/blatUCSC.php"

class BlatTrack extends FeatureTrack {

    constructor(config, browser) {
        super(config, browser);
        if (!this.name) {
            this.name = 'Blat Results';
        }
        this.sequence = config.sequence;
        this.table = undefined;

        // On initial creation features are fetched before track construction
        if(config.features) {
            this._features = config.features;
            this.featureSource = new StaticFeatureSource({features: config.features}, this.browser.genome);
            delete config.features;
        }
    }

    async postInit() {
        if(!this.featureSource) {
            // This will be the case when restoring from a session
            const db = this.browser.genome.ucscID;   // TODO -- blat specific property
            const url = this.browser.config["blatServerURL"];
            const features = await blat({url, userSeq: this.sequence, db});
            this._features = features;
            this.featureSource = new StaticFeatureSource({features}, this.browser.genome);
        }

        this._initialColor = this.color || this.constructor.defaultColor;
        this._initialAltColor = this.altColor || this.constructor.defaultColor;

    }

    openTableView() {

        if (undefined === this.table) {

            const rows = this._features.map(f => [
                this.browser.genome.getChromosomeDisplayName(f.chr),
                (f.start + 1),
                f.end,
                f.strand,
                f.score,
                f.matches,
                f.misMatches,
                f.repMatches,
                f.nCount,
                f.qNumInsert,
                f.qBaseInsert,
                f.tNumInsert,
                f.tBaseInsert
            ]);

            const config =
                {
                    browser: this.browser,
                    parent: this.browser.columnContainer,
                    headerTitle: this.config.title,
                    description: this.sequence,
                    dismissHandler: () => {
                        this.table.dismiss();
                        this.table.dispose();
                        this.table = undefined;
                    },
                    columnFormat: BlatTable.getColumnFormatConfiguration(),
                    gotoButtonHandler: BlatTable.gotoButtonHandler
                };

            this.table = new BlatTable(config);
            this.table.renderTable(rows);
        }

        this.table.present();

    }

    menuItemList() {

        const menuItems = super.menuItemList();

        menuItems.push('<hr/>');

        function click() {
            this.openTableView();
        }
        menuItems.push({ label: 'Open table view', click });

        return menuItems
    }


    /**
     * Track has been permanently removed.  Release resources and other cleanup
     */
    dispose() {
        super.dispose();
        // Release DOM element for table
        if (this.table) {
            this.table.popover.parentElement.removeChild(this.table.popover);
        }
    }
}


async function createBlatTrack({sequence, browser, name, title}) {

    if (sequence.length > maxSequenceSize$1) {
        browser.alert.present(`Sequence size exceeds maximum allowed length (${sequence.length} > ${maxSequenceSize$1})`);
        return
    }

    try {

        const db = browser.genome.ucscID;   // TODO -- blat specific property
        const url = browser.config["blatServerURL"] || defaultBlatServer;
        const features = await blat({url, userSeq: sequence, db});

        const trackConfig = {
            type: 'blat',
            name: name || 'blat results',
            title: title || 'blat results',
            sequence: sequence,
            altColor: 'rgb(176, 176, 236)',
            color: 'rgb(236, 176, 176)',
            searchable: false,
            features
        };

        const track = (await browser.loadTrackList([trackConfig]))[0];
        track.openTableView();

    } catch (e) {
        browser.alert.present(`Error performing blat search:  ${e}`);
    }

}

const defaultSequenceTrackOrder = Number.MIN_SAFE_INTEGER;

const translationDict = {
    'TTT': 'F',
    'TTC': 'F',
    'TTA': 'L',
    'TTG': 'L',
    'CTT': 'L',
    'CTC': 'L',
    'CTA': 'L',
    'CTG': 'L',
    'ATT': 'I',
    'ATC': 'I',
    'ATA': 'I',
    'ATG': 'M',
    'GTT': 'V',
    'GTC': 'V',
    'GTA': 'V',
    'GTG': 'V',
    'TCT': 'S',
    'TCC': 'S',
    'TCA': 'S',
    'TCG': 'S',
    'CCT': 'P',
    'CCC': 'P',
    'CCA': 'P',
    'CCG': 'P',
    'ACT': 'T',
    'ACC': 'T',
    'ACA': 'T',
    'ACG': 'T',
    'GCT': 'A',
    'GCC': 'A',
    'GCA': 'A',
    'GCG': 'A',
    'TAT': 'Y',
    'TAC': 'Y',
    'TAA': 'STOP',
    'TAG': 'STOP',
    'CAT': 'H',
    'CAC': 'H',
    'CAA': 'Q',
    'CAG': 'Q',
    'AAT': 'N',
    'AAC': 'N',
    'AAA': 'K',
    'AAG': 'K',
    'GAT': 'D',
    'GAC': 'D',
    'GAA': 'E',
    'GAG': 'E',
    'TGT': 'C',
    'TGC': 'C',
    'TGA': 'STOP',
    'TGG': 'W',
    'CGT': 'R',
    'CGC': 'R',
    'CGA': 'R',
    'CGG': 'R',
    'AGT': 'S',
    'AGC': 'S',
    'AGA': 'R',
    'AGG': 'R',
    'GGT': 'G',
    'GGC': 'G',
    'GGA': 'G',
    'GGG': 'G'
};

const complement = {};
const t1 = ['A', 'G', 'C', 'T', 'Y', 'R', 'W', 'S', 'K', 'M', 'D', 'V', 'H', 'B', 'N', 'X'];
const t2 = ['T', 'C', 'G', 'A', 'R', 'Y', 'W', 'S', 'M', 'K', 'H', 'B', 'D', 'V', 'N', 'X'];
for (let i = 0; i < t1.length; i++) {
    complement[t1[i]] = t2[i];
    complement[t1[i].toLowerCase()] = t2[i].toLowerCase();
}

const DEFAULT_HEIGHT = 25;
const TRANSLATED_HEIGHT = 115;
const SEQUENCE_HEIGHT = 15;
const FRAME_HEIGHT = 25;
const FRAME_BORDER = 5;
const BP_PER_PIXEL_THRESHOLD = 1 / 10;

const bppSequenceThreshold = 10;

class SequenceTrack {


    constructor(config, browser) {

        this.config = config;
        this.browser = browser;
        this.type = "sequence";
        this.removable = config.removable === true;  // defaults to false
        this.name = config.name;
        this.id = config.id;
        this.sequenceType = config.sequenceType || "dna";             //   dna | rna | prot
        this.disableButtons = false;
        this.order = config.order || defaultSequenceTrackOrder;
        this.ignoreTrackMenu = false;

        this.reversed = config.reversed === true;
        this.frameTranslate = config.frameTranslate === true;
        this.height = this.frameTranslate ? TRANSLATED_HEIGHT : DEFAULT_HEIGHT;

        // Hack for backward compatibility
        if (config.url) {
            config.fastaURL = config.url;
        }

        if (!config.fastaURL) {
            // Mark this as the genome reference sequence ==> backward compatibility convention
            this.id = config.id || "sequence";
        }

    }

    menuItemList() {
        return [
            {
                name: this.reversed ? "Forward" : "Reverse",
                click: () => {
                    this.reversed = !this.reversed;
                    this.trackView.repaintViews();
                }
            },
            {
                name: this.frameTranslate ? "Close Translation" : "Three-frame Translate",
                click: () => {
                    this.frameTranslate = !this.frameTranslate;
                    if (this.frameTranslate) {
                        for (let vp of this.trackView.viewports) {
                            vp.setContentHeight(TRANSLATED_HEIGHT);
                        }
                        this.trackView.setTrackHeight(TRANSLATED_HEIGHT);
                    } else {
                        for (let vp of this.trackView.viewports) {
                            vp.setContentHeight(DEFAULT_HEIGHT);
                        }
                        this.trackView.setTrackHeight(DEFAULT_HEIGHT);
                    }
                    this.trackView.repaintViews();

                }
            }
        ]
    }

    contextMenuItemList(clickState) {
        const viewport = clickState.viewport;
        if (viewport.referenceFrame.bpPerPixel <= 1) {
            const pixelWidth = viewport.getWidth();
            const bpWindow = pixelWidth * viewport.referenceFrame.bpPerPixel;
            const chr = viewport.referenceFrame.chr;
            const start = Math.floor(viewport.referenceFrame.start);
            const end = Math.ceil(start + bpWindow);
            const items = [
                {
                    label: this.reversed ? 'View visible sequence (reversed)...' : 'View visible sequence...',
                    click: async () => {
                        let seq = await this.browser.genome.getSequence(chr, start, end);
                        if (!seq) {
                            seq = "Unknown sequence";
                        } else if (this.reversed) {
                            seq = reverseComplementSequence(seq);
                        }
                        this.browser.alert.present(seq);
                    }
                }
            ];

            if (isSecureContext()) {
                items.push({
                    label: 'Copy visible sequence',
                    click: async () => {
                        let seq = await this.browser.genome.getSequence(chr, start, end);
                        if (!seq) {
                            seq = "Unknown sequence";
                        } else if (this.reversed) {
                            seq = reverseComplementSequence(seq);
                        }
                        try {
                            await navigator.clipboard.writeText(seq);
                        } catch (e) {
                            console.error(e);
                            this.browser.alert.present(`error copying sequence to clipboard ${e}`);
                        }
                    }
                });
            }

            items.push({
                label: 'BLAT visible sequence',
                click: async () => {
                    let sequence = await this.browser.genome.getSequence(chr, start, end);
                    if (sequence) {
                        if (this.reversed) {
                            sequence = reverseComplementSequence(sequence);
                        }
                        const name = `blat: ${chr}:${start + 1}-${end}`;
                        const title = `blat: ${chr}:${start + 1}-${end}`;
                        createBlatTrack({sequence, browser: this.browser, name, title});
                    }
                }
            });


            items.push('<hr/>');

            return items
        } else {
            return undefined
        }
    }

    translateSequence(seq) {

        const threeFrame = [[], [], []];

        for (let fNum of [0, 1, 2]) {
            let idx = fNum;

            while ((seq.length - idx) >= 3) {
                let st = seq.slice(idx, idx + 3);
                if (this.reversed) {
                    st = st.split('').reverse().join('');
                }

                const aa = translationDict[st.toUpperCase()] || "";
                threeFrame[fNum].push({
                    codons: st,
                    aminoA: aa
                });
                idx += 3;
            }
        }

        return threeFrame
    }

    /**
     * Return the source for sequence.  If an explicit fasta url is defined, use it, otherwise fetch sequence
     * from the current genome
     * *
     * @returns {Promise<WrappedFasta|*>}
     */
    async getSequenceSource() {
        if (this.config.fastaURL) {
            if (!this.fasta) {
                this.fasta = new WrappedFasta(this.config, this.browser.genome);
                await this.fasta.init();
            }
            return this.fasta
        } else {
            return this.browser.genome
        }
    }

    async getFeatures(chr, start, end, bpPerPixel) {

        start = Math.floor(start);
        end = Math.floor(end);

        if (bpPerPixel && bpPerPixel > bppSequenceThreshold) {
            return null
        } else {
            const sequenceSource = await this.getSequenceSource();
            //const extent = expandRegion(start, end, 1e5)
            const sequence = await sequenceSource.getSequence(chr, start, end);
            return {
                bpStart: start,
                sequence: sequence
            }
        }
    }

    draw(options) {

        const ctx = options.context;

        if (options.features) {

            let sequence = options.features.sequence;
            if (!sequence) {
                return
            }

            if (this.reversed) {
                sequence = sequence.split('').map(function (cv) {
                    return complement[cv]
                }).join('');
            }

            const sequenceBpStart = options.features.bpStart;  // genomic position at start of sequence
            const bpEnd = 1 + options.bpStart + (options.pixelWidth * options.bpPerPixel);

            for (let bp = Math.floor(options.bpStart); bp <= bpEnd; bp++) {

                const seqIdx = Math.floor(bp - sequenceBpStart);

                if (seqIdx >= 0 && seqIdx < sequence.length) {

                    const offsetBP = bp - options.bpStart;
                    const aPixel = offsetBP / options.bpPerPixel;
                    const pixelWidth = 1 / options.bpPerPixel;
                    const baseLetter = sequence[seqIdx];
                    const color = this.fillColor(baseLetter.toUpperCase());

                    if (options.bpPerPixel > BP_PER_PIXEL_THRESHOLD) {
                        IGVGraphics.fillRect(ctx, aPixel, FRAME_BORDER, pixelWidth, SEQUENCE_HEIGHT - FRAME_BORDER, {fillStyle: color});
                    } else {
                        const textPixel = aPixel + 0.5 * (pixelWidth - ctx.measureText(baseLetter).width);


                        if ('y' === options.axis) {
                            ctx.save();
                            IGVGraphics.labelTransformWithContext(ctx, textPixel);
                            IGVGraphics.strokeText(ctx, baseLetter, textPixel, SEQUENCE_HEIGHT, {strokeStyle: color});
                            ctx.restore();
                        } else {
                            IGVGraphics.strokeText(ctx, baseLetter, textPixel, SEQUENCE_HEIGHT, {strokeStyle: color});
                        }

                    }
                }
            }

            if (this.frameTranslate) {

                let y = SEQUENCE_HEIGHT + 2 * FRAME_BORDER;
                const translatedSequence = this.translateSequence(sequence);

                for (let fNum = 0; fNum < translatedSequence.length; fNum++) {    // == 3, 1 for each frame

                    const aaSequence = translatedSequence[fNum];  // AA sequence for this frame

                    for (let idx = 0; idx < aaSequence.length; idx++) {

                        let color = 0 === idx % 2 ? 'rgb(160,160,160)' : 'rgb(224,224,224)';
                        const cv = aaSequence[idx];

                        const bpPos = sequenceBpStart + fNum + (idx * 3);
                        const bpOffset = bpPos - options.bpStart;
                        const p0 = Math.floor(bpOffset / options.bpPerPixel);
                        const p1 = Math.floor((bpOffset + 3) / options.bpPerPixel);
                        const pc = Math.round((p0 + p1) / 2);

                        if (p1 < 0) {
                            continue   // off left edge
                        } else if (p0 > options.pixelWidth) {
                            break      // off right edge
                        }

                        let aaLabel = cv.aminoA;
                        if (cv.aminoA.indexOf('STOP') > -1) {
                            color = 'rgb(255, 0, 0)';
                            aaLabel = 'STOP'; //Color blind accessible
                        } else if (cv.aminoA === 'M') {
                            color = 'rgb(0, 153, 0)';
                            aaLabel = 'START'; //Color blind accessible
                        }

                        IGVGraphics.fillRect(ctx, p0, y, p1 - p0, FRAME_HEIGHT, {fillStyle: color});

                        if (options.bpPerPixel <= 1 / 10) {
                            IGVGraphics.strokeText(ctx, aaLabel, pc - (ctx.measureText(aaLabel).width / 2), y + 15);
                        }
                    }
                    y += (FRAME_HEIGHT + FRAME_BORDER);
                }
            }
        }
    }

    get supportsWholeGenome() {
        return false
    }

    computePixelHeight(ignore) {
        this.height = this.frameTranslate ? TRANSLATED_HEIGHT : DEFAULT_HEIGHT;
        return this.height
    }

    fillColor(index) {

        if (this.color) {
            return this.color
        } else if ("dna" === this.sequenceType) {
            return this.browser.nucleotideColors[index] || 'gray'
        } else {
            return 'rgb(0, 0, 150)'
        }
    }

    /**
     * Return the current state of the track.  Used to create sessions and bookmarks.
     *
     * @returns {*|{}}
     */
    getState() {
        const config = {
            type: "sequence"
        };
        if (this.order !== defaultSequenceTrackOrder) {
            config.order = this.order;
        }
        if (this.reversed) {
            config.revealed = true;
        }
        return config
    }

}

/**
 * Wrapper for a Fasta object that does chr name alias translation.   This is not neccessary for the genome fasta,
 * as it defines the reference name, but can be neccessary if loading an additional fasta as a track
 *
 */
class WrappedFasta {

    constructor(config, genome) {
        this.config = config;
        this.genome = genome;
    }

    async init() {
        this.fasta = await loadSequence(this.config);
        this.chrNameMap = new Map();
        for (let name of this.fasta.chromosomeNames) {
            this.chrNameMap.set(this.genome.getChromosomeName(name), name);
        }
    }

    async getSequence(chr, start, end) {
        const chrName = this.chrNameMap.has(chr) ? this.chrNameMap.get(chr) : chr;
        return this.fasta.getSequence(chrName, start, end)
    }

}

class Viewport {

    constructor(trackView, viewportColumn, referenceFrame, width) {
        this.guid = guid$2();
        this.trackView = trackView;
        this.referenceFrame = referenceFrame;

        this.browser = trackView.browser;

        this.viewportElement = document.createElement('div');
        this.viewportElement.className = 'igv-viewport';
        viewportColumn.appendChild(this.viewportElement);

        if (trackView.track.height) {
            this.setHeight(trackView.track.height);
        }

        // Associate the track type with the viewport element using data attribute
        const trackType = trackView.track.type || 'untyped';
        this.viewportElement.setAttribute('data-track-type', trackType);

        // Create an alert dialog for the sequence track to copy ref sequence to.
        if (trackView.track instanceof SequenceTrack) {
            this.alert = new AlertDialog$1(this.viewportElement);
        }

        this.contentTop = 0;
        this.contentHeight = this.viewportElement.clientHeight;

        this.setWidth(width);

        this.initializationHelper();
    }

    initializationHelper() {}

    showMessage(message) {
        if (!this.messageDiv) {
            this.messageDiv = document.createElement('div');
            this.messageDiv.className = 'igv-viewport-message';
            this.viewportElement.appendChild(this.messageDiv);
        }
        this.messageDiv.textContent = message;
        this.messageDiv.style.display = 'inline-block';
    }

    hideMessage() {
        if (this.messageDiv) {
            this.messageDiv.style.display = 'none';
        }
    }

    setTrackLabel(label) {}

    startSpinner() {}

    stopSpinner() {}

    checkZoomIn() {
        return true;
    }

    shift() {}

    setTop(contentTop) {
        this.contentTop = contentTop;
    }

    async loadFeatures() {
        return undefined;
    }

    clearCache() {}

    repaint() {}

    draw(drawConfiguration, features, roiFeatures) {
    }

    checkContentHeight(features) {
        const track = this.trackView.track;
        features = features || this.cachedFeatures;
        if (track.displayMode === 'FILL') {
            this.setContentHeight(this.viewportElement.clientHeight);
        } else if (typeof track.computePixelHeight === 'function') {
            if (features && features.length > 0) {
                const requiredContentHeight = track.computePixelHeight(features, this.referenceFrame.bpPerPixel);
                if (requiredContentHeight !== this.contentHeight) {
                    this.setContentHeight(requiredContentHeight);
                }
            }
        }
    }

    getContentHeight() {
        return this.contentHeight;
    }

    setContentHeight(contentHeight) {
        this.contentHeight = contentHeight;
    }

    isLoading() {
        return false;
    }

    saveSVG() {}

    isVisible() {
        return this.viewportElement.clientWidth > 0;
    }

    setWidth(width) {
        this.viewportElement.style.width = `${width}px`;
    }

    getWidth() {
        return this.viewportElement.clientWidth;
    }

    setHeight(height) {
        this.viewportElement.style.height = `${height}px`;
    }

    getContentTop() {
        return this.contentTop;
    }

    containsPosition(chr, position) {
    }

    addMouseHandlers() {}

    dispose() {
        this.viewportElement.remove();

        // Nullify all properties to free memory
        for (const key in this) {
            if (this.hasOwnProperty(key)) {
                this[key] = undefined;
            }
        }
    }
}

/*!!
 *  Canvas 2 Svg v1.0.19
 *  A low level canvas to SVG converter. Uses a mock canvas context to build an SVG document.
 *
 *  Licensed under the MIT license:
 *  http://www.opensource.org/licenses/mit-license.php
 *
 *  Author:
 *  Kerry Liu
 *
 *  Copyright (c) 2014 Gliffy Inc.
 */

//helper function to format a string
function format(str, args) {
    var keys = Object.keys(args), i;
    for (i = 0; i < keys.length; i++) {
        str = str.replace(new RegExp("\\{" + keys[i] + "\\}", "gi"), args[keys[i]]);
    }
    return str
}

//helper function that generates a random string
function randomString(holder) {
    var chars, randomstring, i;
    if (!holder) {
        throw new Error("cannot create a random attribute name for an undefined object")
    }
    chars = "ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
    randomstring = "";
    do {
        randomstring = "";
        for (i = 0; i < 12; i++) {
            randomstring += chars[Math.floor(Math.random() * chars.length)];
        }
    } while (holder[randomstring])
    return randomstring
}

//helper function to map named to numbered entities
function createNamedToNumberedLookup(items, radix) {
    var i, entity, lookup = {}, base10;
    items = items.split(',');
    radix = radix || 10;
    // Map from named to numbered entities.
    for (i = 0; i < items.length; i += 2) {
        entity = '&' + items[i + 1] + ';';
        base10 = parseInt(items[i], radix);
        lookup[entity] = '&#' + base10 + ';';
    }
    //FF and IE need to create a regex from hex values ie &nbsp; == \xa0
    lookup["\\xa0"] = '&#160;';
    return lookup
}

//helper function to map canvas-textAlign to svg-textAnchor
function getTextAnchor(textAlign) {
    //TODO: support rtl languages
    var mapping = {"left": "start", "right": "end", "center": "middle", "start": "start", "end": "end"};
    return mapping[textAlign] || mapping.start
}

//helper function to map canvas-textBaseline to svg-dominantBaseline
function getDominantBaseline(textBaseline) {
    //INFO: not supported in all browsers
    var mapping = {
        "alphabetic": "alphabetic",
        "hanging": "hanging",
        "top": "text-before-edge",
        "bottom": "text-after-edge",
        "middle": "central"
    };
    return mapping[textBaseline] || mapping.alphabetic
}


/**
 * Return a new normalized vector of given vector
 */
function normalize(vector) {
    var len = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1]);
    return [vector[0] / len, vector[1] / len]
}


function intersectRect(rect1, rect2) {
    return (rect1.x < rect2.x + rect2.width &&
        rect1.x + rect1.width > rect2.x &&
        rect1.y < rect2.y + rect2.height &&
        rect1.y + rect1.height > rect2.y)
}


// Unpack entities lookup where the numbers are in radix 32 to reduce the size
// entity mapping courtesy of tinymce
const namedEntities = createNamedToNumberedLookup(
    '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' +
    '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' +
    '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' +
    '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' +
    '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' +
    '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' +
    '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' +
    '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' +
    '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' +
    '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' +
    'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' +
    'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' +
    't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' +
    'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' +
    'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' +
    '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' +
    '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' +
    '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' +
    '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' +
    '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' +
    'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' +
    'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' +
    'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' +
    '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' +
    '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32);


//Some basic mappings for attributes and default values.
const STYLES = {
    "strokeStyle": {
        svgAttr: "stroke", //corresponding svg attribute
        canvas: "#000000", //canvas default
        svg: "none",       //svg default
        apply: "stroke"    //apply on stroke() or fill()
    },
    "fillStyle": {
        svgAttr: "fill",
        canvas: "#000000",
        svg: null, //svg default is black, but we need to special case this to handle canvas stroke without fill
        apply: "fill"
    },
    "lineCap": {
        svgAttr: "stroke-linecap",
        canvas: "butt",
        svg: "butt",
        apply: "stroke"
    },
    "lineJoin": {
        svgAttr: "stroke-linejoin",
        canvas: "miter",
        svg: "miter",
        apply: "stroke"
    },
    "miterLimit": {
        svgAttr: "stroke-miterlimit",
        canvas: 10,
        svg: 4,
        apply: "stroke"
    },
    "lineWidth": {
        svgAttr: "stroke-width",
        canvas: 1,
        svg: 1,
        apply: "stroke"
    },
    "globalAlpha": {
        svgAttr: "opacity",
        canvas: 1,
        svg: 1,
        apply: "fill stroke"
    },
    "font": {
        //font converts to multiple svg attributes, there is custom logic for this
        canvas: "10px sans-serif"
    },
    "shadowColor": {
        canvas: "#000000"
    },
    "shadowOffsetX": {
        canvas: 0
    },
    "shadowOffsetY": {
        canvas: 0
    },
    "shadowBlur": {
        canvas: 0
    },
    "textAlign": {
        canvas: "start"
    },
    "textBaseline": {
        canvas: "alphabetic"
    },
    "lineDash": {
        svgAttr: "stroke-dasharray",
        canvas: [],
        svg: null,
        apply: "stroke"
    }
};

/**
 *
 * @param gradientNode - reference to the gradient
 * @constructor
 */
class CanvasGradient {
    constructor(gradientNode, ctx) {
        this.__root = gradientNode;
        this.__ctx = ctx;
    }

    /**
     * Adds a color stop to the gradient root
     */
    addColorStop(offset, color) {
        var stop = this.__ctx.__createElement("stop"), regex, matches;
        stop.setAttribute("offset", offset);
        if (color && color.indexOf("rgba") !== -1) {
            //separate alpha value, since webkit can't handle it
            regex = /rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d?\.?\d*)\s*\)/gi;
            matches = regex.exec(color);
            stop.setAttribute("stop-color", format("rgb({r},{g},{b})", {r: matches[1], g: matches[2], b: matches[3]}));
            stop.setAttribute("stop-opacity", matches[4]);
        } else {
            stop.setAttribute("stop-color", color);
        }
        this.__root.appendChild(stop);
    }
}

class CanvasPattern {
    constructor(pattern, ctx) {
        this.__root = pattern;
        this.__ctx = ctx;
    }
}

/**
 * The mock canvas context
 * @param config - options include:
 * ctx - existing Context2D to wrap around
 * width - width of your canvas (defaults to 500)
 * height - height of your canvas (defaults to 500)
 * enableMirroring - enables canvas mirroring (get image data) (defaults to false)
 * document - the document object (defaults to the current document)
 */
class ctx {
    constructor(config) {

        if (!(this instanceof ctx)) {
            //did someone call this without new?
            return new ctx(config)
        }

        // clone config
        this.config = config;

        //setup options
        this.width = config.width;
        this.height = config.height;
        this.enableMirroring = config.enableMirroring || false;

        this.canvas = this;   ///point back to this instance!
        this.__document = document;

        // allow passing in an existing context to wrap around
        // if a context is passed in, we know a canvas already exist
        if (config.ctx) {
            this.__ctx = config.ctx;
        } else {
            this.__canvas = this.__document.createElement("canvas");
            this.__ctx = this.__canvas.getContext("2d");
        }

        // give this canvas a type
        this.isSVG = true;

        this.__setDefaultStyles();
        this.__stack = [this.__getStyleState()];
        this.__groupStack = [];

        // root svg element
        this.__root = this.__createElement("svg");
        this.__root.setAttribute("width", this.width);
        this.__root.setAttribute("height", this.height);

        // allow contents to overflow svg bbox
        this.__root.setAttribute('overflow', 'visible');

        // viewbox
        if (config.viewbox) {
            const str = config.viewbox.x + ' ' + config.viewbox.y + ' ' + config.viewbox.width + ' ' + config.viewbox.height;
            this.__root.setAttribute("viewBox", str);

            this.viewbox = config.viewbox;
        }

        // make sure we don't generate the same ids in defs
        this.__ids = {};

        // defs
        this.__defs = this.__createElement("defs");
        this.__root.appendChild(this.__defs);

        this.multiLocusGap = config.multiLocusGap;

        // svg background color
        let backdropConfig =
            {
                id: 'svg_output_backdrop',
                width: '100%',
                height: '100%',
                fill: config.backdropColor || 'white'
            };

        let backdropRect = this.__createElement('rect', backdropConfig);
        this.__root.appendChild(backdropRect);

        // root group
        this.__rootGroup = this.__createElement('g', {id: 'root-group'});
        this.__root.appendChild(this.__rootGroup);

        // point current element to root group
        this.__currentElement = this.__rootGroup;
    }

    setWidth(width) {

        this.width = width;
        this.__root.setAttribute("width", this.width);

        const str = this.config.viewbox.x + ' ' + this.config.viewbox.y + ' ' + width + ' ' + this.config.viewbox.height;
        this.__root.setAttribute("viewBox", str);

    };

    setHeight(height) {

        this.height = height;
        this.__root.setAttribute("height", this.height);

        const str = this.config.viewbox.x + ' ' + this.config.viewbox.y + ' ' + this.config.viewbox.width + ' ' + height;
        this.__root.setAttribute("viewBox", str);

    };

    /**
     * Creates the specified svg element
     * @private
     */
    __createElement(elementName, properties, resetFill) {

        if (typeof properties === "undefined") {
            properties = {};
        }

        let element = this.__document.createElementNS("http://www.w3.org/2000/svg", elementName);

        if (resetFill) {
            //if fill or stroke is not specified, the svg element should not display. By default SVG's fill is black.
            element.setAttribute("fill", "none");
            element.setAttribute("stroke", "none");
        }

        for (let key of Object.keys(properties)) {
            element.setAttribute(key, properties[key]);
        }

        return element
    };

    /**
     * Applies default canvas styles to the context
     * @private
     */
    __setDefaultStyles() {
        //default 2d canvas context properties see:http://www.w3.org/TR/2dcontext/
        var keys = Object.keys(STYLES), i, key;
        for (i = 0; i < keys.length; i++) {
            key = keys[i];
            this[key] = STYLES[key].canvas;
        }
    };

    /**
     * Applies styles on restore
     * @param styleState
     * @private
     */
    __applyStyleState(styleState) {
        var keys = Object.keys(styleState), i, key;
        for (i = 0; i < keys.length; i++) {
            key = keys[i];
            this[key] = styleState[key];
        }
    };

    /**
     * Gets the current style state
     * @return {Object}
     * @private
     */
    __getStyleState() {
        var i, styleState = {}, keys = Object.keys(STYLES), key;
        for (i = 0; i < keys.length; i++) {
            key = keys[i];
            styleState[key] = this[key];
        }
        return styleState
    };

    /**
     * Apples the current styles to the current SVG element. On "ctx.fill" or "ctx.stroke"
     * @param type
     * @private
     */
    __applyStyleToCurrentElement(type) {
        var currentElement = this.__currentElement;
        var currentStyleGroup = this.__currentElementsToStyle;
        if (currentStyleGroup) {
            currentElement.setAttribute(type, "");
            currentElement = currentStyleGroup.element;
            currentStyleGroup.children.forEach(function (node) {
                node.setAttribute(type, "");
            });
        }

        var keys = Object.keys(STYLES), i, style, value, id, regex, matches;
        for (i = 0; i < keys.length; i++) {
            style = STYLES[keys[i]];
            value = this[keys[i]];
            if (style.apply) {
                //is this a gradient or pattern?
                if (value instanceof CanvasPattern) {
                    //pattern
                    if (value.__ctx) {
                        //copy over defs
                        while (value.__ctx.__defs.childNodes.length) {
                            id = value.__ctx.__defs.childNodes[0].getAttribute("id");
                            this.__ids[id] = id;
                            this.__defs.appendChild(value.__ctx.__defs.childNodes[0]);
                        }
                    }
                    currentElement.setAttribute(style.apply, format("url(#{id})", {id: value.__root.getAttribute("id")}));
                } else if (value instanceof CanvasGradient) {
                    //gradient
                    currentElement.setAttribute(style.apply, format("url(#{id})", {id: value.__root.getAttribute("id")}));
                } else if (style && style.apply.indexOf(type) !== -1 && style.svg !== value) {
                    if ((style.svgAttr === "stroke" || style.svgAttr === "fill") && value && value.indexOf("rgba") !== -1) {
                        //separate alpha value, since illustrator can't handle it
                        regex = /rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d?\.?\d*)\s*\)/gi;
                        matches = regex.exec(value);
                        currentElement.setAttribute(style.svgAttr, format("rgb({r},{g},{b})", {
                            r: matches[1],
                            g: matches[2],
                            b: matches[3]
                        }));
                        //should take globalAlpha here
                        var opacity = matches[4];
                        var globalAlpha = this.globalAlpha;
                        if (globalAlpha != null) {
                            opacity *= globalAlpha;
                        }
                        currentElement.setAttribute(style.svgAttr + "-opacity", opacity);
                    } else {
                        var attr = style.svgAttr;
                        if (keys[i] === 'globalAlpha') {
                            attr = type + '-' + style.svgAttr;
                            if (currentElement.getAttribute(attr)) {
                                //fill-opacity or stroke-opacity has already been set by stroke or fill.
                                continue
                            }
                        }
                        //otherwise only update attribute if right type, and not svg default
                        currentElement.setAttribute(attr, value);
                    }
                }
            }
        }
    };

    /**
     * Will return the closest group or svg node. May return the current element.
     * @private
     */
    __closestGroupOrSvg(node) {
        node = node || this.__currentElement;
        if (node.nodeName === "g" || node.nodeName === "svg") {
            return node
        } else {
            return this.__closestGroupOrSvg(node.parentNode)
        }
    };

    /**
     * Returns the serialized value of the svg so far
     * @param fixNamedEntities - Standalone SVG doesn't support named entities, which document.createTextNode encodes.
     *                           If true, we attempt to find all named entities and encode it as a numeric entity.
     * @return serialized svg
     */
    getSerializedSvg(fixNamedEntities) {
        var serialized = new XMLSerializer().serializeToString(this.__root),
            keys, i, key, value, regexp;

        //IE search for a duplicate xmnls because they didn't implement setAttributeNS correctly
        // xmlns = /xmlns="http:\/\/www\.w3\.org\/2000\/svg".+xmlns="http:\/\/www\.w3\.org\/2000\/svg/gi;
        // if (xmlns.test(serialized)) {
        //     serialized = serialized.replace('xmlns="http://www.w3.org/2000/svg','xmlns:xlink="http://www.w3.org/1999/xlink');
        // }

        if (fixNamedEntities) {
            keys = Object.keys(namedEntities);
            //loop over each named entity and replace with the proper equivalent.
            for (i = 0; i < keys.length; i++) {
                key = keys[i];
                value = namedEntities[key];
                regexp = new RegExp(key, "gi");
                if (regexp.test(serialized)) {
                    serialized = serialized.replace(regexp, value);
                }
            }
        }

        return serialized
    };


    /**
     * Returns the root svg
     * @return
     */
    getSvg() {
        return this.__root
    };

    /**
     * Will generate a group tag.
     */
    saveWithTranslationAndClipRect(id, tx, ty, width, height, clipYOffset) {

        // clip rect
        const clip_id = `${id}_clip_rect`;
        let clipPath = this.__createElement('clipPath', {id: clip_id});

        this.__defs.appendChild(clipPath);

        const config =
            {
                x: '0',
                y: clipYOffset.toString(),
                width: width.toString(),
                height: height.toString()
            };

        clipPath.appendChild(this.__createElement('rect', config));

        const group = this.__createElement("g");
        group.setAttribute('transform', format('translate({x},{y})', {x: tx, y: ty}));
        group.setAttribute('clip-path', format('url(#{id})', {id: clip_id}));

        const parent = this.__closestGroupOrSvg();
        parent.appendChild(group);
        this.__groupStack.push(parent);

        this.__currentElement = group;
        this.__stack.push(this.__getStyleState());
    };

    save() {
        var group = this.__createElement("g");
        var parent = this.__closestGroupOrSvg();
        this.__groupStack.push(parent);
        parent.appendChild(group);
        this.__currentElement = group;
        this.__stack.push(this.__getStyleState());
    };

    /**
     * Sets current element to parent, or just root if already root
     */
    restore() {
        this.__currentElement = this.__groupStack.pop();
        this.__currentElementsToStyle = null;
        //Clearing canvas will make the poped group invalid, currentElement is set to the root group node.
        if (!this.__currentElement) {
            this.__currentElement = this.__root.childNodes[1];
        }
        var state = this.__stack.pop();
        this.__applyStyleState(state);
    };

    /**
     * Helper method to add transform
     * @private
     */
    __addTransform(t) {
        //if the current element has siblings, add another group
        var parent = this.__closestGroupOrSvg();
        if (parent.childNodes.length > 0) {
            if (this.__currentElement.nodeName === "path") {
                if (!this.__currentElementsToStyle) this.__currentElementsToStyle = {element: parent, children: []};
                this.__currentElementsToStyle.children.push(this.__currentElement);
                this.__applyCurrentDefaultPath();
            }

            var group = this.__createElement("g");
            parent.appendChild(group);
            this.__currentElement = group;
        }

        var transform = this.__currentElement.getAttribute("transform");
        if (transform) {
            transform += " ";
        } else {
            transform = "";
        }
        transform += t;
        this.__currentElement.setAttribute("transform", transform);
    };

    addTrackGroupWithTranslationAndClipRect(id, tx, ty, width, height, clipYOffset) {

        // clip rect
        const clip_id = id + '_clip_rect';
        let clipPath = this.__createElement('clipPath', {id: clip_id});

        this.__defs.appendChild(clipPath);
        clipPath.appendChild(this.__createElement('rect', {
            x: '0',
            y: clipYOffset.toString(),
            width: width.toString(),
            height: height.toString()
        }));

        let group = this.__createElement('g');
        this.__rootGroup.appendChild(group);

        group.setAttribute('transform', format('translate({x},{y})', {x: tx, y: ty}));
        group.setAttribute('id', (id + '_group'));

        // add clip rect
        group.setAttribute('clip-path', format('url(#{id})', {id: clip_id}));

        this.__currentElement = group;
    };

    /**
     *  scales the current element
     */
    scale(x, y) {
        if (y === undefined) {
            y = x;
        }
        this.__addTransform(format("scale({x},{y})", {x: x, y: y}));
    };

    /**
     * rotates the current element
     */
    rotate(angle) {
        var degrees = (angle * 180 / Math.PI);
        this.__addTransform(format("rotate({angle},{cx},{cy})", {angle: degrees, cx: 0, cy: 0}));
    };

    /**
     * translates the current element
     */
    translate(x, y) {
        this.__addTransform(format("translate({x},{y})", {x: x, y: y}));
    };

    /**
     * applies a transform to the current element
     */
    transform(a, b, c, d, e, f) {
        this.__addTransform(format("matrix({a},{b},{c},{d},{e},{f})", {a: a, b: b, c: c, d: d, e: e, f: f}));
    };

    /**
     * Create a new Path Element
     */
    beginPath() {
        var path, parent;

        // Note that there is only one current default path, it is not part of the drawing state.
        // See also: https://html.spec.whatwg.org/multipage/scripting.html#current-default-path
        this.__currentDefaultPath = "";
        this.__currentPosition = {};

        path = this.__createElement("path", {}, true);
        parent = this.__closestGroupOrSvg();
        parent.appendChild(path);
        this.__currentElement = path;
    };

    /**
     * Helper function to apply currentDefaultPath to current path element
     * @private
     */
    __applyCurrentDefaultPath() {
        var currentElement = this.__currentElement;
        if (currentElement.nodeName === "path") {
            currentElement.setAttribute("d", this.__currentDefaultPath);
        } else {
            console.error("Attempted to apply path command to node", currentElement.nodeName);
        }
    };

    /**
     * Helper function to add path command
     * @private
     */
    __addPathCommand(command) {
        this.__currentDefaultPath += " ";
        this.__currentDefaultPath += command;
    };

    /**
     * Adds the move command to the current path element,
     * if the currentPathElement is not empty create a new path element
     */
    moveTo(x, y) {
        if (this.__currentElement.nodeName !== "path") {
            this.beginPath();
        }

        // creates a new subpath with the given point
        this.__currentPosition = {x: x, y: y};
        this.__addPathCommand(format("M {x} {y}", {x: x, y: y}));
    };

    /**
     * Closes the current path
     */
    closePath() {
        if (this.__currentDefaultPath) {
            this.__addPathCommand("Z");
        }
    };

    /**
     * Adds a line to command
     */
    lineTo(x, y) {
        this.__currentPosition = {x: x, y: y};
        if (this.__currentDefaultPath && this.__currentDefaultPath.indexOf('M') > -1) {
            this.__addPathCommand(format("L {x} {y}", {x: x, y: y}));
        } else {
            this.__addPathCommand(format("M {x} {y}", {x: x, y: y}));
        }
    };

    /**
     * Add a bezier command
     */
    bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
        this.__currentPosition = {x: x, y: y};
        this.__addPathCommand(format("C {cp1x} {cp1y} {cp2x} {cp2y} {x} {y}",
            {cp1x: cp1x, cp1y: cp1y, cp2x: cp2x, cp2y: cp2y, x: x, y: y}));
    };

    /**
     * Adds a quadratic curve to command
     */
    quadraticCurveTo(cpx, cpy, x, y) {
        this.__currentPosition = {x: x, y: y};
        this.__addPathCommand(format("Q {cpx} {cpy} {x} {y}", {cpx: cpx, cpy: cpy, x: x, y: y}));
    };

    /**
     * Adds the arcTo to the current path
     *
     * @see http://www.w3.org/TR/2015/WD-2dcontext-20150514/#dom-context-2d-arcto
     */
    arcTo(x1, y1, x2, y2, radius) {
        // Let the point (x0, y0) be the last point in the subpath.
        var x0 = this.__currentPosition && this.__currentPosition.x;
        var y0 = this.__currentPosition && this.__currentPosition.y;

        // First ensure there is a subpath for (x1, y1).
        if (typeof x0 == "undefined" || typeof y0 == "undefined") {
            return
        }

        // Negative values for radius must cause the implementation to throw an IndexSizeError exception.
        if (radius < 0) {
            throw new Error("IndexSizeError: The radius provided (" + radius + ") is negative.")
        }

        // If the point (x0, y0) is equal to the point (x1, y1),
        // or if the point (x1, y1) is equal to the point (x2, y2),
        // or if the radius radius is zero,
        // then the method must add the point (x1, y1) to the subpath,
        // and connect that point to the previous point (x0, y0) by a straight line.
        if (((x0 === x1) && (y0 === y1))
            || ((x1 === x2) && (y1 === y2))
            || (radius === 0)) {
            this.lineTo(x1, y1);
            return
        }

        // Otherwise, if the points (x0, y0), (x1, y1), and (x2, y2) all lie on a single straight line,
        // then the method must add the point (x1, y1) to the subpath,
        // and connect that point to the previous point (x0, y0) by a straight line.
        var unit_vec_p1_p0 = normalize([x0 - x1, y0 - y1]);
        var unit_vec_p1_p2 = normalize([x2 - x1, y2 - y1]);
        if (unit_vec_p1_p0[0] * unit_vec_p1_p2[1] === unit_vec_p1_p0[1] * unit_vec_p1_p2[0]) {
            this.lineTo(x1, y1);
            return
        }

        // Otherwise, let The Arc be the shortest arc given by circumference of the circle that has radius radius,
        // and that has one point tangent to the half-infinite line that crosses the point (x0, y0) and ends at the point (x1, y1),
        // and that has a different point tangent to the half-infinite line that ends at the point (x1, y1), and crosses the point (x2, y2).
        // The points at which this circle touches these two lines are called the start and end tangent points respectively.

        // note that both vectors are unit vectors, so the length is 1
        var cos = (unit_vec_p1_p0[0] * unit_vec_p1_p2[0] + unit_vec_p1_p0[1] * unit_vec_p1_p2[1]);
        var theta = Math.acos(Math.abs(cos));

        // Calculate origin
        var unit_vec_p1_origin = normalize([
            unit_vec_p1_p0[0] + unit_vec_p1_p2[0],
            unit_vec_p1_p0[1] + unit_vec_p1_p2[1]
        ]);
        var len_p1_origin = radius / Math.sin(theta / 2);
        var x = x1 + len_p1_origin * unit_vec_p1_origin[0];
        var y = y1 + len_p1_origin * unit_vec_p1_origin[1];

        // Calculate start angle and end angle
        // rotate 90deg clockwise (note that y axis points to its down)
        var unit_vec_origin_start_tangent = [
            -unit_vec_p1_p0[1],
            unit_vec_p1_p0[0]
        ];
        // rotate 90deg counter clockwise (note that y axis points to its down)
        var unit_vec_origin_end_tangent = [
            unit_vec_p1_p2[1],
            -unit_vec_p1_p2[0]
        ];
        var getAngle = function (vector) {
            // get angle (clockwise) between vector and (1, 0)
            var x = vector[0];
            var y = vector[1];
            if (y >= 0) { // note that y axis points to its down
                return Math.acos(x)
            } else {
                return -Math.acos(x)
            }
        };
        var startAngle = getAngle(unit_vec_origin_start_tangent);
        var endAngle = getAngle(unit_vec_origin_end_tangent);

        // Connect the point (x0, y0) to the start tangent point by a straight line
        this.lineTo(x + unit_vec_origin_start_tangent[0] * radius,
            y + unit_vec_origin_start_tangent[1] * radius);

        // Connect the start tangent point to the end tangent point by arc
        // and adding the end tangent point to the subpath.
        this.arc(x, y, radius, startAngle, endAngle);
    };

    /**
     * Sets the stroke property on the current element
     */
    stroke() {
        if (this.__currentElement.nodeName === "path") {
            this.__currentElement.setAttribute("paint-order", "fill stroke markers");
        }
        this.__applyCurrentDefaultPath();
        this.__applyStyleToCurrentElement("stroke");
    };

    /**
     * Sets fill properties on the current element
     */
    fill() {
        if (this.__currentElement.nodeName === "path") {
            this.__currentElement.setAttribute("paint-order", "stroke fill markers");
        }
        this.__applyCurrentDefaultPath();
        this.__applyStyleToCurrentElement("fill");
    };

    /**
     *  Adds a rectangle to the path.
     */
    rect(x, y, width, height) {
        if (this.__currentElement.nodeName !== "path") {
            this.beginPath();
        }
        this.moveTo(x, y);
        this.lineTo(x + width, y);
        this.lineTo(x + width, y + height);
        this.lineTo(x, y + height);
        this.lineTo(x, y);
        this.closePath();
    };


    /**
     * adds a rectangle element
     */
    fillRect(x, y, width, height) {

        if (height < 0) {
            y += height;
            height = -height;
        }
        if (width < 0) {
            x += width;
            width = -width;
        }
        // See if rect intersects current viewbox
        var r2 = {
            x: x,
            y: y,
            width: width,
            height: height
        };

        if (this.viewbox) {
            if (!intersectRect(this.viewbox, r2)) {
                return
            }
        }

        var rect, parent;
        rect = this.__createElement("rect", r2, true);
        parent = this.__closestGroupOrSvg();
        parent.appendChild(rect);
        this.__currentElement = rect;
        this.__applyStyleToCurrentElement("fill");
    };

    /**
     * Draws a rectangle with no fill
     * @param x
     * @param y
     * @param width
     * @param height
     */
    strokeRect(x, y, width, height) {
        var rect, parent;
        rect = this.__createElement("rect", {
            x: x,
            y: y,
            width: width,
            height: height
        }, true);
        parent = this.__closestGroupOrSvg();
        parent.appendChild(rect);
        this.__currentElement = rect;
        this.__applyStyleToCurrentElement("stroke");
    };

    // stroke ellipse
    strokeEllipse(cx, cy, rx, ry, rotation, startAngle, endAngle, isCCW) {
        this.__ellipse(cx, cy, rx, ry, rotation, startAngle, endAngle, isCCW, 'stroke');
    }

    // fill ellipse
    fillEllipse(cx, cy, rx, ry, rotation, startAngle, endAngle, isCCW) {
        this.__ellipse(cx, cy, rx, ry, rotation, startAngle, endAngle, isCCW, 'fill');
    }

    // ellipse helper
    __ellipse(cx, cy, rx, ry, rotation, startAngle, endAngle, isCCW, style) {

        const config =
            {
                cx,
                cy,
                rx,
                ry
            };
        const element = this.__createElement('ellipse', config, true);
        const parent = this.__closestGroupOrSvg();
        parent.appendChild(element);
        this.__currentElement = element;
        this.__applyStyleToCurrentElement(style);
    }

    /**
     * Clear entire canvas:
     * 1. save current transforms
     * 2. remove all the childNodes of the root g element
     */
    __clearCanvas() {
        var current = this.__closestGroupOrSvg(),
            transform = current.getAttribute("transform");
        var rootGroup = this.__root.childNodes[1];
        var childNodes = rootGroup.childNodes;
        for (var i = childNodes.length - 1; i >= 0; i--) {
            if (childNodes[i]) {
                rootGroup.removeChild(childNodes[i]);
            }
        }
        this.__currentElement = rootGroup;
        //reset __groupStack as all the child group nodes are all removed.
        this.__groupStack = [];
        if (transform) {
            this.__addTransform(transform);
        }
    };

    /**
     * "Clears" a canvas by just drawing a white rectangle in the current group.
     */
    clearRect(x, y, width, height) {
        //clear entire canvas
        if (x === 0 && y === 0 && width === this.width && height === this.height) {
            this.__clearCanvas();
            return
        }
        var rect, parent = this.__closestGroupOrSvg();
        rect = this.__createElement("rect", {
            x: x,
            y: y,
            width: width,
            height: height,
            fill: "#FFFFFF"
        }, true);
        parent.appendChild(rect);
    };

    /**
     * Adds a linear gradient to a defs tag.
     * Returns a canvas gradient object that has a reference to it's parent def
     */
    createLinearGradient(x1, y1, x2, y2) {
        var grad = this.__createElement("linearGradient", {
            id: randomString(this.__ids),
            x1: x1 + "px",
            x2: x2 + "px",
            y1: y1 + "px",
            y2: y2 + "px",
            "gradientUnits": "userSpaceOnUse"
        }, false);
        this.__defs.appendChild(grad);
        return new CanvasGradient(grad, this)
    };

    /**
     * Adds a radial gradient to a defs tag.
     * Returns a canvas gradient object that has a reference to it's parent def
     */
    createRadialGradient(x0, y0, r0, x1, y1, r1) {
        var grad = this.__createElement("radialGradient", {
            id: randomString(this.__ids),
            cx: x1 + "px",
            cy: y1 + "px",
            r: r1 + "px",
            fx: x0 + "px",
            fy: y0 + "px",
            "gradientUnits": "userSpaceOnUse"
        }, false);
        this.__defs.appendChild(grad);
        return new CanvasGradient(grad, this)

    };

    /**
     * Parses the font string and returns svg mapping
     * @private
     */
    __parseFont() {
        var regex = /^\s*(?=(?:(?:[-a-z]+\s*){0,2}(italic|oblique))?)(?=(?:(?:[-a-z]+\s*){0,2}(small-caps))?)(?=(?:(?:[-a-z]+\s*){0,2}(bold(?:er)?|lighter|[1-9]00))?)(?:(?:normal|\1|\2|\3)\s*){0,3}((?:xx?-)?(?:small|large)|medium|smaller|larger|[.\d]+(?:\%|in|[cem]m|ex|p[ctx]))(?:\s*\/\s*(normal|[.\d]+(?:\%|in|[cem]m|ex|p[ctx])))?\s*([-,\'\"\sa-z0-9]+?)\s*$/i;
        var fontPart = regex.exec(this.font);
        var data = {
            style: fontPart[1] || 'normal',
            size: fontPart[4] || '10px',
            family: fontPart[6] || 'sans-serif',
            weight: fontPart[3] || 'normal',
            decoration: fontPart[2] || 'normal',
            href: null
        };

        //canvas doesn't support underline natively, but we can pass this attribute
        if (this.__fontUnderline === "underline") {
            data.decoration = "underline";
        }

        //canvas also doesn't support linking, but we can pass this as well
        if (this.__fontHref) {
            data.href = this.__fontHref;
        }

        return data
    };

    /**
     * Helper to link text fragments
     * @param font
     * @param element
     * @return {*}
     * @private
     */
    __wrapTextLink(font, element) {
        if (font.href) {
            var a = this.__createElement("a");
            a.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", font.href);
            a.appendChild(element);
            return a
        }
        return element
    };

    /**
     * Fills or strokes text
     * @param text
     * @param x
     * @param y
     * @param action - stroke or fill
     * @private
     */
    __applyText(text, x, y, action) {
        var font = this.__parseFont(),
            parent = this.__closestGroupOrSvg(),
            textElement = this.__createElement("text", {
                "font-family": font.family,
                "font-size": font.size,
                "font-style": font.style,
                "font-weight": font.weight,
                "text-decoration": font.decoration,
                "x": x,
                "y": y,
                "text-anchor": getTextAnchor(this.textAlign),
                "dominant-baseline": getDominantBaseline(this.textBaseline)
            }, true);

        textElement.appendChild(this.__document.createTextNode(text));
        this.__currentElement = textElement;
        this.__applyStyleToCurrentElement(action);
        parent.appendChild(this.__wrapTextLink(font, textElement));
    };

    /**
     * Creates a text element
     * @param text
     * @param x
     * @param y
     */
    fillText(text, x, y) {
        this.__applyText(text, x, y, "fill");
    };

    /**
     * Strokes text
     * @param text
     * @param x
     * @param y
     */
    strokeText(text, x, y) {
        this.__applyText(text, x, y, "stroke");
    };

    /**
     * No need to implement this for svg.
     * @param text
     * @return {TextMetrics}
     */
    measureText(text) {
        this.__ctx.font = this.font;
        return this.__ctx.measureText(text)
    };

    /**
     *  Arc command!
     */
    arc(x, y, radius, startAngle, endAngle, counterClockwise) {
        // in canvas no circle is drawn if no angle is provided.
        if (startAngle === endAngle) {
            return
        }
        startAngle = startAngle % (2 * Math.PI);
        endAngle = endAngle % (2 * Math.PI);
        if (startAngle === endAngle) {
            //circle time! subtract some of the angle so svg is happy (svg elliptical arc can't draw a full circle)
            endAngle = ((endAngle + (2 * Math.PI)) - 0.001 * (counterClockwise ? -1 : 1)) % (2 * Math.PI);
        }
        var endX = x + radius * Math.cos(endAngle),
            endY = y + radius * Math.sin(endAngle),
            startX = x + radius * Math.cos(startAngle),
            startY = y + radius * Math.sin(startAngle),
            sweepFlag = counterClockwise ? 0 : 1,
            largeArcFlag = 0,
            diff = endAngle - startAngle;

        // https://github.com/gliffy/canvas2svg/issues/4
        if (diff < 0) {
            diff += 2 * Math.PI;
        }

        if (counterClockwise) {
            largeArcFlag = diff > Math.PI ? 0 : 1;
        } else {
            largeArcFlag = diff > Math.PI ? 1 : 0;
        }

        this.lineTo(startX, startY);
        this.__addPathCommand(format("A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}",
            {
                rx: radius,
                ry: radius,
                xAxisRotation: 0,
                largeArcFlag: largeArcFlag,
                sweepFlag: sweepFlag,
                endX: endX,
                endY: endY
            }));

        this.__currentPosition = {x: endX, y: endY};
    };

    /**
     * The ellipse() method creates an elliptical arc centered at (x, y) with the radii radiusX and radiusY. The path
     * starts at startAngle and ends at endAngle, and travels in the direction given by counterclockwise (defaulting to clockwise).
     */
// ellipse (x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise) {
//     // TODO -- implement
// }

    /**
     * Generates a ClipPath from the clip command.
     */
    clip() {
        var group = this.__closestGroupOrSvg(),
            clipPath = this.__createElement("clipPath"),
            id = randomString(this.__ids),
            newGroup = this.__createElement("g");

        this.__applyCurrentDefaultPath();
        group.removeChild(this.__currentElement);
        clipPath.setAttribute("id", id);
        clipPath.appendChild(this.__currentElement);

        this.__defs.appendChild(clipPath);

        //set the clip path to this group
        group.setAttribute("clip-path", format("url(#{id})", {id: id}));

        //clip paths can be scaled and transformed, we need to add another wrapper group to avoid later transformations
        // to this path
        group.appendChild(newGroup);

        this.__currentElement = newGroup;

    };

    /**
     * Draws a canvas, image or mock context to this canvas.
     * Note that all svg dom manipulation uses node.childNodes rather than node.children for IE support.
     * http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-drawimage
     */
    drawImage() {
        //convert arguments to a real array
        var args = Array.prototype.slice.call(arguments),
            image = args[0],
            dx, dy, dw, dh, sx = 0, sy = 0, sw, sh, parent, svg, defs, group,
            svgImage, canvas, context, id;

        if (args.length === 3) {
            dx = args[1];
            dy = args[2];
            sw = image.width;
            sh = image.height;
            dw = sw;
            dh = sh;
        } else if (args.length === 5) {
            dx = args[1];
            dy = args[2];
            dw = args[3];
            dh = args[4];
            sw = image.width;
            sh = image.height;
        } else if (args.length === 9) {
            sx = args[1];
            sy = args[2];
            sw = args[3];
            sh = args[4];
            dx = args[5];
            dy = args[6];
            dw = args[7];
            dh = args[8];
        } else {
            throw new Error("Invalid number of arguments passed to drawImage: " + arguments.length)
        }

        parent = this.__closestGroupOrSvg();
        this.__currentElement;
        var translateDirective = "translate(" + dx + ", " + dy + ")";
        if (image instanceof ctx) {
            //canvas2svg mock canvas context. In the future we may want to clone nodes instead.
            //also I'm currently ignoring dw, dh, sw, sh, sx, sy for a mock context.
            svg = image.getSvg().cloneNode(true);
            if (svg.childNodes && svg.childNodes.length > 1) {
                defs = svg.childNodes[0];
                while (defs.childNodes.length) {
                    id = defs.childNodes[0].getAttribute("id");
                    this.__ids[id] = id;
                    this.__defs.appendChild(defs.childNodes[0]);
                }
                group = svg.childNodes[1];
                if (group) {
                    //save original transform
                    var originTransform = group.getAttribute("transform");
                    var transformDirective;
                    if (originTransform) {
                        transformDirective = originTransform + " " + translateDirective;
                    } else {
                        transformDirective = translateDirective;
                    }
                    group.setAttribute("transform", transformDirective);
                    parent.appendChild(group);
                }
            }
        } else if (image.nodeName === "CANVAS" || image.nodeName === "IMG") {
            //canvas or image
            svgImage = this.__createElement("image");
            svgImage.setAttribute("width", dw);
            svgImage.setAttribute("height", dh);
            svgImage.setAttribute("preserveAspectRatio", "none");

            if (sx || sy || sw !== image.width || sh !== image.height) {
                //crop the image using a temporary canvas
                canvas = this.__document.createElement("canvas");
                canvas.width = dw;
                canvas.height = dh;
                context = canvas.getContext("2d");
                context.drawImage(image, sx, sy, sw, sh, 0, 0, dw, dh);
                image = canvas;
            }
            svgImage.setAttribute("transform", translateDirective);
            svgImage.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href",
                image.nodeName === "CANVAS" ? image.toDataURL() : image.getAttribute("src"));
            parent.appendChild(svgImage);
        }
    };

    /**
     * Generates a pattern tag
     */
    createPattern(image, repetition) {

        let pattern = this.__document.__createElement("pattern");
        let id = randomString(this.__ids);
        let img;

        pattern.setAttribute("id", id);
        pattern.setAttribute("width", image.width);
        pattern.setAttribute("height", image.height);
        if (image.nodeName === "CANVAS" || image.nodeName === "IMG") {
            img = this.__createElement("image");
            img.setAttribute("width", image.width);
            img.setAttribute("height", image.height);
            img.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href",
                image.nodeName === "CANVAS" ? image.toDataURL() : image.getAttribute("src"));
            pattern.appendChild(img);
            this.__defs.appendChild(pattern);
        } else if (image instanceof ctx) {
            pattern.appendChild(image.__root.childNodes[1]);
            this.__defs.appendChild(pattern);
        }
        return new CanvasPattern(pattern, this)
    };

    setLineDash(dashArray) {
        if (dashArray && dashArray.length > 0) {
            this.lineDash = dashArray.join(",");
        } else {
            this.lineDash = null;
        }
    };

    /**
     * Not yet implemented
     */
    drawFocusRing() {
    };

    createImageData() {
    };

    getImageData() {
    };

    putImageData() {
    };

    globalCompositeOperation() {
    };

    setTransform() {
    };
}

function convertToHubURL(accension) {
    //https://hgdownload.soe.ucsc.edu/hubs/GCF/016/808/095/GCF_016808095.1/
    //https://hgdownload.soe.ucsc.edu/hubs/GCA/028/534/965/GCA_028534965.1/
    if (accension.startsWith("GCF") || accension.startsWith("GCA") && accension.length >= 13) {
        const prefix = accension.substring(0, 3);
        const n1 = accension.substring(4, 7);
        const n2 = accension.substring(7, 10);
        const n3 = accension.substring(10, 13);
        return "https://hgdownload.soe.ucsc.edu/hubs/" + prefix + "/" + n1 + "/" + n2 + "/" + n3 + "/" + accension + "/hub.txt";
    } else {
        return undefined;
    }
}

const parentOverrideProperties = new Set(["visibility", "priority", "group"]);

const nonInheritableProperties = new Set([
    "track", "type", "shortLabel", "longLabel", "bigDataUrl",
    "parent", "superTrack", "priority", "view", "compositeContainer", "compositeTrack"
]);


class Stanza {

    properties = new Map()

    constructor(type, name) {
        this.type = type;
        this.name = name;
    }

    setProperty(key, value) {
        this.properties.set(key, value);
    }

    getProperty(key) {
        if (this.properties.has("noInherit")) {
            return this.properties.get(key)
        } else if (this.parent && parentOverrideProperties.has(key) && this.parent.hasProperty(key)) {
            return this.parent.getProperty(key)
        } else if (this.properties.has(key)) {
            return this.properties.get(key)
        } else if (this.parent && !nonInheritableProperties.has(key)) {
            return this.parent.getProperty(key)
        } else {
            return undefined
        }
    }

    hasProperty(key) {
        return this.getProperty(key) !== null && this.getProperty(key) !== undefined
    }

    hasOwnProperty(key) {
        return this.properties.has(key)
    }

    getOwnProperty(key) {
        return this.properties.get(key)
    }

    removeProperty(key) {
        this.properties.delete(key);
    }

    get format() {
        const type = this.getProperty("type");
        if (type) {
            // Trim extra bed qualifiers (e.g. bigBed + 4)
            return firstWord$1(type)
        }
        return undefined // unknown type
    }

    /**
     * IGV display mode
     */
    get displayMode() {
        let viz = this.getProperty("visibility");
        if (!viz) {
            return "COLLAPSED"
        } else {
            viz = viz.toLowerCase();
            switch (viz) {
                case "dense":
                    return "COLLAPSED"
                case "pack":
                    return "EXPANDED"
                case "squish":
                    return "SQUISHED"
                default:
                    return "COLLAPSED"
            }
        }
    }
}


function firstWord$1(str) {
    const idx = str.indexOf(' ');
    return idx > 0 ? str.substring(0, idx) : str
}

class TrackConfigContainer {
        constructor(name, label, priority, defaultOpen) {
            this.name = name;
            this.priority = priority;
            this.label = label;
            this.defaultOpen = defaultOpen;
            this.tracks = [];
            this.children = [];
        }

        isEmpty() {
            return this.tracks.length === 0 &&
                (!this.children || this.children.length === 0 || this.children.every(child => child.isEmpty()));
        }

        map(callback) {
            this.tracks.forEach(callback);
            this.children.forEach(child => child.map(callback));
        }

        findTracks(filter) {
            const found = [];
            this._find(found, filter);
            return found;
        }

        _find(found, filter) {
            this.tracks.forEach(track => {
                if (filter(track)) {
                    found.push(track);
                }
            });
            this.children.forEach(child => child._find(found, filter));
        }

        countTracks() {
            return this.tracks.length + this.children.reduce((count, child) => count + child.countTracks(), 0);
        }

        countSelectedTracks() {
            const selectedCount = this.tracks.filter(track => track.visible).length;
            return selectedCount + this.children.reduce((count, child) => count + child.countSelectedTracks(), 0);
        }

        trim() {
            this.children = this.children.filter(child => !child.isEmpty());
            this.children.forEach(child => child.trim());
        }

        setTrackVisibility(loadedTrackPaths) {
            this.tracks.forEach(track => {
                track.visible = loadedTrackPaths.has(track.url);
            });
            this.children.forEach(child => child.setTrackVisibility(loadedTrackPaths));
        }
    }

const supportedTypes = new Set([
    "bigbed", "bigwig", "biggenepred", "vcftabix", "refgene",
    "bam", "sampleinfo", "vcf.list", "ucscsnp", "bed", "tdf", "gff", "gff3", "gtf", "vcf", "vcfphasedtrio",
    "bigdbsnp", "rmask", "genepred", "wig", "bedgraph", "interact", "broadpeak", "narrowpeak", "gappedpeak",
    "gistic", "seg", "mut, bigrmsk"
]);

const filterTracks = new Set(["cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps",
    "cpgIslandExtUnmasked", "windowMasker"]);

class TrackDbHub {

    constructor(trackStanzas, groupStanzas) {
        this.groupStanzas = groupStanzas;
        this.trackStanzas = trackStanzas;
    }


    getSupportedTrackCount() {
        let count = 0;
        for (const t of this.trackStanzas) {
            if (!filterTracks.has(t.name) &&
                t.hasProperty("bigDataUrl") &&
                t.format &&
                supportedTypes.has(t.format.toLowerCase())) {
                count++;
            }
        }
        return count
    }

    getGroupedTrackConfigurations() {

        if (!this.groupTrackConfigs) {
            this.groupTrackConfigs = [];
            const trackContainers = new Map();

            // create a container for tracks with no parent
            const nullContainer = new TrackConfigContainer('', '', 0, true);
            this.groupTrackConfigs.push(nullContainer);

            const hasGroups = this.groupStanzas && this.groupStanzas.length > 0;
            if (hasGroups) {
                for (const groupStanza of this.groupStanzas) {
                    const name = groupStanza.getProperty("name");
                    const defaultOpen = groupStanza.getProperty("defaultIsClosed") === "0";
                    const priority = groupStanza.hasProperty("priority") ? getPriority(groupStanza.getProperty("priority")) : Number.MAX_SAFE_INTEGER - 1;
                    const container = new TrackConfigContainer(name, groupStanza.getProperty("label"), priority, defaultOpen);
                    trackContainers.set(name, container);
                    this.groupTrackConfigs.push(container);
                }
            }

            for (let s of this.trackStanzas) {

                const isContainer = (s.hasOwnProperty("superTrack") && !s.hasOwnProperty("bigDataUrl")) ||
                    s.hasOwnProperty("compositeTrack") || s.hasOwnProperty("view") ||
                    (s.hasOwnProperty("container") && s.getOwnProperty("container").equals("multiWig"));

                // Find parent, if any. "group" containers can be implicit, all other types should be explicitly
                // defined before their children
                let parent;

                if (s.hasOwnProperty("parent")) {
                    parent = trackContainers.get(s.getOwnProperty("parent"));
                }

                if (!parent && hasGroups && s.hasProperty("group")) {
                    const groupName = s.getProperty("group");
                    if (trackContainers.has(groupName)) {
                        parent = trackContainers.get(groupName);
                    } else {
                        const container = new TrackConfigContainer(groupName, groupName, 1000, true);
                        trackContainers.set(groupName, container);
                        this.groupTrackConfigs.push(container);
                        parent = container;
                    }
                }

                if (isContainer) {

                    const name = s.getProperty("track");
                    const priority = s.hasProperty("priority") ? getPriority(s.getProperty("priority")) : Number.MAX_SAFE_INTEGER - 1;
                    const defaultOpen = s.getProperty("defaultIsClosed") === "0";
                    const longLabel = s.getOwnProperty("longLabel");
                    const label = longLabel && longLabel.length < 50 ? longLabel : s.getOwnProperty("shortLabel");
                    const container = new TrackConfigContainer(name, label, priority, defaultOpen);

                    if (trackContainers.has(name)) {
                        throw new Error(`Duplicate track container: ${name}`)
                    }
                    trackContainers.set(name, container);

                    if (parent) {
                        parent.children.push(container);
                    } else {
                        // No parent or a superTrack => promote to top level
                        this.groupTrackConfigs.push(container);
                    }
                } else if (!filterTracks.has(s.name) &&
                    s.hasProperty("bigDataUrl") &&
                    s.format &&
                    supportedTypes.has(s.format.toLowerCase())) {

                    const trackConfig = this.#getTrackConfig(s);
                    if (parent) {
                        parent.tracks.push(trackConfig);
                    } else {
                        nullContainer.tracks.push(trackConfig);
                    }
                }
            }

        }

        // Filter empty groups and sort
        this.groupTrackConfigs.forEach(c => c.trim());
        this.groupTrackConfigs = this.groupTrackConfigs.filter(t => !t.isEmpty());

        this.groupTrackConfigs.sort((a, b) => a.priority - b.priority);
        return this.groupTrackConfigs
    }

    /**
     * Return an array of igv track config objects that satisfy the filter
     */
    #getTracksConfigs(filter) {
        return this.trackStanzas.filter(t => {
            return supportedTypes.has(t.format) && t.hasProperty("bigDataUrl") && (!filter || filter(t))
        })
            .map(t => this.#getTrackConfig(t))
    }


    /** example
     * track gc5Base
     * shortLabel GC Percent
     * longLabel GC Percent in 5-Base Windows
     * group map
     * visibility full
     * autoScale Off
     * maxHeightPixels 128:36:16
     * graphTypeDefault Bar
     * gridDefault OFF
     * windowingFunction Mean
     * color 0,0,0
     * altColor 128,128,128
     * viewLimits 30:70
     * type bigWig 0 100
     * bigDataUrl bbi/GCA_011100615.1_Macaca_fascicularis_6.0.gc5Base.bw
     * html html/GCA_011100615.1_Macaca_fascicularis_6.0.gc5Base
     * @param t
     */
    #getTrackConfig(t) {

        const format = t.format;

        const config = {
            "id": t.getProperty("track"),
            "name": t.getProperty("shortLabel"),
            "format": format,
            "url": t.getProperty("bigDataUrl"),
            "displayMode": t.displayMode,
        };

        if ("vcfTabix" === format) {
            config.indexURL = config.url + ".tbi";
        }

        if (t.hasProperty("longLabel") && t.hasProperty("html")) {
            if (config.description) config.description += "<br/>";
            config.description =
                `<a target="_blank" href="${t.getProperty("html")}">${t.getProperty("longLabel")}</a>`;
        } else if (t.hasProperty("longLabel")) {
            config.description = t.getProperty("longLabel");
        }

        if (t.hasProperty("autoScale")) {
            config.autoscale = t.getProperty("autoScale").toLowerCase() === "on";
        }
        if (t.hasProperty("maxHeightPixels")) {
            const tokens = t.getProperty("maxHeightPixels").split(":");
            config.maxHeight = Number.parseInt(tokens[0]);
            config.height = Number.parseInt(tokens[1]);
            config.minHeight = Number.parseInt(tokens[2]);
        }
        // TODO -- graphTypeDefault
        // TODO -- windowingFunction
        if (t.hasProperty("color")) {
            const c = t.getProperty("color");
            config.color = c.indexOf(",") > 0 ? `rgb(${c})` : c;
        }
        if (t.hasProperty("altColor")) {
            const c = t.getProperty("altColor");
            config.altColor = c.indexOf(",") > 0 ? `rgb(${c})` : c;
        }
        if (t.hasProperty("viewLimits")) {
            const tokens = t.getProperty("viewLimits").split(":");
            let min, max;
            if (tokens.length > 1) {
                min = Number.parseInt(tokens[0]);
                max = Number.parseInt(tokens[1]);
            }
            if (Number.isNaN(max) || Number.isNaN(min)) {
                console.warn(`Unexpected viewLimits value in track line: ${properties["viewLimits"]}`);
            } else {
                config.min = min;
                config.max = max;
            }

        }
        if (t.hasProperty("itemRgb")) ;
        if ("hide" === t.getProperty("visibility")) {
            // TODO -- this not supported yet
            config.visible = false;
        }
        if (t.hasProperty("url")) {
            config.infoURL = t.getProperty("url");
        }
        if (t.hasProperty("searchIndex")) {
            config.searchIndex = t.getProperty("searchIndex");
        }
        if (t.hasProperty("searchTrix")) {
            config.trixURL = t.getProperty("searchTrix");
        }
        if (t.hasProperty("html")) {
            config.html = t.getProperty("html");
        }

        if (t.hasProperty("group")) {
            config._group = t.getProperty("group");
            if (this.groupPriorityMap && this.groupPriorityMap.has(config._group)) {
                const nextPriority = this.groupPriorityMap.get(config._group) + 1;
                config.order = nextPriority;
                this.groupPriorityMap.set(config._group, nextPriority);
            }
        }

        if (t.hasProperty("metadata")) {
            config.attributes = parseMetadata(t.getProperty("metadata"));
        }

        if (t.hasProperty("maxWindowToDraw")) {
            let maxWindowToDraw = parseInt(t.getProperty("maxWindowToDraw"), 10);
            if (maxWindowToDraw > Number.MAX_SAFE_INTEGER) {
                maxWindowToDraw = Number.MAX_SAFE_INTEGER;
            }
            config.visibilityWindow = maxWindowToDraw;
        }

        // IGV does not support "maxWindowCoverage" in the same way as UCSC. Use to limit visibility window
        if (t.hasProperty("maxWindowCoverage")) {
            let maxWindowToDraw = parseInt(t.getProperty("maxWindowCoverage"), 10);
            if (maxWindowToDraw > Number.MAX_SAFE_INTEGER) {
                maxWindowToDraw = Number.MAX_SAFE_INTEGER;
            }
            config.visibilityWindow = maxWindowToDraw;
        }

        return config
    }
}

function htmlText(html) {
    // Assumes a pattern like <span style="color:#C58DAA">Digestive</span>
    const idx1 = html.indexOf('>');
    const idx2 = html.indexOf('<', idx1);
    if (idx1 > 0 && idx2 > idx1) {
        return html.substring(idx1 + 1, idx2)
    } else {
        return html
    }
}

/**
 * Return the priority for the group. The priority format is uncertain, but extends to at least 2 levels (e.g. 3.4).
 * Ignore levels > 3
 *
 * @param {string} priorityString Priority as a string (e.g. 3.4)
 * @return {number} A priority as an integer
 */
function getPriority(priorityString) {
    try {
        const tokens = priorityString.trim().split(".");
        let p = parseInt(tokens[0], 10) * 100;
        if (tokens.length > 1) {
            p += parseInt(tokens[1], 10) * 10;
        }
        if (tokens.length > 2) {
            p += parseInt(tokens[2], 10);
        }
        return p
    } catch (e) {
        console.error(`Error parsing priority string: ${priorityString}`, e);
        return Number.MAX_SAFE_INTEGER
    }
}

function parseMetadata(metadata) {
    const attrs = new Map();
    let lastMetdataLengh = -1;
    while (metadata && metadata.length > 0) {
        try {
            if (metadata.length === lastMetdataLengh) {
                break
            }
            lastMetdataLengh = metadata.length;
            let idx = metadata.indexOf("=");
            if (idx === -1 || idx === metadata.length - 1) {
                break
            }
            let idx2;
            const key = capitalize(stripQuotes$2(metadata.substring(0, idx)));
            let value;

            if (metadata.charAt(idx + 1) === '"') {
                idx++;
                idx2 = metadata.indexOf('" ', idx + 1);
                value = idx2 > 0 ? metadata.substring(idx + 1, idx2) : metadata.substring(idx + 1);
                idx2++;
            } else {
                idx2 = metadata.indexOf(" ", idx + 1);
                if (idx2 === -1) {
                    idx2 = metadata.length;
                }
                value = metadata.substring(idx + 1, idx2);
            }
            value = stripQuotes$2(value);
            if (value.endsWith('"')) {
                value = value.substring(0, value.length - 1);
            }
            if (value.startsWith("<") && value.endsWith(">")) {
                value = htmlText(value);
            }
            attrs.set(key, value);
            if (idx2 === metadata.length) {
                break
            }
            metadata = idx2 > 0 ? metadata.substring(idx2 + 1).trim() : "";
        } catch (e) {
            // We don't want to fail parsing the hub due to a failure parsing metadata. Also, we don't want to
            // overwhelm the log. Metadata is of marginal importance in IGV.
        }
    }
    return attrs
}

/*
 https://genomewiki.ucsc.edu/index.php/Assembly_Hubs
 https://genome.ucsc.edu/goldenpath/help/hgTrackHubHelp.html
 https://genome.ucsc.edu/goldenPath/help/hgTrackHubHelp
 https://genome.ucsc.edu/goldenpath/help/trackDb/trackDbHub.html
 */

const idMappings = new Map([
    ["hg38", "GCF_000001405.40"],
    ["hg38_1kg", "GCF_000001405.40"],
    ["mm39", "GCF_000001635.27"],
    ["mm10", "GCF_000001635.26"],
    ["bosTau9", "GCF_002263795.1"],
    ["canFam4", "GCF_011100685.1"],
    ["canFam6", "GCF_000002285.5"],
    ["ce11", "GCF_000002985.6"],
    ["dm6", "GCF_000001215.4"],
    ["galGal6", "GCF_000002315.6"],
    ["gorGor6", "GCF_008122165.1"],
    ["macFas5", "GCA_000364345.1"],
    ["panTro6", "GCA_002880755.3"],
    ["rn6", "GCF_000001895.5"],
    ["rn7", "GCF_015227675.2"],
    ["sacCer3", "GCF_000146045.2"],
    ["sacCer2", "GCF_000146045.2"],
    ["susScr11", "GCF_000003025.6"],
    ["taeGut1", "GCF_000002275.3"],
    ["tetNig2", "GCF_000002275.3"],
    ["xenTro10", "GCF_000002035.6"],
    ["xenTro9", "GCF_000002035.6"],
    ["tair10", "GCF_000001735.4"],
]);

class Hub {

    static supportedTypes = new Set(["bigBed", "bigWig", "bigGenePred", "vcfTabix"])
    static filterTracks = new Set(["cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps",
        "cpgIslandExtUnmasked", "windowMasker"])

    constructor(url, hubStanza, genomeStanzas, trackStanzas, groupStanzas) {

        this.url = url;
        this.hubStanza = hubStanza;
        this.genomeStanzas = genomeStanzas;
        this.trackStanzas = trackStanzas;
        this.groupStanzas = groupStanzas;
        this.cytobandStanza = null;
        this.trackHubMap = new Map();

        // trackStanzas will not be null if this is a "onefile" hub
        if (trackStanzas) {
            const genomeId = genomeStanzas[0].getProperty("genome"); // Assumption here this is a single genome hub
            this.trackHubMap.set(genomeId, new TrackDbHub(trackStanzas, groupStanzas));

            // Search for cytoband track.  This supports a special but important case -- Genark assembly hubs
            this.cytobandStanza = this.trackStanzas.find(t => t.name === "cytoBandIdeo" && t.hasProperty("bigDataUrl")) || null;
        }
    }


    getName() {
        return this.hubStanza.getProperty("hub")
    }

    getShortLabel() {
        return this.hubStanza.getProperty("shortLabel")
    }

    getLongLabel() {
        return this.hubStanza.getProperty("longLabel")
    }

    getDescriptionUrl() {
        return this.hubStanza.getProperty("descriptionUrl")
    }


    /*  Example genome stanza
genome GCF_000186305.1
taxId 176946
groups groups.txt
description Burmese python
twoBitPath GCF_000186305.1.2bit
twoBitBptURL GCF_000186305.1.2bit.bpt
chromSizes GCF_000186305.1.chrom.sizes.txt
chromAliasBb GCF_000186305.1.chromAlias.bb
organism Python_molurus_bivittatus-5.0.2 Sep. 2013
defaultPos NW_006532014.1:484194-494194
scientificName Python bivittatus
htmlPath html/GCF_000186305.1_Python_molurus_bivittatus-5.0.2.description.html
blat dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
transBlat dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
 */

    getGenomeConfig(genomeId) {

        const genomeStanza = genomeId ? this.genomeStanzas.find(s => s.getProperty("genome") === genomeId) : this.genomeStanzas[0];
        if (!genomeStanza) {
            throw new Error(`Genome not found in hub: ${genomeId}`)
        }
        return this.#getGenomeConfig(genomeStanza)
    }

    #getGenomeConfig(genomeStanza) {

        const id = genomeStanza.getProperty("genome");
        const gsName =
            this.hubStanza.getProperty("shortLabel") ||
            genomeStanza.getProperty("scientificName") ||
            genomeStanza.getProperty("organism") ||
            genomeStanza.getProperty("description");
        const name = gsName + (gsName ? ` (${id})` : ` ${id}`);

        const config = {

            id: id,
            name: name,
            twoBitURL: genomeStanza.getProperty("twoBitPath"),
            nameSet: "ucsc",
            hubs: [this.url]
        };

        if (genomeStanza.hasProperty("chromSizes")) {
            config.chromSizesURL = genomeStanza.getProperty("chromSizes");
        } else {
            config.wholeGenomeView = false;
            config.showChromosomeWidget = false;
        }

        if (genomeStanza.hasProperty("defaultPos")) {
            const hubLocus = genomeStanza.getProperty("defaultPos");
            // Strip out coordinates => whole chromosome view
            // if (hubLocus) {
            //     const idx = hubLocus.lastIndexOf(":")
            //     config.locus = idx > 0 ? hubLocus.substring(0, idx) : hubLocus
            // }
            config.locus = hubLocus;
        }

        if (genomeStanza.hasProperty("blat")) {
            config.blat = genomeStanza.getProperty("blat");
        }
        if (genomeStanza.hasProperty("chromAliasBb")) {
            config.chromAliasBbURL = genomeStanza.getProperty("chromAliasBb");
        }
        if (genomeStanza.hasProperty("chromAlias")) {
            config.aliasURL = genomeStanza.getProperty("chromAlias");
        }
        if (genomeStanza.hasProperty("twoBitBptURL")) {
            config.twoBitBptURL = genomeStanza.getProperty("twoBitBptURL");
        }

        if (genomeStanza.hasProperty("twoBitBptUrl")) {
            config.twoBitBptURL = genomeStanza.getProperty("twoBitBptUrl");
        }

        if(this.cytobandStanza){
            config.cytobandBbURL = this.cytobandStanza.getProperty("bigDataUrl");
        }

        if (this.hubStanza.hasProperty("longLabel")) {
            config.description = this.hubStanza.getProperty("longLabel").replace("/", "\n");
        } else {
            config.description = config.id;
            if (genomeStanza.hasProperty("description")) {
                config.description += `\n${genomeStanza.getProperty("description")}`;
            }
            if (genomeStanza.hasProperty("organism")) {
                config.description += `\n${genomeStanza.getProperty("organism")}`;
            }
            if (genomeStanza.hasProperty("scientificName")) {
                config.description += `\n${genomeStanza.getProperty("scientificName")}`;
            }

            if (genomeStanza.hasProperty("htmlPath")) {
                config.infoURL = genomeStanza.getProperty("htmlPath");
            }
        }

        // Tracks.
        const filter = (t) => !Hub.filterTracks.has(t.name) && "hide" !== t.getProperty("visibility");
        config.tracks = this.#getTracksConfigs(filter);

        return config
    }

    async getGroupedTrackConfigurations(genomeId) {
        let trackHub = await this.#getTrackDbHub(genomeId);
        if (!trackHub && idMappings.has(genomeId)) {
            trackHub = await this.#getTrackDbHub(idMappings.get(genomeId));
        }
        return trackHub ? trackHub.getGroupedTrackConfigurations() : []
    }

    async #getTrackDbHub(genomeId) {
        let trackHub = this.trackHubMap.get(genomeId);
        if (!trackHub) {
            for (let stanza of this.genomeStanzas) {
                if (genomeId === stanza.getProperty("genome")) {
                    try {
                        const trackDbURL = stanza.getProperty("trackDb");
                        const trackStanzas = await loadStanzas(trackDbURL);
                        trackHub = new TrackDbHub(trackStanzas, this.groupStanzas);
                        this.trackHubMap.set(genomeId, trackHub);
                    } catch (error) {
                        console.error(`Error loading trackDb file: ${stanza.getProperty("trackDb")}`, error);
                    }
                    break
                }
            }
        }
        return trackHub
    }


    /**
     * Return an array of igv track config objects that satisfy the filter
     */
    #getTracksConfigs(filter) {
        return this.trackStanzas.filter(t => {
            return Hub.supportedTypes.has(t.format) && t.hasProperty("bigDataUrl") && (!filter || filter(t))
        })
            .map(t => this.#getTrackConfig(t))
    }

    /** example
     * track gc5Base
     * shortLabel GC Percent
     * longLabel GC Percent in 5-Base Windows
     * group map
     * visibility full
     * autoScale Off
     * maxHeightPixels 128:36:16
     * graphTypeDefault Bar
     * gridDefault OFF
     * windowingFunction Mean
     * color 0,0,0
     * altColor 128,128,128
     * viewLimits 30:70
     * type bigWig 0 100
     * bigDataUrl bbi/GCA_011100615.1_Macaca_fascicularis_6.0.gc5Base.bw
     * html html/GCA_011100615.1_Macaca_fascicularis_6.0.gc5Base
     * @param t
     */
    #getTrackConfig(t) {

        const format = t.format;

        const config = {
            "id": t.getProperty("track"),
            "name": t.getProperty("shortLabel"),
            "format": format,
            "url": t.getProperty("bigDataUrl"),
            "displayMode": t.displayMode,
        };

        if ("vcfTabix" === format) {
            config.indexURL = config.url + ".tbi";
        }

        if (t.hasProperty("longLabel") && t.hasProperty("html")) {
            if (config.description) config.description += "<br/>";
            config.description =
                `<a target="_blank" href="${t.getProperty("html")}">${t.getProperty("longLabel")}</a>`;
        } else if (t.hasProperty("longLabel")) {
            config.description = t.getProperty("longLabel");
        }

        if (t.hasProperty("autoScale")) {
            config.autoscale = t.getProperty("autoScale").toLowerCase() === "on";
        }
        if (t.hasProperty("maxHeightPixels")) {
            const tokens = t.getProperty("maxHeightPixels").split(":");
            config.maxHeight = Number.parseInt(tokens[0]);
            config.height = Number.parseInt(tokens[1]);
            config.minHeight = Number.parseInt(tokens[2]);
        }
        // TODO -- graphTypeDefault
        // TODO -- windowingFunction
        if (t.hasProperty("color")) {
            const c = t.getProperty("color");
            config.color = c.indexOf(",") > 0 ? `rgb(${c})` : c;
        }
        if (t.hasProperty("altColor")) {
            const c = t.getProperty("altColor");
            config.altColor = c.indexOf(",") > 0 ? `rgb(${c})` : c;
        }
        if (t.hasProperty("viewLimits")) {
            const tokens = t.getProperty("viewLimits").split(":");
            let min, max;
            if (tokens.length > 1) {
                min = Number.parseInt(tokens[0]);
                max = Number.parseInt(tokens[1]);
            }
            if (Number.isNaN(max) || Number.isNaN(min)) {
                console.warn(`Unexpected viewLimits value in track line: ${t.getProperty("viewLimits")}`);
            } else {
                config.min = min;
                config.max = max;
            }

        }
        if (t.hasProperty("itemRgb")) ;
        if ("hide" === t.getProperty("visibility")) {
            // TODO -- this not supported yet
            config.visible = false;
        }
        if (t.hasProperty("url")) {
            config.infoURL = t.getProperty("url");
        }
        if (t.hasProperty("searchIndex")) {
            config.searchIndex = t.getProperty("searchIndex");
        }
        if (t.hasProperty("searchTrix")) {
            config.trixURL = t.getProperty("searchTrix");
        }
        if (t.hasProperty("group")) {
            config._group = t.getProperty("group");
            if (this.groupPriorityMap && this.groupPriorityMap.has(config._group)) {
                const nextPriority = this.groupPriorityMap.get(config._group) + 1;
                config.order = nextPriority;
                this.groupPriorityMap.set(config._group, nextPriority);
            }
        }
        const labelFields = t.hasProperty("defaultLabelFields") ?
            t.getProperty("defaultLabelFields") :
            t.getProperty("labelFields");
        if (labelFields) {
            config.labelField = labelFields.split(",")[0];
        }

        return config
    }

}

/*
 https://genomewiki.ucsc.edu/index.php/Assembly_Hubs
 https://genome.ucsc.edu/goldenpath/help/hgTrackHubHelp.html
 https://genome.ucsc.edu/goldenPath/help/hgTrackHubHelp
 https://genome.ucsc.edu/goldenpath/help/trackDb/trackDbHub.html
 */

const urlProperties = new Set(["descriptionUrl", "desriptionUrl",
    "twoBitPath", "blat", "chromAliasBb", "twoBitBptURL", "twoBitBptUrl", "htmlPath", "bigDataUrl",
    "genomesFile", "trackDb", "groups", "include", "html", "searchTrix", "groups",
    "chromSizes"]);


const hubCache = new Map();

async function loadHub(url) {
    if (hubCache.has(url)) {
        return hubCache.get(url)
    }

    const stanzas = await loadStanzas(url);
    if (stanzas.length < 1) {
        throw new Error("Empty hub file")
    }

    const hubStanza = stanzas[0];
    if (hubStanza.type !== "hub") {
        throw new Error("First stanza must be a hub stanza")
    }

    let genomeStanzas;
    let trackStanzas;
    if (hubStanza.getProperty("useOneFile") === "on") {
        // This is a "onefile" hub, all stanzas are in the same file
        if (stanzas[1].type !== "genome") {
            throw new Error("Unexpected hub file -- expected 'genome' stanza but found " + stanzas[1].type)
        }
        const genomeStanza = stanzas[1];
        genomeStanzas = [genomeStanza];
        trackStanzas = stanzas.slice(2);

        // If this is an assembly check chromSizes. This file can be very large, and not needed if whole genome view
        // is not enabled.  Remove it if > 100 kb
        if (genomeStanza.hasOwnProperty("chromSizes")) {
            const chromSizes = genomeStanza.getProperty("chromSizes");
            try {
                const contentLength = await igvxhr$1.getContentLength(chromSizes);
                if (contentLength > 100000) {
                    genomeStanza.removeProperty("chromSizes");
                }
            } catch (e) {
                console.error(`Error getting content length for chromSizes ${chromSizes}`, e);
            }

        }

    } else {
        if (!hubStanza.hasProperty("genomesFile")) {
            throw new Error("hub.txt must specify 'genomesFile'")
        }
        genomeStanzas = await loadStanzas(hubStanza.getProperty("genomesFile"));
    }

    // Load group files for all genomes, if any.
    const uniqGroupURLs = new Set();
    genomeStanzas.forEach(s => {
        const groupURL = s.getProperty("groups");
        if (groupURL) uniqGroupURLs.add(groupURL);

    });
    const groupStanzas = [];
    const groupPromises = Array.from(uniqGroupURLs).map(async url => {
        const stanza = await loadStanzas(url);
        return stanza
    });
    const groupResults = await Promise.all(groupPromises);
    groupResults.forEach(stanza => groupStanzas.push(...stanza));

    const hub = new Hub(url, hubStanza, genomeStanzas, trackStanzas, groupStanzas);

    hubCache.set(url, hub);

    return hub
}


/**
 * Parse a UCSC  file
 * @param url
 * @returns {Promise<*[]>}
 */
async function loadStanzas(url) {

    const idx = url.lastIndexOf("/");
    const baseURL = url.substring(0, idx + 1);
    const host = getHost(url);

    //const response = await fetch(url)
    const data = await igvxhr$1.loadString(url, {});    //await response.text()
    const lines = data.split(/\n|\r\n|\r/g);

    const nodes = [];
    let currentNode;
    let startNewNode = true;
    for (let i = 0; i < lines.length; i++) {

        let line = lines[i].trim();

        if (line.length == 0) {
            // Break - start a new node
            startNewNode = true;
        } else {
            if (line.startsWith("#")) {
                continue
            }

            while (line.endsWith('\\')) {
                i++;
                if (i >= lines.length) {
                    break
                }
                line = line.substring(0, line.length - 1) + lines[i].trim();
            }

            if (line.startsWith("include")) {
                const relativeURL = line.substring(8).trim();
                const includeURL = getDataURL(relativeURL, host, baseURL);
                const includeStanzas = await loadStanzas(includeURL);
                for (let s of includeStanzas) {
                    nodes.push(s);
                }
            }


            const index = line.indexOf(' ');
            const key = line.substring(0, index).trim();
            let value = line.substring(index + 1).trim();

            if (key === "type") {
                // The "type" property contains format and sometimes other information. For example, data range
                // on a bigwig "type bigWig 0 .5"
                const tokens = value.split(/\s+/);
                value = tokens[0];
                if (value === "bigWig" && tokens.length === 3) {
                    // This is a bigWig with a range
                    const min = tokens[1];
                    const max = tokens[2];
                    if (currentNode) {
                        currentNode.setProperty("min", min);
                        currentNode.setProperty("max", max);
                    }
                }

            } else if (!["shortLabel", "longLabel", "metadata", "label"].includes(key)) {
                const tokens = value.split(/\s+/);
                value = tokens[0];
            }

            if (urlProperties.has(key) || value.endsWith("URL") || value.endsWith("Url")) {
                value = getDataURL(value, host, baseURL);
            }

            if (startNewNode) {
                currentNode = new Stanza(key, value);
                nodes.push(currentNode);
                startNewNode = false;
            }

            currentNode.setProperty(key, value);
        }
    }
    return resolveParents(nodes)
}

function firstWord(str) {
    const idx = str.indexOf(' ');
    return idx > 0 ? str.substring(0, idx) : str
}

function resolveParents(nodes) {
    const nodeMap = new Map();
    for (let n of nodes) {
        nodeMap.set(n.name, n);
    }
    for (let n of nodes) {
        if (n.properties.has("parent")) {
            const parentName = firstWord(n.properties.get("parent"));
            n.parent = nodeMap.get(parentName);
        }
    }
    return nodes
}

function getDataURL(url, host, baseURL) {
    if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("gs://") || url.startsWith("s3://")) {
        return url
    } else if (url.startsWith("/")) {
        return host + url
    } else {
        return baseURL + url
    }
}

function getHost(url) {
    let host;
    if (url.startsWith("https://") || url.startsWith("http://")) {
        try {
            const tmp = new URL(url);
            host = `${tmp.protocol}//${tmp.host}`;
        } catch (e) {
            console.error("Error parsing base URL host", e);
            throw e
        }
    } else {
        host = '';
    }
    return host
}

const DEFAULT_GENOMES_URL = "https://igv.org/genomes/genomes3.json";
const BACKUP_GENOMES_URL = "https://raw.githubusercontent.com/igvteam/igv-data/refs/heads/main/genomes/web/genomes.json";

const GenomeUtils = {

    initializeGenomes: async function (config) {

        if (!GenomeUtils.KNOWN_GENOMES) {

            let table = {};

            const processJson = (jsonArray, table) => {
                jsonArray.forEach(function (json) {
                    table[json.id] = json;
                });
                return table
            };

            // Get default genomes
            if (config.loadDefaultGenomes !== false) {
                try {
                    const jsonArray = await igvxhr$1.loadJson(DEFAULT_GENOMES_URL, {timeout: 2000});
                    processJson(jsonArray, table);
                } catch (error) {
                    try {
                        console.error("Error initializing default genomes:", error);
                        const jsonArray = await igvxhr$1.loadJson(BACKUP_GENOMES_URL, {timeout: 10000});
                        processJson(jsonArray, table);
                    } catch (e) {
                        console.error("Error initializing backup genomes:", error);
                    }
                }
            }

            // Append user-defined genomes, which might override defaults
            const genomeList = config.genomeList || config.genomes;
            if (genomeList) {
                if (typeof genomeList === 'string') {
                    const jsonArray = await igvxhr$1.loadJson(genomeList, {});
                     processJson(jsonArray, table);
                } else {
                     processJson(genomeList, table);
                }
            }
            GenomeUtils.KNOWN_GENOMES = table;
        }
    },

    isWholeGenomeView: function (chr) {
        return 'all' === chr.toLowerCase()
    },

    // Expand a genome id to a reference object, if needed
    expandReference: async function (alert, idOrConfig) {

        // idOrConfig might be a json string?  I'm actually not sure how this arises.
        if (isString$3(idOrConfig) && idOrConfig.startsWith("{")) {
            try {
                idOrConfig = JSON.parse(idOrConfig);
            } catch (e) {
                // Apparently its not json,  could be an ID starting with "{".  Unusual but legal.
            }
        }

        let genomeID;
        if (isString$3(idOrConfig)) {
            genomeID = idOrConfig;
        } else if (idOrConfig.genome) {
            genomeID = idOrConfig.genome;
        } else if (idOrConfig.id !== undefined && !(idOrConfig.fastaURL || idOrConfig.twobitURL)) {
            // Backward compatibility
            genomeID = idOrConfig.id;
        }

        if (genomeID) {
            const knownGenomes = GenomeUtils.KNOWN_GENOMES;
            let reference = knownGenomes[genomeID];
            if (!reference) {
                if ((genomeID.startsWith("GCA_") || genomeID.startsWith("GCF_")) && genomeID.length >= 13) {
                    try {
                        const hubURL = convertToHubURL(genomeID);
                        const hub = await loadHub(hubURL);
                        reference = hub.getGenomeConfig(genomeID);
                    } catch (e) {
                        console.error(e);
                    }
                }

                if (!reference) {
                    alert.present(new Error(`Unknown genome id: ${genomeID}`), undefined);
                }
            }
            return reference
        } else {
            return idOrConfig
        }
    }
};

/**
 * Created by dat on 9/16/16.
 */

const NOT_LOADED_MESSAGE = 'Error loading track data';
let lastClickTime = 0;
let popupTimerID;
let trackViewportPopoverList = [];

let popover;

class TrackViewport extends Viewport {

    constructor(trackView, viewportColumn, referenceFrame, width) {
        super(trackView, viewportColumn, referenceFrame, width);
    }

    initializationHelper() {

        this.spinnerElement = document.createElement('div');
        this.spinnerElement.className = 'igv-loading-spinner-container';
        this.viewportElement.appendChild(this.spinnerElement);
        this.spinnerElement.appendChild(document.createElement('div'));

        this.overlayElement = document.createElement('div');
        //this.overlayElement.className = 'igv-track-viewport-overlay';
        this.overlayElement.style.position = 'absolute';
        this.overlayElement.style.width = '100%';
        this.overlayElement.style.height = '100%';
        this.viewportElement.appendChild(this.overlayElement);

        const track = this.trackView.track;
        if ('sequence' !== track.type) {
            this.zoomInNoticeElement = TrackViewport.createZoomInNotice(this.viewportElement);
        }

        if ("sequence" !== track.id) {
            this.trackLabelElement = document.createElement('div');
            this.trackLabelElement.className = 'igv-track-label';
            this.viewportElement.appendChild(this.trackLabelElement);
            this.setTrackLabel(track.name || "");
            if (false === this.browser.doShowTrackLabels) {
                this.trackLabelElement.style.display = 'none';
            }
            // Setting track height can affect label style
            if (this.trackView.track.height) {
                this.setHeight(this.trackView.track.height);
            }
        }

        this.doRenderBucketLabels = (new Set(['seg', 'mut']).has(this.trackView.track.type));

        this.stopSpinner();
        this.addMouseHandlers();

    }

    didPresentZoomInNotice() {

        const a = !(undefined === this.zoomInNoticeElement);

        if (false === a) {
            return a
        }

        return 'flex' === this.zoomInNoticeElement.style.display
    }

    setContentHeight(contentHeight) {
        super.setContentHeight(contentHeight);
        if (this.featureCache) this.featureCache.redraw = true;
    }

    setTrackLabel(label) {

        this.trackLabelElement.innerHTML = '';
        this.trackLabelElement.innerHTML = label;

        const txt = this.trackLabelElement.textContent;
        this.trackLabelElement.setAttribute('title', txt);
    }

    startSpinner() {
        if (this.spinnerElement) {
            this.spinnerElement.style.display = 'flex';
        }
    }

    stopSpinner() {
        if (this.spinnerElement) {
            this.spinnerElement.style.display = 'none';
        }
    }

    /**
     * Test to determine if we are zoomed in far enough to see features. Applicable to tracks with visibility windows.
     *
     * As a side effect the viewports canvas is removed if zoomed out.
     *
     * @returns {boolean} true if we are zoomed in past visibility window, false otherwise
     */
    checkZoomIn() {

        const zoomedOutOfWindow = () => {
            if (this.referenceFrame.chr.toLowerCase() === "all") {
                return !this.trackView.track.supportsWholeGenome
            } else {
                const visibilityWindow = this.trackView.track.visibilityWindow;
                return (
                    visibilityWindow !== undefined && visibilityWindow > 0 &&
                    (this.referenceFrame.bpPerPixel * this.viewportElement.clientWidth > visibilityWindow))
            }
        };

        if (this.trackView.track && "sequence" === this.trackView.track.type && this.referenceFrame.bpPerPixel > bppSequenceThreshold) {

            if (this.canvas && this.canvas.parentNode) {
                this.canvas.parentNode.removeChild(this.canvas);
            }
            this.canvas = undefined;
            return false
        }

        if (!(this.viewIsReady())) {
            return false
        }


        if (zoomedOutOfWindow()) {

            // Out of visibility window
            if (this.canvas && this.canvas.parentNode) {
                this.canvas.parentNode.removeChild(this.canvas);
                this.canvas = undefined;
            }
            if (this.trackView.track.autoHeight) {
                const minHeight = this.trackView.minHeight || 0;
                this.setContentHeight(minHeight);
            }
            if (this.zoomInNoticeElement) {
                this.zoomInNoticeElement.style.display = 'flex';
            }
            return false
        } else {
            if (this.zoomInNoticeElement) {
                this.zoomInNoticeElement.style.display = 'none';
            }
            return true
        }

    }

    /**
     * Adjust the canvas to the current genomic state.
     */
    shift() {
        const referenceFrame = this.referenceFrame;
        if (this.canvas &&
            this.canvas._data &&
            this.canvas._data.referenceFrame.chr === this.referenceFrame.chr &&
            this.canvas._data.bpPerPixel === referenceFrame.bpPerPixel) {
            this.canvas._data.pixelShift = Math.round((this.canvas._data.bpStart - referenceFrame.start) / referenceFrame.bpPerPixel);
            this.canvas.style.left = this.canvas._data.pixelShift + "px";
        }
    }

    genomicRange() {
        return {
            start: this.referenceFrame.start,
            end: this.referenceFrame.start + this.referenceFrame.bpPerPixel * this.viewportElement.clientWidth
        }
    }

    /**
     * Set the content top of the current view.  This is triggered by scrolling.   If the current canvas extent is not
     * sufficient to cover the new vertical range repaint.
     *
     * @param contentTop - the "top" property of the virtual content div, 0 unless track is scrolled vertically
     *
     *
     */
    setTop(contentTop) {

        super.setTop(contentTop);

        this.overlayElement.style.top = `-${contentTop}px`;

        if (!this.canvas) {
            this.repaint();
        } else {
            // See if currently painted canvas covers the vertical range of the viewport.  If not repaint
            const h = this.viewportElement.clientHeight;
            const vt = this.canvas._data.pixelTop - contentTop;
            const vb = vt + this.canvas._data.pixelHeight;
            if (vt > 0 || vb < h) {
                this.repaint();
            }
        }

        // Offset canvas if contentTop does not match pixelTop.  contetTop is the top of the virtual canvas
        // relative to the viewport, and is always <= 0, i.e. content top is shifted "up" when the
        // track is scrolled vertically  making the top of the virtual canvas above the top of the viewport.
        if (this.canvas) {
            let offset = this.canvas._data.pixelTop - contentTop;
            this.canvas.style.top = `${offset}px`;
        }
    }

    setHeight(h) {
        super.setHeight(h);
        const labelElement = this.viewportElement.querySelector(".igv-track-label");
        if (labelElement) {
            // If the track height is small center the label vertically.
            if (h < 30) {
                labelElement.style.margin = 0;
                labelElement.style.top = "50%";
                labelElement.style.transform = "translateY(-50%)";
                labelElement.style["-ms-transform"] = "translateY(-50%)";
            } else {
                labelElement.style.removeProperty("margin");
                labelElement.style.removeProperty("top");
                labelElement.style.removeProperty("transform");
                labelElement.style.removeProperty("-ms-transform");
            }
        }
    }

    async loadFeatures() {

        try {
            const referenceFrame = this.referenceFrame;
            const chr = referenceFrame.chr;

            // Expand the requested range so we can pan a bit without reloading.  But not beyond chromosome bounds
            const chromosome = await this.browser.genome.loadChromosome(chr);
            const chrLength = chromosome ? chromosome.bpLength : Number.MAX_SAFE_INTEGER;
            const pixelWidth = this.viewportElement.clientWidth;// * 3;
            const bpWidth = pixelWidth * referenceFrame.bpPerPixel;
            const bpStart = Math.floor(Math.max(0, referenceFrame.start - bpWidth));
            const bpEnd = Math.ceil(Math.min(chrLength, referenceFrame.start + bpWidth + bpWidth));  // Add one screen width to end

            if (this.loading && this.loading.start === bpStart && this.loading.end === bpEnd) {
                return undefined
            }
            this.loading = {start: bpStart, end: bpEnd};
            this.startSpinner();

            const track = this.trackView.track;
            const features = await this.getFeatures(track, chr, bpStart, bpEnd, referenceFrame.bpPerPixel);
            if (features) {
                let roiFeatures = [];
                if (track.roiSets && track.roiSets.length > 0) {
                    for (let roiSet of track.roiSets) {
                        const features = await roiSet.getFeatures(chr, bpStart, bpEnd, referenceFrame.bpPerPixel);
                        roiFeatures.push({track: roiSet, features});
                    }
                }

                const mr = track && (track.resolutionAware);   //
                const windowFunction = this.windowFunction;
                this.featureCache = new FeatureCache(chr, bpStart, bpEnd, referenceFrame.bpPerPixel, features, roiFeatures, mr, windowFunction);
                this.loading = false;
                this.hideMessage();
                this.stopSpinner();

                // Notify listeners, like any interactive filtering handlers,
                // that data is ready for this track.
                this.browser.fireEvent('featuresloaded', [this]);

                return this.featureCache
            }
        } catch (error) {
            // Track might have been removed during load
            if (this.trackView && this.trackView.disposed !== true) {
                this.showMessage(NOT_LOADED_MESSAGE);
                this.browser.alert.present(error);
                console.error(error);
            }
        } finally {
            this.loading = false;
            this.stopSpinner();
        }
    }

    get track() {
        return this.trackView.track
    }

    get windowFunction() {
        return this.track ? this.track.windowFunction : undefined
    }

    /**
     * Compute the genomic extent and needed pixelWidth to repaint the canvas for the current genomic state.
     * Normally the canvas is size 3X the width of the viewport, however there is no left-right panning for WGV so
     * canvas width is viewport width.
     * @returns {{bpEnd: *, pixelWidth: (*|number), bpStart: number}}
     */
    repaintDimensions() {
        const isWGV = GenomeUtils.isWholeGenomeView(this.referenceFrame.chr);
        const pixelWidth = isWGV ? this.viewportElement.clientWidth : 3 * this.viewportElement.clientWidth;
        const bpPerPixel = this.referenceFrame.bpPerPixel;
        const bpStart = this.referenceFrame.start - (isWGV ? 0 : this.viewportElement.clientWidth * bpPerPixel);
        const bpEnd = isWGV ? Number.MAX_SAFE_INTEGER : this.referenceFrame.start + 2 * this.viewportElement.clientWidth * bpPerPixel + 1;
        return {
            bpStart, bpEnd, pixelWidth
        }
    }

    /**
     * Repaint the canvas using the cached features
     *
     */
    repaint() {

        if (undefined === this.featureCache) {
            return
        }

        const {features, roiFeatures} = this.featureCache;

        // Canvas dimensions.
        // For deep tracks we paint a canvas == 3*viewportHeight centered on the current vertical scroll position
        const {bpStart, bpEnd, pixelWidth} = this.repaintDimensions();
        const viewportHeight = this.viewportElement.clientHeight;
        const contentHeight = this.getContentHeight();
        const maxHeight = roiFeatures ? Math.max(contentHeight, viewportHeight) : contentHeight;  // Need to fill viewport for ROIs.
        const pixelHeight = Math.min(maxHeight, 3 * viewportHeight);
        if (0 === pixelWidth || 0 === pixelHeight) {
            if (this.canvas && this.canvas.parentNode) {
                this.canvas.parentNode.removeChild(this.canvas);
            }
            return
        }
        const pixelTop = Math.max(0, this.contentTop - Math.floor(pixelHeight / 3));
        const bpPerPixel = this.referenceFrame.bpPerPixel;
        const pixelXOffset = Math.round((bpStart - this.referenceFrame.start) / bpPerPixel);
        const canvasTop = pixelTop - (this.contentTop || 0);
        const newCanvas = document.createElement('canvas');
        newCanvas.style.position = 'relative';
        newCanvas.style.display = 'block';
        newCanvas.style.width = pixelWidth + "px";
        newCanvas.style.height = pixelHeight + "px";
        newCanvas.style.left = pixelXOffset + "px";
        newCanvas.style.top = canvasTop + "px";

        // Always use high DPI if in "FILL" display mode, otherwise use track setting;
        const devicePixelRatio = ("FILL" === this.trackView.track.displayMode || this.trackView.track.supportHiDPI !== false) ?
            window.devicePixelRatio : 1;
        newCanvas.width = devicePixelRatio * pixelWidth;
        newCanvas.height = devicePixelRatio * pixelHeight;

        const ctx = newCanvas.getContext("2d");
        ctx.scale(devicePixelRatio, devicePixelRatio);
        ctx.translate(0, -pixelTop);

        const drawConfiguration =
            {
                context: ctx,
                contentTop: this.contentTop,
                pixelXOffset,
                pixelWidth,
                pixelHeight,
                pixelTop,
                bpStart,
                bpEnd,
                bpPerPixel,
                pixelShift: pixelXOffset,              // Initial value, changes with track pan (drag)
                windowFunction: this.windowFunction,
                referenceFrame: this.referenceFrame,
                selection: this.selection,
                viewport: this,
                viewportWidth: this.viewportElement.clientWidth
            };

        this.draw(drawConfiguration, features, roiFeatures);

        if (this.canvas && this.canvas.parentNode) {
            this.canvas.parentNode.removeChild(this.canvas);
        }
        newCanvas._data = drawConfiguration;
        this.canvas = newCanvas;
        this.viewportElement.appendChild(newCanvas);

    }

    refresh() {
        if (!(this.canvas && this.featureCache)) return

        const drawConfiguration = this.canvas._data;
        drawConfiguration.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        const {features, roiFeatures} = this.featureCache;
        this.draw(drawConfiguration, features, roiFeatures);
    }

    /**
     * Draw the associated track.
     *
     * @param drawConfiguration
     * @param features
     * @param roiFeatures
     */
    draw(drawConfiguration, features, roiFeatures) {

        if (features) {
            drawConfiguration.features = features;
            this.trackView.track.draw(drawConfiguration);
        }
        if (roiFeatures && roiFeatures.length > 0) {
            for (let r of roiFeatures) {
                drawConfiguration.features = r.features;
                r.track.draw(drawConfiguration);
            }
        }
    }

    containsPosition(chr, position) {
        if (this.referenceFrame.chr === chr && position >= this.referenceFrame.start) {
            return position <= this.referenceFrame.calculateEnd(this.getWidth())
        } else {
            return false
        }
    }

    isLoading() {
        return this.loading
    }

    savePNG() {

        if (!this.canvas) return

        const w = this.viewportElement.clientWidth * window.devicePixelRatio;
        const h = this.viewportElement.clientHeight * window.devicePixelRatio;

        const x = -this.canvas.offsetLeft * window.devicePixelRatio;

        const canvasMetadata = this.canvas._data;
        const canvasTop = canvasMetadata ? canvasMetadata.pixelTop : 0;
        const y = (this.contentTop - canvasTop) * window.devicePixelRatio;

        const ctx = this.canvas.getContext("2d");
        const imageData = ctx.getImageData(x, y, w, h);
        const exportCanvas = document.createElement('canvas');
        const exportCtx = exportCanvas.getContext('2d');
        exportCanvas.width = imageData.width;
        exportCanvas.height = imageData.height;
        exportCtx.putImageData(imageData, 0, 0);

        // filename = this.trackView.track.name + ".png";
        const filename = (this.trackLabelElement && this.trackLabelElement.textContent ? this.trackLabelElement.textContent : "image") + ".png";
        const data = exportCanvas.toDataURL("image/png");
        download$1(filename, data);
    }

    saveSVG() {

        const marginTop = 32;
        const marginLeft = 32;

        let {width, height} = this.browser.columnContainer.getBoundingClientRect();

        const h_render = 8000;

        const config =
            {
                width,
                height: h_render,
                backdropColor: 'white',
                multiLocusGap: 0,
                viewbox:
                    {
                        x: 0,
                        y: 0,
                        width,
                        height: h_render
                    }
            };

        const context = new ctx(config);

        const delta =
            {
                deltaX: marginLeft,
                deltaY: marginTop
            };

        this.renderSVGContext(context, delta, false);

        // reset height to trim away unneeded svg canvas real estate. Yes, a bit of a hack.
        context.setHeight(height);

        const str = (this.trackView.track.name || this.trackView.track.id).replace(/\W/g, '');
        const index = this.browser.referenceFrameList.indexOf(this.referenceFrame);

        const svg = context.getSerializedSvg(true);
        const data = URL.createObjectURL(new Blob([svg], {type: "application/octet-stream"}));

        const id = `${str}_referenceFrame_${index}_guid_${guid$2()}`;
        download$1(`${id}.svg`, data);

    }

    renderSVGContext(context, {deltaX, deltaY}, includeLabel = true) {

        if (false === this.didPresentZoomInNotice()) {

            const {width, height} = this.viewportElement.getBoundingClientRect();

            const str = (this.trackView.track.name || this.trackView.track.id).replace(/\W/g, '');
            const index = this.browser.referenceFrameList.indexOf(this.referenceFrame);
            const id = `${str}_referenceFrame_${index}_guid_${guid$2()}`;

            const x = deltaX;
            const y = deltaY - this.contentTop;
            const yClipOffset = this.contentTop;

            context.saveWithTranslationAndClipRect(id, x, y, width, height, yClipOffset);

            const {start, bpPerPixel} = this.referenceFrame;
            const pixelXOffset = Math.round((start - this.referenceFrame.start) / bpPerPixel);

            const config =
                {
                    context,
                    contentTop: this.contentTop,
                    pixelXOffset,
                    pixelWidth: width,
                    pixelHeight: height,
                    pixelTop: yClipOffset,
                    bpStart: start,
                    bpEnd: start + (width * bpPerPixel),
                    bpPerPixel,
                    pixelShift: pixelXOffset,              // Initial value, changes with track pan (drag)
                    referenceFrame: this.referenceFrame,
                    selection: this.selection,
                    viewport: this,
                    viewportWidth: this.viewportElement.clientWidth
                };

            const features = this.featureCache ? this.featureCache.features : undefined;
            const roiFeatures = this.featureCache ? this.featureCache.roiFeatures : undefined;
            this.draw(config, features, roiFeatures);

            context.restore();
        }

        if (includeLabel && this.trackLabelElement && this.browser.doShowTrackLabels) {
            const {x: x_p, y: y_p, width: width_p, height: height_p} = this.viewportElement.getBoundingClientRect();
            const {x: x_c, y: y_c, width: width_c, height: height_c} = this.trackLabelElement.getBoundingClientRect();
            const x = x_c - x_p;
            const y = y_c - y_p;
            const width = width_c;
            const height = height_c;
            this.renderTrackLabelSVG(context, deltaX + x, deltaY + y, width, height);
        }

    }

    renderTrackLabelSVG(context, tx, ty, width, height) {

        const str = (this.trackView.track.name || this.trackView.track.id).replace(/\W/g, '');
        const id = `${str}_track_label_guid_${guid$2()}`;

        const text = this.trackLabelElement.textContent;
        const {width: stringWidth} = context.measureText(text);

        const dx = 0.25 * (width - stringWidth);
        const dy = 0.7 * (height - 12);

        this.renderElementSVG(context, id, tx, ty, width, height, text, dx, dy);
    }

    // render track label element called from renderSVGContext()
    renderElementSVG(context, id, tx, ty, width, height, text, dx, dy) {

        context.saveWithTranslationAndClipRect(id, tx, ty, width, height, 0);

        context.fillStyle = "white";
        context.fillRect(0, 0, width, height);

        context.font = "12px Arial";
        context.fillStyle = 'rgb(68, 68, 68)';

        context.fillText(text, dx, height - dy);

        context.strokeStyle = 'rgb(68, 68, 68)';
        context.strokeRect(0, 0, width, height);

        context.restore();

    }

    get cachedFeatures() {
        return this.featureCache ? this.featureCache.features : []
    }

    clearCache() {
        this.featureCache = undefined;
        if (this.canvas) this.canvas._data = undefined;
    }

    async getFeatures(track, chr, start, end, bpPerPixel) {
        if (this.featureCache && this.featureCache.containsRange(chr, start, end, bpPerPixel, this.windowFunction)) {
            return this.featureCache.features
        } else if (typeof track.getFeatures === "function") {
            const features = await track.getFeatures(chr, start, end, bpPerPixel, this);
            this.checkContentHeight(features);
            return features
        } else {
            return undefined
        }
    }

    needsRepaint() {

        if (!this.canvas) return true

        const data = this.canvas._data;
        return !data ||
            this.referenceFrame.start < data.bpStart ||
            this.referenceFrame.end > data.bpEnd ||
            this.referenceFrame.chr !== data.referenceFrame.chr ||
            this.referenceFrame.bpPerPixel !== data.bpPerPixel ||
            this.windowFunction !== data.windowFunction
    }

    needsReload() {
        if (!this.featureCache) return true
        const {chr, bpPerPixel} = this.referenceFrame;
        const {bpStart, bpEnd} = this.repaintDimensions();
        return (!this.featureCache.containsRange(chr, bpStart, bpEnd, bpPerPixel, this.windowFunction))
    }

    static createZoomInNotice(parentElement) {

        const container = document.createElement('div');
        container.className = 'igv-zoom-in-notice-container';
        parentElement.appendChild(container);

        const element = document.createElement('div');
        container.appendChild(element);
        element.textContent = 'Zoom in to see features';

        container.style.display = 'none';

        return container
    }

    viewIsReady() {
        return this.browser && this.browser.referenceFrameList && this.referenceFrame
    }

    addMouseHandlers() {

        this.addViewportContextMenuHandler(this.viewportElement);

        // Mouse down
        const md = (event) => {
            this.enableClick = true;
            this.browser.mouseDownOnViewport(event, this);
            pageCoordinates(event);
        };
        this.viewportElement.addEventListener('mousedown', md);
        this.viewportElement.addEventListener('touchstart', md);

        // Mouse up
        const mu = (event) => {
            // Any mouse up cancels drag and scrolling
            if (this.browser.dragObject || this.browser.isScrolling) {
                this.browser.cancelTrackPan();
                // event.preventDefault();
                // event.stopPropagation();
                this.enableClick = false;   // Until next mouse down
            } else {
                this.browser.cancelTrackPan();
                this.browser.endTrackDrag();
            }
        };
        this.viewportElement.addEventListener('mouseup', mu);
        this.viewportElement.addEventListener('touchend', mu);

        // Mouse move
        if (typeof this.trackView.track.hoverText === 'function') {
            let lastTooltipUpdate = 0;
            this.viewportElement.addEventListener('mousemove', (event => {
                const now = Date.now();
                if (event.buttons === 0 && (now - lastTooltipUpdate > 100)) {
                    lastTooltipUpdate = now;
                    const clickState = this.createClickState(event);
                    if (clickState) {
                        const tooltip = this.trackView.track.hoverText(clickState);
                        if (tooltip) {
                            this.viewportElement.setAttribute("title", tooltip);
                        } else {
                            this.viewportElement.removeAttribute("title");
                        }
                    }
                }
            }));
        }

        this.addViewportClickHandler(this.viewportElement);

        if (this.trackView.track.name && "sequence" !== this.trackView.track.config.type) {
            this.addTrackLabelClickHandler(this.trackLabelElement);
        }

    }

    addViewportContextMenuHandler(viewport) {

        viewport.addEventListener('contextmenu', (event) => {

            // Ignore if we are doing a drag.  This can happen with touch events.
            if (this.browser.dragObject) {
                return false
            }

            const clickState = this.createClickState(event);

            if (undefined === clickState) {
                return false
            }

            event.preventDefault();

            // Track specific items
            let menuItems = [];
            if (typeof this.trackView.track.contextMenuItemList === "function") {
                const trackMenuItems = this.trackView.track.contextMenuItemList(clickState);
                if (trackMenuItems) {
                    menuItems = trackMenuItems;
                }
            }

            // Add items common to all tracks
            if (menuItems.length > 0) {
                menuItems.push({label: document.createElement('hr')});
            }

            menuItems.push({label: 'Save Image (PNG)', click: () => this.savePNG()});
            menuItems.push({label: 'Save Image (SVG)', click: () => this.saveSVG()});

            this.browser.menuPopup.presentTrackContextMenu(event, menuItems);
        });

    }


    addViewportClickHandler(viewport) {

        viewport.addEventListener('click', (event) => {

            if (this.enableClick && this.canvas) {
                if (3 === event.which || event.ctrlKey) {
                    return
                }

                if (this.browser.dragObject || this.browser.isScrolling) {
                    return
                }

                // Treat as a mouse click, it's either a single or double click.
                // Handle here and stop propagation / default
                event.preventDefault();

                const mouseX = translateMouseCoordinates(event, this.viewportElement).x;
                const mouseXCanvas = translateMouseCoordinates(event, this.canvas).x;
                const referenceFrame = this.referenceFrame;
                Math.floor((referenceFrame.start) + referenceFrame.toBP(mouseXCanvas));

                const time = Date.now();

                if (time - lastClickTime < this.browser.constants.doubleClickDelay) {

                    // double-click
                    if (popupTimerID) {
                        window.clearTimeout(popupTimerID);
                        popupTimerID = undefined;
                    }

                    const centerBP = Math.round(referenceFrame.start + referenceFrame.toBP(mouseX));

                    let string;

                    if ('all' === this.referenceFrame.chr.toLowerCase()) {

                        const chr = this.browser.genome.getChromosomeCoordinate(centerBP).chr;

                        if (1 === this.browser.referenceFrameList.length) {
                            string = chr;
                        } else {
                            const loci = this.browser.referenceFrameList.map(({locusSearchString}) => locusSearchString);
                            const index = this.browser.referenceFrameList.indexOf(this.referenceFrame);
                            loci[index] = chr;
                            string = loci.join(' ');
                        }

                        this.browser.search(string);

                    } else {
                        if (event.shiftKey) {
                            this.browser.zoomWithScaleFactor(2, centerBP, this.referenceFrame);
                        } else {
                            this.browser.zoomWithScaleFactor(0.5, centerBP, this.referenceFrame);
                        }
                    }


                } else {
                    // single-click

                    /*if (event.shiftKey && typeof this.trackView.track.shiftClick === "function") {

                        this.trackView.track.shiftClick(xBP, event)

                    } else */

                    if (typeof this.trackView.track.popupData === "function") {

                        popupTimerID = setTimeout(() => {

                                const content = this.getPopupContent(event);
                                if (content) {

                                    if (false === event.shiftKey) {

                                        if (popover) {
                                            popover.dispose();
                                        }

                                        if (trackViewportPopoverList.length > 0) {
                                            for (const gp of trackViewportPopoverList) {
                                                gp.dispose();
                                            }
                                            trackViewportPopoverList.length = 0;
                                        }

                                        popover = new Popover(this.viewportElement.parentElement, true, undefined, () => {
                                            popover.dispose();
                                        });

                                        popover.presentContentWithEvent(event, content);
                                    } else {

                                        let po = new Popover(this.viewportElement.parentElement, true, undefined, () => {
                                            const index = trackViewportPopoverList.indexOf(po);
                                            trackViewportPopoverList.splice(index, 1);
                                            po.dispose();
                                        });

                                        trackViewportPopoverList.push(po);

                                        po.presentContentWithEvent(event, content);
                                    }

                                }
                                window.clearTimeout(popupTimerID);
                                popupTimerID = undefined;
                            },
                            this.browser.constants.doubleClickDelay);
                    }
                }

                lastClickTime = time;

            }
        });
    }

    addTrackLabelClickHandler(trackLabel) {
        trackLabel.addEventListener('click', (event) => {
            event.stopPropagation();

            // Remove any existing popover
            this.removeTrackLabelPopover();

            const {track} = this.trackView;
            let content;
            if (typeof track.description === 'function') {
                content = track.description(); // Should return a DOM node or fragment
            } else if (track.description) {
                // Fallback: wrap string in a row
                const row = document.createElement('div');
                row.className = 'igv-track-label-popover__row';
                row.textContent = track.description;
                content = row;
            }

            if (content) {
                this.showTrackLabelPopover(event, content, track.name || '');
            }
        });
    }

    showTrackLabelPopover(event, content, title) {
        // Create popover container
        const popover = document.createElement('div');
        popover.className = 'igv-track-label-popover';

        // Header
        const header = document.createElement('div');
        header.className = 'igv-track-label-popover__header';

        const titleDiv = document.createElement('div');
        titleDiv.className = 'igv-track-label-popover__title';
        titleDiv.textContent = title;

        const closeBtn = document.createElement('div');
        closeBtn.className = 'igv-track-label-popover__close';
        closeBtn.setAttribute('tabindex', '0');
        closeBtn.setAttribute('aria-label', 'Close');
        closeBtn.appendChild(createIcon$1('times'));
        closeBtn.addEventListener('click', () => this.removeTrackLabelPopover());

        header.appendChild(titleDiv);
        header.appendChild(closeBtn);

        // Body
        const body = document.createElement('div');
        body.className = 'igv-track-label-popover__body';
        body.appendChild(content);

        // Assemble popover
        popover.appendChild(header);
        popover.appendChild(body);

        // Position popover near the track label
        const labelRect = this.trackLabelElement.getBoundingClientRect();
        const containerRect = this.browser.columnContainer.getBoundingClientRect();
        const offsetX = labelRect.left - containerRect.left;
        const offsetY = labelRect.bottom - containerRect.top + 5;

        popover.style.left = `${offsetX}px`;
        popover.style.top = `${offsetY}px`;
        popover.style.position = 'absolute';

        // Store reference for later removal
        this._trackLabelPopover = popover;

        // Add to DOM
        this.browser.columnContainer.appendChild(popover);

        makeDraggable$1(popover, header, {minX: 0, minY: 0});

        // Remove on outside click
        // setTimeout(() => {
        //     document.addEventListener('mousedown', this._trackLabelPopoverListener = (evt) => {
        //         if (!popover.contains(evt.target)) {
        //             this.removeTrackLabelPopover();
        //         }
        //     });
        // }, 0);
    }

    removeTrackLabelPopover() {
        if (this._trackLabelPopover) {
            this._trackLabelPopover.remove();
            this._trackLabelPopover = null;
            if (this._trackLabelPopoverListener) {
                document.removeEventListener('mousedown', this._trackLabelPopoverListener);
                this._trackLabelPopoverListener = null;
            }
        }
    }

    createClickState(event) {

        if (!this.canvas) return  // Can happen during initialization

        const referenceFrame = this.referenceFrame;
        const viewportCoords = translateMouseCoordinates(event, this.viewportElement);
        const canvasCoords = translateMouseCoordinates(event, this.canvas);
        const genomicLocation = (((referenceFrame.start) + referenceFrame.toBP(viewportCoords.x)));

        return {
            event,
            viewport: this,
            referenceFrame,
            genomicLocation,
            y: viewportCoords.y + this.contentTop,
            canvasX: canvasCoords.x,
            canvasY: canvasCoords.y
        }

    }

    getPopupContent(event) {

        const clickState = this.createClickState(event);

        if (undefined === clickState) {
            return
        }

        let track = this.trackView.track;
        const dataList = track.popupData(clickState);

        const popupClickHandlerResult = this.browser.fireEvent('trackclick', [track, dataList, clickState.genomicLocation]);

        let content;
        if (undefined === popupClickHandlerResult || true === popupClickHandlerResult) {
            // Indicates handler did not handle the result, or the handler wishes default behavior to occur
            if (dataList && dataList.length > 0) {
                content = formatPopoverText(dataList);
            }

        } else if (typeof popupClickHandlerResult === 'string') {
            content = popupClickHandlerResult;
        }

        return content
    }

    dispose() {

        if (this.popover) {
            this.popover.dispose();
        }

        super.dispose();
    }

}

function formatPopoverText(nameValues) {

    const rows = nameValues.map(nameValue => {

        if (nameValue.name) {
            const str = `<span>${nameValue.name}</span>&nbsp&nbsp&nbsp${nameValue.value}`;
            return `<div title="${nameValue.value}">${str}</div>`
        } else if ('<hr>' === nameValue) { // this can be retired if nameValue.html is allowed.
            return nameValue
        } else if (nameValue.html) {
            return nameValue.html
        } else {
            return `<div title="${nameValue}">${nameValue}</div>`
        }

    });

    return rows.join('')
}

class FeatureCache {

    constructor(chr, tileStart, tileEnd, bpPerPixel, features, roiFeatures, multiresolution, windowFunction) {
        this.chr = chr;
        this.bpStart = tileStart;
        this.bpEnd = tileEnd;
        this.bpPerPixel = bpPerPixel;
        this.features = features;
        this.roiFeatures = roiFeatures;
        this.multiresolution = multiresolution;
        this.windowFunction = windowFunction;
    }

    containsRange(chr, start, end, bpPerPixel, windowFunction) {

        if (windowFunction && windowFunction !== this.windowFunction) return false

        // For multi-resolution tracks allow for a 2X change in bpPerPixel
        const r = this.multiresolution ? this.bpPerPixel / bpPerPixel : 1;

        return start >= this.bpStart && end <= this.bpEnd && chr === this.chr && r > 0.5 && r < 2
    }

    overlapsRange(chr, start, end) {
        return this.chr === chr && end >= this.bpStart && start <= this.bpEnd
    }
}

const appleCrayonColorName = 'nickel';

const ROI_DEFAULT_ALPHA = 1 / 16;

const ROI_DEFAULT_COLOR = appleCrayonRGBA(appleCrayonColorName, ROI_DEFAULT_ALPHA);
const ROI_DEFAULT_HEADER_COLOR = 'rgb(190,190,190)';

const ROI_USER_HEADER_DEFINED_COLOR = 'rgba(155,185,129)';
const ROI_USER_DEFINED_COLOR = ROI_DEFAULT_COLOR;

class ROISet {

    constructor(config, genome) {

        this.url = config.url;

        if (config.name) {
            this.name = config.name;
        }

        this.isUserDefined = config.isUserDefined;

        if (config.featureSource) {
            // This is unusual, but permitted
            this.featureSource = config.featureSource;
        } else if (config.features) {
            this.featureSource = new DynamicFeatureSource(config.features, genome);
        } else if (config.format) {
            this.featureSource = FeatureSource(config, genome);
        } else {
            throw Error('ROI configuration must define either features or file format')
        }

        if(config.color && !config.color.startsWith("rgba")) {
            config.color = IGVColor.addAlpha(config.color, ROI_DEFAULT_ALPHA);
        }

        if (true === this.isUserDefined) {
            this.color = config.color || ROI_USER_DEFINED_COLOR;
            this.headerColor = ROI_USER_HEADER_DEFINED_COLOR;

        } else {
            this.color = config.color || ROI_DEFAULT_COLOR;
            this.headerColor = ROI_DEFAULT_HEADER_COLOR;

            // Use body color with alpha pinned to 1
             const [ r, g, b, discard ] = rgbaStringTokens(this.color);
             this.headerColor = `rgba(${ r },${ g },${ b },${ 1.0 })`;
        }

        delete config.isVisible;  // Deprecated

    }

    async getFeatures(chr, start, end) {
        return this.featureSource.getFeatures({chr, start, end})
    }

    async getAllFeatures() {
        return typeof this.featureSource.getAllFeatures === 'function' ? await this.featureSource.getAllFeatures() : {}
    }

    addFeature(feature) {
        this.featureSource.addFeature(feature);
    }

    removeFeature(feature) {
        this.featureSource.removeFeature(feature);
    }

    toJSON() {
        if (this.url) {
            return {
                name: this.name,
                color: this.color,
                url: this.url,
                isUserDefined: this.isUserDefined,
                isVisible: this.isVisible
            }
        } else {
            const featureMap = this.featureSource.getAllFeatures();
            const features = [];
            for (let chr of Object.keys(featureMap)) {
                for (let f of featureMap[chr]) {
                    features.push(f);
                }
            }
            return {
                name: this.name,
                color: this.color,
                features: features,
                isUserDefined: this.isUserDefined,
                isVisible: this.isVisible
            }
        }
    }

    dispose() {
        for (let key of Object.keys(this)) {
            this[key] = undefined;
        }
    }

}

const SCREEN_COORDS_WIDTH_THRESHOLD = 3;

function screenCoordinates(regionStartBP, regionEndBP, bpStart, bpp) {

    let xStart = Math.round((regionStartBP - bpStart) / bpp);
    const xEnd = Math.round((regionEndBP - bpStart) / bpp);

    let width = xEnd - xStart;

    if (width < SCREEN_COORDS_WIDTH_THRESHOLD) {
        width = SCREEN_COORDS_WIDTH_THRESHOLD;
        xStart -= 1;
    }

    return {x: xStart, width}
}


/**
 * Special feature source that allows addition of features dynamically
 */
class DynamicFeatureSource {

    constructor(features, genome) {
        this.featureMap = {};
        this.genome = genome;

        for (let feature of features) {

            // Store as canonical chr name (i.e. translate aliases)
            const chrKey = genome ? genome.getChromosomeName(feature.chr) : feature.chr;

            let featureList = this.featureMap[chrKey];
            if (!featureList) {
                featureList = [];
                this.featureMap[chrKey] = featureList;
            }
            featureList.push(feature);
        }

        for (let key of Object.keys(this.featureMap)) {
            this.featureMap[key].sort((a, b) => a.start - b.start);
        }
    }

    async getFeatures({chr, start, end}) {
        if (chr.toLowerCase() === 'all') {
            return await computeWGFeatures(this.featureMap, this.genome)
        } else {
            // TODO -- this use of filter is O(N), and might not scale well for large feature lists.
            const featureList = this.featureMap[chr];
            return featureList ? featureList.filter(feature => feature.end > start && feature.start < end) : []
        }
    }

    getAllFeatures() {
        return this.featureMap
    }

    supportsWholeGenome() {
        return true
    }

    addFeature(feature) {
        let featureList = this.featureMap[feature.chr];
        if (!featureList) {
            featureList = [];
            this.featureMap[feature.chr] = featureList;
        }
        featureList.push(feature);
        featureList.sort((a, b) => a.start - b.start);
    }

    removeFeature({chr, start, end}) {

        if (this.featureMap[chr]) {
            const match = `${chr}-${start}-${end}`;
            this.featureMap[chr] = this.featureMap[chr].filter(feature => match !== `${feature.chr}-${feature.start}-${feature.end}`);
            // Check if featureMap for a specific chromosome is empty now and delete it if yes
            if (this.featureMap[chr].length === 0) {
                delete this.featureMap[chr];
            }
        }
    }
}

const RULER_SWEEPER_COLOR = 'rgba(68, 134, 247, 0.25)';

class RulerSweeper {

    constructor(rulerViewport, column, browser, referenceFrame) {

        this.rulerViewport = rulerViewport;

        this.rulerSweeper = div$2({class: 'igv-ruler-sweeper'});
        column.appendChild(this.rulerSweeper);

        this.browser = browser;
        this.referenceFrame = referenceFrame;

        this.isMouseHandlers = undefined;

        this.addBrowserObserver();
    }

    addBrowserObserver() {

        const observerHandler = () => {
            if (this.referenceFrame) {
                GenomeUtils.isWholeGenomeView(this.referenceFrame.chr) ? this.removeMouseHandlers() : this.addMouseHandlers();
            }
        };

        // Viewport Content
        this.boundObserverHandler = observerHandler.bind(this);
        this.browser.on('locuschange', this.boundObserverHandler);

    }

    removeBrowserObserver() {
        this.browser.off('locuschange', this.boundObserverHandler);
    }

    addMouseHandlers() {

        if (true === this.isMouseHandlers) {
            return
        }

        const threshold = 1;

        let isMouseDown;
        let isMouseIn;
        let mouseDownX;
        let left;
        let width;
        let dx;

        // Viewport Content
        this.boundContentMouseDownHandler = contentMouseDownHandler.bind(this);
        this.rulerViewport.contentDiv.addEventListener('mousedown', this.boundContentMouseDownHandler);

        function contentMouseDownHandler(event) {

            isMouseDown = true;
            isMouseIn = true;

            const {x} = translateMouseCoordinates(event, this.rulerViewport.contentDiv);
            left = mouseDownX = x;

            width = threshold;


            this.rulerSweeper.style.display = 'block';
            this.rulerSweeper.style.backgroundColor = true === event.shiftKey ? ROI_USER_DEFINED_COLOR : RULER_SWEEPER_COLOR;

            this.rulerSweeper.style.left = `${left}px`;
            this.rulerSweeper.style.width = `${width}px`;

        }

        // Document
        this.boundDocumentMouseMoveHandler = documentMouseMoveHandler.bind(this);
        document.addEventListener('mousemove', this.boundDocumentMouseMoveHandler);

        function documentMouseMoveHandler(event) {

            let mouseCurrentX;

            if (isMouseDown && isMouseIn) {

                const {x} = translateMouseCoordinates(event, this.rulerViewport.contentDiv);
                mouseCurrentX = Math.max(Math.min(x, this.rulerViewport.contentDiv.clientWidth), 0);

                dx = mouseCurrentX - mouseDownX;

                width = Math.abs(dx);
                this.rulerSweeper.style.width = `${width}px`;

                if (dx < 0) {
                    left = mouseDownX + dx;
                    this.rulerSweeper.style.left = `${left}px`;
                }

            }

        }

        this.boundDocumentMouseUpHandler = documentMouseUpHandler.bind(this);
        document.addEventListener('mouseup', this.boundDocumentMouseUpHandler);

        function documentMouseUpHandler(event) {

            let genomicExtent;

            if (true === isMouseDown && true === isMouseIn) {

                isMouseDown = isMouseIn = undefined;

                this.rulerSweeper.style.display = 'none';

                if (width > threshold) {

                    genomicExtent =
                        {
                            start: Math.floor(this.referenceFrame.calculateEnd(left)),
                            end: Math.floor(this.referenceFrame.calculateEnd(left + width)),
                        };


                    const shiftKeyPressed = event.shiftKey;

                    if (true === shiftKeyPressed) {
                        this.browser.roiManager.updateUserDefinedROISet(Object.assign({chr: this.referenceFrame.chr}, genomicExtent));
                    } else {

                        validateGenomicExtent(this.browser.genome.getChromosome(this.referenceFrame.chr).bpLength, genomicExtent, this.browser.minimumBases());
                        updateReferenceFrame(this.referenceFrame, genomicExtent, this.rulerViewport.contentDiv.clientWidth);
                        this.browser.updateViews(this.referenceFrame);

                    }

                }

            }

        }

        this.isMouseHandlers = true;
    }

    removeMouseHandlers() {
        this.rulerViewport.contentDiv.removeEventListener('mousedown', this.boundContentMouseDownHandler);
        document.removeEventListener('mousemove', this.boundDocumentMouseMoveHandler);
        document.removeEventListener('mouseup', this.boundDocumentMouseUpHandler);
        this.isMouseHandlers = false;
    }

    dispose() {
        this.removeBrowserObserver();
        this.removeMouseHandlers();
        this.rulerSweeper.remove();
    }

}

function updateReferenceFrame(referenceFrame, genomicExtent, pixelWidth) {
    referenceFrame.start = Math.round(genomicExtent.start);
    referenceFrame.end = Math.round(genomicExtent.end);
    referenceFrame.bpPerPixel = (referenceFrame.end - referenceFrame.start) / pixelWidth;
}

function getChrColor$1(chr) {
    if (chrColorMap$1[chr]) {
        return chrColorMap$1[chr]
    } else if (chrColorMap$1["chr" + chr]) {
        const color = chrColorMap$1["chr" + chr];
        chrColorMap$1[chr] = color;
        return color
    } else {
        const color = IGVColor.randomRGB(0, 255);
        chrColorMap$1[chr] = color;
        return color
    }
}

const chrColorMap$1 = {
    "chrX": "rgb(204, 153, 0)",
    "chrY": "rgb(153, 204, 0)",
    "chrUn": "rgb(50, 50, 50)",
    "chr1": "rgb(80, 80, 255)",
    "chrI": "rgb(139, 155, 187)",
    "chr2": "rgb(206, 61, 50)",
    "chrII": "rgb(206, 61, 50)",
    "chr2a": "rgb(216, 71, 60)",
    "chr2b": "rgb(226, 81, 70)",
    "chr3": "rgb(116, 155, 88)",
    "chrIII": "rgb(116, 155, 88)",
    "chr4": "rgb(240, 230, 133)",
    "chrIV": "rgb(240, 230, 133)",
    "chr5": "rgb(70, 105, 131)",
    "chr6": "rgb(186, 99, 56)",
    "chr7": "rgb(93, 177, 221)",
    "chr8": "rgb(128, 34, 104)",
    "chr9": "rgb(107, 215, 107)",
    "chr10": "rgb(213, 149, 167)",
    "chr11": "rgb(146, 72, 34)",
    "chr12": "rgb(131, 123, 141)",
    "chr13": "rgb(199, 81, 39)",
    "chr14": "rgb(213, 143, 92)",
    "chr15": "rgb(122, 101, 165)",
    "chr16": "rgb(228, 175, 105)",
    "chr17": "rgb(59, 27, 83)",
    "chr18": "rgb(205, 222, 183)",
    "chr19": "rgb(97, 42, 121)",
    "chr20": "rgb(174, 31, 99)",
    "chr21": "rgb(231, 199, 111)",
    "chr22": "rgb(90, 101, 94)",
    "chr23": "rgb(204, 153, 0)",
    "chr24": "rgb(153, 204, 0)",
    "chr25": "rgb(51, 204, 0)",
    "chr26": "rgb(0, 204, 51)",
    "chr27": "rgb(0, 204, 153)",
    "chr28": "rgb(0, 153, 204)",
    "chr29": "rgb(10, 71, 255)",
    "chr30": "rgb(71, 117, 255)",
    "chr31": "rgb(255, 194, 10)",
    "chr32": "rgb(255, 209, 71)",
    "chr33": "rgb(153, 0, 51)",
    "chr34": "rgb(153, 26, 0)",
    "chr35": "rgb(153, 102, 0)",
    "chr36": "rgb(128, 153, 0)",
    "chr37": "rgb(51, 153, 0)",
    "chr38": "rgb(0, 153, 26)",
    "chr39": "rgb(0, 153, 102)",
    "chr40": "rgb(0, 128, 153)",
    "chr41": "rgb(0, 51, 153)",
    "chr42": "rgb(26, 0, 153)",
    "chr43": "rgb(102, 0, 153)",
    "chr44": "rgb(153, 0, 128)",
    "chr45": "rgb(214, 0, 71)",
    "chr46": "rgb(255, 20, 99)",
    "chr47": "rgb(0, 214, 143)",
    "chr48": "rgb(20, 255, 177)",
};

let timer$1;
let currentViewport = undefined;
const toolTipTimeout$1 = 1e4;

class RulerViewport extends TrackViewport {

    constructor(trackView, $viewportColumn, referenceFrame, width) {
        super(trackView, $viewportColumn, referenceFrame, width);
    }

    get contentDiv() {
        return this.viewportElement
    }

    initializationHelper() {
        // Create the multi-locus close button
        this.multiLocusCloseButton = document.createElement('div');
        this.multiLocusCloseButton.className = 'igv-multi-locus-close-button';
        this.viewportElement.appendChild(this.multiLocusCloseButton);

        const closeIcon = createIcon$1("times-circle");
        this.multiLocusCloseButton.appendChild(closeIcon);

        this.multiLocusCloseButton.addEventListener('click', () => {
            this.browser.removeMultiLocusPanel(this.referenceFrame);
        });

        // Create the ruler label
        this.rulerLabel = document.createElement('div');
        this.rulerLabel.className = 'igv-multi-locus-ruler-label';
        this.viewportElement.appendChild(this.rulerLabel);

        const rulerLabelDiv = document.createElement('div');
        this.rulerLabel.appendChild(rulerLabelDiv);

        this.rulerLabel.addEventListener('click', async (event) => {
            event.stopPropagation();
            await this.browser.gotoMultilocusPanel(this.referenceFrame);
        });

        // Create the tooltip
        this.tooltip = document.createElement('div');
        this.tooltip.className = 'igv-ruler-tooltip';
        this.tooltip.style.height = `${this.viewportElement.clientHeight}px`;
        this.viewportElement.appendChild(this.tooltip);

        this.tooltipContent = document.createElement('div');
        this.tooltip.appendChild(this.tooltipContent);

        // Instantiate RulerSweeper
        this.rulerSweeper = new RulerSweeper(this, this.viewportElement.parentElement, this.browser, this.referenceFrame);

        // Attach mouse handlers
        this.attachMouseHandlers(GenomeUtils.isWholeGenomeView(this.referenceFrame.chr));

        // Initially hide the tooltip
        this.tooltip.style.display = 'none';

        // Dismiss locus label
        this.dismissLocusLabel();
    }

    presentLocusLabel(viewportWidth) {
        this.multiLocusCloseButton.style.display = 'block';

        this.rulerLabel.style.display = 'block';
        this.rulerLabel.style.backgroundColor = getChrColor$1(this.referenceFrame.chr);

        const textDiv = this.rulerLabel.querySelector('div');

        const { width } = this.rulerLabel.getBoundingClientRect();

        textDiv.innerHTML = `${this.referenceFrame.getMultiLocusLabel(viewportWidth)}`;
        const { width: textDivWidth } = textDiv.getBoundingClientRect();

        if (textDivWidth / width > 0.5) {
            textDiv.innerHTML = `${this.referenceFrame.getMultiLocusLabelBPLengthOnly(viewportWidth)}`;
        }

        //console.log(`${Date.now()} textDiv ${StringUtils.numberFormatter(Math.floor(textDivWidth))}`);
    }

// Use in conjunction with .igv-multi-locus-ruler-label-square-dot css class (_dom-misc.scss)
    dismissLocusLabel() {
        this.rulerLabel.style.display = 'none';
        this.multiLocusCloseButton.style.display = 'none';
    }

    attachMouseHandlers(isWholeGenomeView) {

        if (true === isWholeGenomeView) {

            this.viewportElement.addEventListener('click', (e) => {

                const {x: pixel} = translateMouseCoordinates(e, this.viewportElement);
                const bp = Math.round(this.referenceFrame.start + this.referenceFrame.toBP(pixel));

                let searchString;

                const {chr} = this.browser.genome.getChromosomeCoordinate(bp);

                if (1 === this.browser.referenceFrameList.length) {
                    searchString = chr;
                } else {
                    const index = this.browser.referenceFrameList.indexOf(this.referenceFrame);
                    const loci = this.browser.referenceFrameList.map(({locusSearchString}) => locusSearchString);
                    loci[index] = chr;
                    searchString = loci.join(' ');
                }

                this.browser.search(searchString);

            });

            this.viewportElement.style.cursor = 'pointer';
        } else {
            this.viewportElement.style.cursor = 'default';
        }

    }

    mouseMove(event) {
        if (this.browser.doShowCursorGuide) {
            if (currentViewport === undefined) {
                currentViewport = this;
                this.tooltip.style.display = "block";
            } else if (currentViewport.guid !== this.guid) {
                if (currentViewport.tooltip) {
                    currentViewport.tooltip.style.display = "none";
                }
                this.tooltip.style.display = "block";
                currentViewport = this;
            } else {
                this.tooltip.style.display = "block";
            }

            const isWholeGenome = (
                this.browser.isMultiLocusWholeGenomeView() ||
                GenomeUtils.isWholeGenomeView(this.referenceFrame.chr)
            );

            if (isWholeGenome) {
                this.tooltip.style.display = "none";
                return undefined;
            }

            const { x } = translateMouseCoordinates(event, this.viewportElement);
            const { start, end, bpPerPixel } = this.referenceFrame;
            const bp = Math.round(0.5 + start + Math.max(0, x) * bpPerPixel);

            this.tooltipContent.textContent = numberFormatter$1(bp);

            const tooltipRect = this.tooltipContent.getBoundingClientRect();
            const viewportRect = this.viewportElement.getBoundingClientRect();

            const tooltipLeft = IGVMath.clamp(x, 0, viewportRect.width - tooltipRect.width);
            this.tooltip.style.left = `${tooltipLeft}px`;

            // hide tooltip when movement stops
            clearTimeout(timer$1);
            timer$1 = setTimeout(() => {
                if (this.tooltip) this.tooltip.style.display = "none";
            }, toolTipTimeout$1);

            return { start, bp, end };
        }
    }

    startSpinner() {
    }

    stopSpinner() {
    }

    dispose() {
        this.rulerSweeper.dispose();
        super.dispose();
    }

}

let timer;
const toolTipTimeout = 1e4;

class IdeogramViewport extends TrackViewport {

    featureCache = new IdeogramFeatureCache()

    constructor(trackView, viewportColumn, referenceFrame, width) {
        super(trackView, viewportColumn, referenceFrame, width);
    }

    initializationHelper() {

        this.canvas = document.createElement('canvas');

        this.canvas.className = 'igv-ideogram-canvas';
        this.viewportElement.appendChild(this.canvas);
        this.ideogram_ctx = this.canvas.getContext('2d');

        // Create the tooltip
        this.tooltip = document.createElement('div');
        this.tooltip.className = 'igv-cytoband-tooltip';
        this.tooltip.style.height = `${this.viewportElement.clientHeight}px`;
        this.viewportElement.appendChild(this.tooltip);

        // Add tooltip for cytoband names
        this.tooltipContent = document.createElement('div');
        this.tooltip.appendChild(this.tooltipContent);

        // Initially hide the tooltip
        this.tooltip.style.display = 'none';

        this.addMouseHandlers();
    }

    async getFeatures(chr, start, end, bpPerPixel) {
        if (this.featureCache.containsRange(chr)) {
            return this.featureCache.get(chr)
        } else {
          return this.loadFeatures()
        }
    }

    async loadFeatures() {
        const chr = this.referenceFrame.chr;
        const features = await  this.referenceFrame.genome.getCytobands(chr);
        this.featureCache.set(chr, features);
        return features
    }

    repaint() {

        if (undefined === this.featureCache) {
            return
        }

        const {width, height} = this.viewportElement.getBoundingClientRect();
        IGVGraphics.configureHighDPICanvas(this.ideogram_ctx, width, height);

        const chr = this.referenceFrame.chr;
        const features = this.featureCache.get(chr);

        const config =
            {
                context: this.ideogram_ctx,
                pixelWidth: width,
                pixelHeight: height,
                referenceFrame: this.referenceFrame,
                features
            };

        this.trackView.track.draw(config);

    }


    addMouseHandlers() {
        this.addViewportClickHandler(this.viewportElement);

        // Add tooltip when showing contig name
        if (this.trackView.track.showCytobandNames) {
            this.viewportElement.addEventListener('mousemove', this.mouseMove.bind(this));
            this.viewportElement.addEventListener('mouseleave', this.mouseLeave.bind(this));
        }
    }

    mouseMove(event) {
        const {x} = translateMouseCoordinates(event, this.viewportElement);

        // Get features
        const features = this.featureCache.get(this.referenceFrame.chr);
        if (features) {
            const {width: w} = this.viewportElement.getBoundingClientRect();

            const chrLength = features[features.length - 1].end;
            const scale = w / chrLength;

            let found = false;
            // Find cytoband that the mouse is over
            for (let i = 0; i < features.length; i++) {
                const cytoband = features[i];
                const start = cytoband.start * scale;
                const end = cytoband.end * scale;

                // If the mouse is over the cytoband, show the tooltip
                if (x >= start && x <= end) {
                    this.tooltipContent.textContent = cytoband.name;
                    const {width: ww} = this.tooltipContent.getBoundingClientRect();
                    let center = (start + end) / 2 - ww / 2;

                    const tooltipLeft = IGVMath.clamp(center, 0, w - ww);
                    this.tooltip.style.left = `${tooltipLeft}px`;

                    // hide tooltip when movement stops
                    clearTimeout(timer);
                    timer = setTimeout(() => {
                        if (this.tooltip) this.tooltip.style.display = "none";
                    }, toolTipTimeout);

                    this.tooltip.style.display = "block";

                    found = true;
                    break
                }
            }
            if (found)
                return;
        }

        // If the mouse is not over a cytoband, or there are no features, hide the tooltip
        this.tooltip.style.display = 'none';
    }

    mouseLeave(event) {
        this.tooltip.style.display = 'none';
    }

    addViewportClickHandler(viewport) {

        this.boundClickHandler = clickHandler.bind(this);
        viewport.addEventListener('click', this.boundClickHandler);

        function clickHandler(event) {

            const {xNormalized, width} = translateMouseCoordinates(event, this.ideogram_ctx.canvas);
            const {bpLength} = this.browser.genome.getChromosome(this.referenceFrame.chr);
            const locusLength = this.referenceFrame.bpPerPixel * width;
            const chrCoveragePercentage = locusLength / bpLength;

            let xPercentage = xNormalized;
            if (xPercentage - (chrCoveragePercentage / 2.0) < 0) {
                xPercentage = chrCoveragePercentage / 2.0;
            }

            if (xPercentage + (chrCoveragePercentage / 2.0) > 1.0) {
                xPercentage = 1.0 - chrCoveragePercentage / 2.0;
            }

            const ss = Math.round((xPercentage - (chrCoveragePercentage / 2.0)) * bpLength);
            const ee = Math.round((xPercentage + (chrCoveragePercentage / 2.0)) * bpLength);

            this.referenceFrame.start = ss;
            this.referenceFrame.end = ee;
            this.referenceFrame.bpPerPixel = (ee - ss) / width;

            this.browser.updateViews(this.referenceFrame, this.browser.trackViews, true);

        }

    }

    setWidth(width) {
        this.viewportElement.style.width = width + 'px';
    }

    renderSVGContext(context, {deltaX, deltaY}, includeLabel = true) {

        const {width, height} = this.viewportElement.getBoundingClientRect();

        const str = 'ideogram';
        const index = this.browser.referenceFrameList.indexOf(this.referenceFrame);
        const id = `${str}_referenceFrame_${index}_guid_${guid$2()}`;

        const x = deltaX;
        const y = this.contentTop + deltaY;
        const yClipOffset = -this.contentTop;

        context.saveWithTranslationAndClipRect(id, x, y, width, height, yClipOffset);
        this.trackView.track.draw({
            context,
            pixelWidth: width,
            pixelHeight: height,
            referenceFrame: this.referenceFrame,
            features: this.featureCache.get(this.referenceFrame.chr)
        });
        context.restore();

    }


    startSpinner() {
    }

    stopSpinner() {
    }
}

/**
 * A very simple feature cache.  The smallest chunk of features for ideograms is a whole chromosome
 */
class IdeogramFeatureCache {
    features = new Map()

    containsRange(chr) {
        return this.features.has(chr)
    }

    set(chr, features) {
        this.features.set(chr, features);
    }

    get(chr) {
        return this.features.get(chr)
    }
}

function createViewport(trackView, column, referenceFrame, width) {

    if ('ruler' === trackView.track.type) {
        return new RulerViewport(trackView, column, referenceFrame, width)
    } else if ('ideogram' === trackView.track.id) {
        return new IdeogramViewport(trackView, column, referenceFrame, width)
    } else {
        const viewportObject = new TrackViewport(trackView, column, referenceFrame, width);
        referenceFrame.viewport = viewportObject;
        return viewportObject
    }
}

// https://sashamaps.net/docs/resources/20-colors/

const _18_DistinctColorsViaChatGPT4 = [
    "rgb(255, 0, 0)",     // Red
    "rgb(0, 255, 0)",     // Green
    "rgb(0, 0, 255)",     // Blue
    "rgb(255, 0, 255)",   // Magenta
    "rgb(0, 255, 255)",   // Cyan
    "rgb(128, 0, 128)",   // Purple
    "rgb(255, 165, 0)",   // Orange
    "rgb(255, 105, 180)", // Hot Pink
    "rgb(255, 127, 80)",  // Coral
    "rgb(220, 20, 60)",   // Crimson
    "rgb(255, 99, 71)",   // Tomato
    "rgb(173, 216, 230)", // Light Blue
    "rgb(144, 238, 144)", // Light Green
    "rgb(224, 255, 255)", // Light Cyan
    "rgb(250, 250, 210)", // Light Goldenrod
    "rgb(152, 251, 152)", // Pale Green
    "rgb(70, 130, 180)",  // Steel Blue
    "rgb(102, 205, 170)"  // Medium Aquamarine
];

const distinctColorsPalette = _18_DistinctColorsViaChatGPT4.map(str => {
        const [ _0, g, _1] = str.split(',');
        const [ _2, r ] = _0.split('(');
        const [ b, _3 ] = _1.split(')');
        return [ r, g, b ]
});

class SampleInfo {

    static emptySpaceReplacement = '|'
    static colorForNA = appleCrayonRGB('magnesium')
    static sampleInfoFileHeaders = ['#sampleTable', '#sampleMapping', '#colors']

    constructor(browser) {
        const found = browser.tracks.some(t => typeof t.getSamples === 'function');
        if (found.length > 0) {
            browser.sampleInfoControl.setButtonVisibility(true);
        }
        this.initialize();
    }

    initialize() {
        this.sampleInfoFiles = [];
        this.attributeNames = [];
        this.sampleDictionary = {};
        this.sampleMappingDictionary = {};
        this.colorDictionary = {};
        this.attributeRangeLUT = {};
        this.initialized = false;
    }

    get attributeCount() {
        return this.attributeNames ? this.attributeNames.length : 0
    }

    isInitialized() {
        return this.initialized
    }

    hasAttributes() {
        return this.attributeCount > 0
    }

    getAttributes(sampleName) {

        const key = this.sampleMappingDictionary[sampleName] || sampleName;
        return this.sampleDictionary[key]
    }

    getAttributeValue(sampleName, attribute) {
        const attributes = this.getAttributes(sampleName);
        return attributes ? attributes[attribute] : undefined
    }

    async loadSampleInfo(config) {

        if (config.url) {
            await this.loadSampleInfoFile(config.url);
        } else {

            const samples = {...config};
            for (const [key, record] of Object.entries(samples)) {
                samples[key] = SampleInfo.toNumericalRepresentation(record);
            }

            const [value] = Object.values(samples);
            const attributes = Object.keys(value);

            this.loadSampleInfoHelper(attributes, samples);

        }

        this.initialized = true;
    }

    loadSampleInfoHelper(attributes, samples) {

        // Establish the range of values for each attribute
        const lut = createAttributeRangeLUT(attributes, samples);
        accumulateDictionary(this.attributeRangeLUT, lut);

        // Ensure unique attribute names list
        const currentAttributeNameSet = new Set(this.attributeNames);
        for (const name of attributes) {
            if (!currentAttributeNameSet.has(name)) {
                this.attributeNames.push(name);
            }
        }

        accumulateDictionary(this.sampleDictionary, samples);

    }

    async loadSampleInfoFile(path) {
        const string = await igvxhr$1.loadString(path);
        this.#processSampleInfoFileAsString(string);
        this.sampleInfoFiles.push(path);
    }

    discard() {
        this.initialize();
    }

    getAttributeColor(attribute, value) {

        let color;

        if ('-' === value) {

            color = appleCrayonRGB('snow');

        } else if (typeof value === "string" && this.colorDictionary[value]) {

            color = this.colorDictionary[value]();

        } else if (this.colorDictionary[attribute]) {

            color = this.colorDictionary[attribute](value);

        } else if (typeof value === "string") {

            color = 'NA' === value ? SampleInfo.colorForNA : SampleInfo.stringToRGBString(value);

        } else {

            // if ('%|Tumor|Nuclei' === attribute) {
            //     console.log(`${ attribute } : ${ value }`)
            // }

            const [min, max] = this.attributeRangeLUT[attribute];

            const lowerAlphaThreshold = 2e-1;
            const alpha = Math.max((value - min) / (max - min), lowerAlphaThreshold);

            const [r, g, b] = distinctColorsPalette[Object.keys(this.attributeRangeLUT).indexOf(attribute)];
            color = `rgba(${r},${g},${b},${alpha})`;

        }

        return color

    }

    sortSampleKeysByAttribute(sampleKeys, attribute, sortDirection) {
        const numbers = sampleKeys.filter(key => {
            const attributes = this.getAttributes(key);
            if (undefined === attributes) {
                return false
            }
            const value = attributes[attribute];
            return typeof value === 'number'
        });

        const strings = sampleKeys.filter(key => {
            const attributes = this.getAttributes(key);
            if (undefined === attributes) {
                return false
            }
            const value = attributes[attribute];
            return typeof value === 'string'
        });

        const compare = (a, b) => {
            const aa = this.getAttributes(a)[attribute];
            const bb = this.getAttributes(b)[attribute];

            if (typeof aa === 'string' && typeof bb === 'string') {
                return sortDirection * aa.localeCompare(bb)
            }

            if (typeof aa === 'number' && typeof bb === 'number') {
                return sortDirection * (aa - bb)
            }
        };

        numbers.sort(compare);
        strings.sort(compare);

        return sortDirection === -1 ? [...numbers, ...strings] : [...strings, ...numbers]
    }

    #processSampleInfoFileAsString(string) {

        const sectionDictionary = createSectionDictionary(string);

        for (const [header, value] of Object.entries(sectionDictionary)) {
            switch (header) {
                case '#sampleTable':
                    this.#accumulateSampleTableDictionary(value);
                    break
                case '#sampleMapping':
                    this.#accumulateSampleMappingDictionary(value);
                    break
                case '#colors':
                    this.#accumulateColorScheme(value);
                    break

            }
        }

    }

    #accumulateSampleTableDictionary(lines) {

        // shift array with first item that is 'sample' or 'Linking_id'. Remaining items are attribute names
        const scratch = lines.shift().split('\t').filter(line => line.length > 0);

        // discard 'sample' or 'Linking_id'
        scratch.shift();

        const attributes = scratch.map(label => label.split(' ').join(SampleInfo.emptySpaceReplacement));

        const cooked = lines.filter(line => line.length > 0);

        let samples;
        for (const line of cooked) {

            const record = line.split('\t');
            const _key_ = record.shift();

            if (undefined === samples) {
                samples = {};
            }

            samples[_key_] = {};

            for (let i = 0; i < record.length; i++) {
                const obj = {};

                if ("" === record[i]) {
                    obj[attributes[i]] = '-';
                } else {
                    obj[attributes[i]] = record[i];
                }

                Object.assign(samples[_key_], obj);
            }

        } // for (lines)

        for (const [key, record] of Object.entries(samples)) {
            samples[key] = SampleInfo.toNumericalRepresentation(record);
        }

        this.loadSampleInfoHelper(attributes, samples);

    }

    #accumulateSampleMappingDictionary(lines) {

        for (const line of lines) {
            const [key, value] = line.split('\t');
            this.sampleMappingDictionary[key] = value;
        }
    }

    #accumulateColorScheme(colorSettings) {

        const mappingfunction = (token, index, array) => {

            let result;
            switch (index) {
                case 0:
                    result = token.split(' ').join(SampleInfo.emptySpaceReplacement);
                    break
                case 1:
                    result = token.includes(':') ? token.split(':').map(str => parseFloat(str)) : token;
                    break
                case 2:
                    result = `rgb(${token})`;
                    break
                case 3:
                    result = `rgb(${token})`;
            }

            return result
        };

        const mappings = colorSettings.map(setting => {
            const list = setting.split('\t');
            const result = list.map(mappingfunction);
            return result
        });

        const triplets = mappings
            .filter(mapping => 3 === mapping.length && !mapping.includes('*'))
            .filter(([a, b, c]) => !Array.isArray(b));

        const tmp = {};
        for (const triplet of triplets) {
            const [attribute, value, rgb] = triplet;
            if (undefined === tmp[attribute]) {
                tmp[attribute] = {};
            }
            tmp[attribute][value.toUpperCase()] = rgb;
        }

        for (const [k, v] of Object.entries(tmp)) {
            const lut = Object.assign({}, v);
            this.colorDictionary[k] = attributeValue => {

                const key = attributeValue.toUpperCase();
                const color = lut[key] || appleCrayonRGB('snow');
                return color
            };
        }

        const clamped = mappings.filter(mapping => Array.isArray(mapping[1]));

        for (const cl of clamped) {
            const [a, b] = cl[1];
            const attribute = cl[0];

            if (3 === cl.length) {

                const [_r, _g, _b] = rgbStringTokens(cl[2]);

                this.colorDictionary[attribute] = attributeValue => {
                    attributeValue = IGVMath.clamp(attributeValue, a, b);
                    const interpolant = (attributeValue - a) / (b - a);
                    return rgbaColor(_r, _g, _b, interpolant)
                };

            } else if (4 === cl.length) {

                const [a, b] = cl[1];
                const [attribute, ignore, rgbA, rgbB] = cl;

                this.colorDictionary[attribute] = attributeValue => {
                    attributeValue = IGVMath.clamp(attributeValue, a, b);
                    const interpolant = (attributeValue - a) / (b - a);
                    return rgbStringHeatMapLerp(rgbA, rgbB, interpolant)
                };
            }
        }

        const wildCards = mappings.filter(mapping => 3 === mapping.length && mapping.includes('*'));

        for (const wildCard of wildCards) {

            if ('*' === wildCard[1]) {
                const [attribute, star, rgb] = wildCard;

                this.colorDictionary[attribute] = attributeValue => {

                    if ('NA' === attributeValue) {
                        return SampleInfo.colorForNA
                    } else {
                        const [min, max] = this.attributeRangeLUT[attribute];
                        const interpolant = (attributeValue - min) / (max - min);

                        const [r, g, b] = rgbStringTokens(rgb);
                        return rgbaColor(r, g, b, interpolant)
                    }

                };

            } else if ('*' === wildCard[0]) {
                const [star, attributeValue, rgb] = wildCard;
                this.colorDictionary[attributeValue] = () => rgb;
            }

        }

    }

    static toNumericalRepresentation(obj) {
        const result = Object.assign({}, obj);

        for (const [key, value] of Object.entries(result)) {
            if (typeof value === 'string' && !isNaN(value)) {
                result[key] = Number(value);
            }
        }

        return result
    }

    static stringToRGBString(str) {
        let hash = 0;
        for (let i = 0; i < str.length; i++) {
            hash = str.charCodeAt(i) + ((hash << 5) - hash);
        }

        let color = [];
        for (let i = 0; i < 3; i++) {
            const value = (hash >> (i * 8)) & 0xff;
            color.push(value);
        }

        return `rgb(${color.join(', ')})`
    }

    /**
     * Export the sample information as a JSON object.  This is provided for debugging purposes.
     */
    export() {
        const reverseSampleMappingDictionary = Object.fromEntries(
            Object.entries(this.sampleMappingDictionary).map(([key, value]) => [value, key])
        );
        for (const sampleName of Object.keys(this.sampleDictionary)) {
            reverseSampleMappingDictionary[sampleName] || sampleName;
            this.getAttributes(sampleName);
        }
    }

}

function createSectionDictionary(string) {

    const dictionary = {};

    const lines = string.split(/\r?\n|\r/).map(line => line.trim()).filter(line => '' !== line);

    let currentHeader;

    // If the first line does not start with a section header an initial #sampleTable is implied
    if (!SampleInfo.sampleInfoFileHeaders.includes(lines[0])) {
        currentHeader = '#sampleTable';
        dictionary[currentHeader] = [];
    }

    for (const line of lines) {

        if (SampleInfo.sampleInfoFileHeaders.includes(line)) {
            currentHeader = line;
            dictionary[currentHeader] = [];
        } else if (currentHeader && false === line.startsWith('#')) {
            dictionary[currentHeader].push(line);
        }
    }

    return dictionary
}

function accumulateDictionary(accumulator, dictionary) {
    for (const [key, value] of Object.entries(dictionary)) {
        if (!(key in accumulator) || accumulator[key] !== value) {
            accumulator[key] = value;
        }
    }
}

function createAttributeRangeLUT(names, dictionary) {

    const lut = {};
    for (const value of Object.values(dictionary)) {

        for (const attribute of names) {

            let item = value[attribute];

            if (undefined === lut[attribute]) {
                lut[attribute] = [];
            }

            lut[attribute].push(item);

        } // for (attributeNames)

    } // for (Object.values(sampleDictionary))

    // clean up oddball cases.
    const isNumber = element => typeof element === 'number';
    const isString = element => typeof element === 'string';

    // remove duplicates
    for (const key of Object.keys(lut)) {
        const multiples = lut[key];
        const set = new Set(multiples);
        const list = Array.from(set);

        if (true === list.some(isString) && true === list.some(isNumber)) {
            lut[key] = list.filter(item => !isString(item));
        } else {
            lut[key] = list;
        }

        if (!lut[key].some(isString)) {
            const clone = lut[key].slice();
            lut[key] = [Math.min(...clone), Math.max(...clone)];
        }

    }

    return lut
}

const sampleInfoTileXShim = 8;
const sampleInfoTileWidth = 16;
const defaultRulerHeight = 40;

class RulerTrack {
    constructor(browser) {

        this.browser = browser;
        this.height = defaultRulerHeight;
        this.name = "";
        this.disableButtons = true;
        this.ignoreTrackMenu = true;
        this.order = Number.MIN_SAFE_INTEGER * 1e-2;
        this.removable = false;
        this.type = 'ruler';
        this.id = "ruler";
    }

    async getFeatures(chr, start, end) {
        return []
    };

    computePixelHeight(ignore) {
        return this.height
    };

    draw({context, referenceFrame, pixelWidth, pixelHeight, bpPerPixel, bpStart}) {

        if (GenomeUtils.isWholeGenomeView(referenceFrame.chr)) {
            this.drawWholeGenome({context, pixelWidth, pixelHeight, bpPerPixel});
        } else {
            this.doDraw({context, referenceFrame, pixelWidth, pixelHeight, bpStart});
        }
    }

    drawWholeGenome({context, pixelWidth, pixelHeight, bpPerPixel}) {

        context.save();

        IGVGraphics.fillRect(context, 0, 0, pixelWidth, pixelHeight, {'fillStyle': 'white'});

        for (let name of this.browser.genome.wgChromosomeNames) {

            let xBP = this.browser.genome.getCumulativeOffset(name);
            let wBP = this.browser.genome.getChromosome(name).bpLength;

            let x = Math.round(xBP / bpPerPixel);
            let w = Math.round(wBP / bpPerPixel);

            this.renderChromosomeRect(context, x, 0, w, pixelHeight, name);
        }

        context.restore();

    }

    doDraw({context, referenceFrame, pixelWidth, pixelHeight, bpStart}) {

        context.clearRect(0, 0, pixelWidth, pixelHeight);

        const tickHeight = 6;
        const shim = 2;

        const bpLength = Math.floor(referenceFrame.toBP(pixelWidth));
        const tick = findSpacing(bpLength, context.isSVG);

        let nTick = Math.floor(bpStart / tick.majorTick) - 1;

        const {tickDelta, labelLength} = calculateDeltas(context, referenceFrame, bpStart, nTick, tick);

        this.browser.referenceFrameList.indexOf(referenceFrame);
        // console.log(`ruler(${ index }) label-length ${ labelLength > tickDelta ? 'clobbers' : 'less than' } tick-delta ${ StringUtils.numberFormatter(tickDelta)} `)

        let xTick;
        let bp;
        let accumulatedTickDelta = tickDelta;
        const labelLengthShim = 0.25 * labelLength;
        do {

            bp = Math.floor(nTick * tick.majorTick);
            const rulerLabel = `${numberFormatter$1(Math.floor(bp / tick.unitMultiplier))} ${tick.majorUnit}`;

            xTick = Math.round(referenceFrame.toPixels((bp - 1) - bpStart + 0.5));
            const xLabel = Math.round(xTick - context.measureText(rulerLabel).width / 2);

            if (xLabel > 0 && (labelLengthShim + labelLength) <= accumulatedTickDelta) {
                IGVGraphics.fillText(context, rulerLabel, xLabel, this.height - (tickHeight / 0.75));
                accumulatedTickDelta = 0;
            }

            if (xTick > 0) {
                IGVGraphics.strokeLine(context, xTick, this.height - tickHeight, xTick, this.height - shim);
            }

            bp = Math.floor((1 + nTick) * tick.majorTick);
            let pixel = Math.round(referenceFrame.toPixels((bp - 1) - bpStart + 0.5));
            let delta = (pixel - xTick) / 2;
            let xx = xTick + delta;
            if (xx > 0) {
                IGVGraphics.strokeLine(context, xx, this.height - tickHeight, xx, this.height - shim);
            }

            ++nTick;
            accumulatedTickDelta += tickDelta;

        } while (xTick < pixelWidth)

        IGVGraphics.strokeLine(context, 0, this.height - shim, pixelWidth, this.height - shim);

    }

    renderChromosomeRect(ctx, x, y, w, h, name) {

        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.font = '12px sans-serif';

        IGVGraphics.strokeLine(ctx, x + w, y, x + w, y + h, {strokeStyle: IGVColor.greyScale(191)});

        name = this.browser.genome.getChromosomeDisplayName(name);
        const shortName = (name.startsWith("chr")) ? name.substring(3) : name;

        if (w > ctx.measureText(shortName).width) {
            IGVGraphics.fillText(ctx, shortName, (x + (w / 2)), (y + (h / 2)), {fillStyle: IGVColor.greyScale(68)});
        }
    }

    get supportsWholeGenome() {
        return true
    };

    dispose() {
        // do stuff
    }
}

function findSpacing(bpLength, isSVG) {

    if (bpLength < 10) {
        return new Tick(1, 'bp', 1)
    }

    const nZeroes = Math.floor(Math.log10(bpLength));

    let majorUnit = 'bp';
    let unitMultiplier = 1;

    if (nZeroes > 9) {
        majorUnit = 'gb';
        unitMultiplier = 1e9;
    } else if (nZeroes > 6) {
        majorUnit = 'mb';
        unitMultiplier = 1e6;
    } else if (nZeroes > 3) {
        majorUnit = 'kb';
        unitMultiplier = 1e3;
    }

    const denom = Math.pow(10, nZeroes - 1);
    const nMajorTicks = bpLength / denom;

    // const threshold = 25
    const threshold = 3 * 25;

    const belowThresholdTick = Math.pow(10, nZeroes - 1);
    const aboveThresholdTick = Math.pow(10, nZeroes) / 2;

    // console.log(`zeros ${ nZeroes } tick-threshold ${ threshold } ticks ${ nMajorTicks } belowTick ${ StringUtils.numberFormatter(belowThresholdTick) } aboveTick ${ StringUtils.numberFormatter(aboveThresholdTick) }`)

    const majorTick = (nMajorTicks < threshold && isSVG !== true) ? belowThresholdTick : aboveThresholdTick;

    return new Tick(majorTick, majorUnit, unitMultiplier)
}

function calculateDeltas(context, referenceFrame, bpStart, nTick, tick) {

    const tickDelta = getX(referenceFrame, getBP(1 + nTick, tick), bpStart) - getX(referenceFrame, getBP(nTick, tick), bpStart);

    const label = `${numberFormatter$1(Math.floor(getBP(nTick, tick) / tick.unitMultiplier))} ${tick.majorUnit}`;
    const labelLength = Math.floor(context.measureText(label).width);

    return {tickDelta, labelLength}

    function getBP(nTick, tick) {
        return Math.floor(nTick * tick.majorTick)
    }

    function getX(referenceFrame, bp, bpStart) {
        return Math.round(referenceFrame.toPixels((bp - 1) - bpStart + 0.5))
    }
}

class Tick {

    constructor(majorTick, majorUnit, unitMultiplier) {
        this.majorTick = majorTick;
        this.minorTick = majorTick / 10.0;
        this.majorUnit = majorUnit;
        this.unitMultiplier = unitMultiplier;
    }

    description(blurb) {
    }
}

const GROUP_MARGIN_HEIGHT = 16;

function doSortByAttributes(sampleInfo, sampleKeys) {


        const attributeNameSet = new Set(sampleInfo.attributeNames);
        const anySampleKey = sampleKeys[0];
        const dictionary = sampleInfo.getAttributes(anySampleKey);

        if (undefined === dictionary) {
            return false
        } else {
            const sampleAttributeNames = Object.keys(sampleInfo.getAttributes(anySampleKey));
            for (const name of sampleAttributeNames) {
                if (false === attributeNameSet.has(name)) {
                    return false
                }
            }
        }

    return true
}

/**
 * Draw horizontal lines to separate groups of samples.
 * @param context
 * @param pixelTop - the vertical start position in pixels relative to track top in which to draw the dividers.   For deep tracks that are scrolled, this can be non-zero.
 * @param pixelWidth - viewport width in pixels
 * @param pixelHeight - the vertical extent of the track to draw in pixels.
 * @param offset - an offset in pixels from the top of the track to the top of the first sample.  Normally this is zero, but if there is content above the samples, this will be non-zero.
 * @param sampleHeight - height of each sample in pixels
 * @param groups - Map of group name to group object, where group object has a count property
 */
function drawGroupDividers(context, pixelTop, pixelWidth, pixelHeight, offset, sampleHeight, groups) {

    if (!groups || groups.size === 0) return

    const pixelBottom = pixelTop + pixelHeight;
    context.save();
    context.fillStyle = 'black';
    let y = offset + GROUP_MARGIN_HEIGHT / 2;
    if (y > pixelTop) {
        IGVGraphics.dashedLine(context, 0, y, pixelWidth, y);
    }
    for (const group of groups.values()) {
        y += group.count * sampleHeight + GROUP_MARGIN_HEIGHT;
        if (y > pixelBottom) {
            break
        }
        if (y > pixelTop) {
            IGVGraphics.dashedLine(context, 0, y, pixelWidth, y);
        }
    }
    context.restore();
}

const MaxSampleInfoColumnHeight = 128;

class SampleInfoViewport {

    constructor(trackView, column, width) {

        this.guid = guid$2();
        this.trackView = trackView;
        this.isIdeogram = 'ideogram' === trackView.track.type;

        this.browser = trackView.browser;

        this.viewport = div$2({class: 'igv-viewport'});

        column.appendChild(this.viewport);

        this.viewport.style.height = `${trackView.track.height}px`;

        if (this.isIdeogram) {
            this.viewport.style.zIndex = 16;
            this.viewport.style.overflow = 'visible';
        }

        this.canvas = document.createElement('canvas');
        this.viewport.appendChild(this.canvas);
        this.ctx = this.canvas.getContext("2d");
        this.ctx.font = '10px verdana';

        this.contentTop = 0;
        this.hitList = undefined;

        this.setWidth(width);

        this.addMouseHandlers();
    }


    resizeCanvas() {

        const dpi = window.devicePixelRatio;
        const requiredWidth = this.browser.getSampleInfoViewportWidth();

        let requiredHeight;
        if (this.browser.trackViews.length > 1 && this.isIdeogram) {
            const [at, bt] = [this.browser.ideogramTrackView.track, this.browser.rulerTrackView.track];
            requiredHeight = at.height + bt.height;
        } else {
            requiredHeight = this.viewport.clientHeight;
        }


        if (this.canvas.width !== requiredWidth * dpi || this.canvas.height !== requiredHeight * dpi) {
            const canvas = this.canvas;
            canvas.width = requiredWidth * dpi;
            canvas.height = requiredHeight * dpi;
            canvas.style.width = `${requiredWidth}px`;
            canvas.style.height = `${requiredHeight}px`;
            this.ctx = this.canvas.getContext("2d");
            this.ctx.scale(dpi, dpi);

            if (this.isIdeogram) {
                IGVGraphics.fillRect(this.ctx, 0, 0, this.ctx.canvas.width, this.ctx.canvas.height, {fillStyle: appleCrayonRGB('snow')});
            }

        }

    }

    setTop(contentTop) {

        if (typeof this.trackView.track.getSamples === 'function') {
            this.contentTop = contentTop;
            this.repaint();
        }

    }

    setWidth(width) {
        this.viewport.innerWidth = width;
        this.resizeCanvas();
    }

    setHeight(newHeight) {

        const w = this.browser.getSampleInfoViewportWidth();

        this.viewport.style.width = `${w}px`;
        this.viewport.style.height = `${newHeight}px`;

        const dpi = window.devicePixelRatio;

        this.canvas.width = w * dpi;
        this.canvas.height = newHeight * dpi;

        this.canvas.style.width = `${w}px`;
        this.canvas.style.height = `${newHeight}px`;

        this.ctx = this.canvas.getContext('2d');
        this.ctx.scale(dpi, dpi);

        if (this.isIdeogram) {
            IGVGraphics.fillRect(this.ctx, 0, 0, this.ctx.canvas.width, this.ctx.canvas.height, {fillStyle: appleCrayonRGB('snow')});
        }

    }

    repaint() {

        this.resizeCanvas();

        if (this.isIdeogram) {
            // Resize the rulertrack to create more space for the sample info columns.  TODO -- this should not be done on every repaint.
            if (this.browser.rulerTrackView) {
                this.browser.rulerTrackView.setTrackHeight(true === this.browser.sampleInfoControl.showSampleInfo ? this.calculateSampleInfoColumnHeight() : defaultRulerHeight, true);
            }
            this.renderSampleInfoColumns(this.ctx);
        }

        if (typeof this.trackView.track.getSamples === 'function') {
            const samples = this.trackView.track.getSamples();
            if (samples.names && samples.names.length > 0) {
                this.draw({context: this.ctx, samples});
            }
        }
    }

    calculateSampleInfoColumnHeight() {
        const lengths = this.browser.sampleInfo.attributeNames.map(name => this.ctx.measureText(name).width);
        const fudge = 4;
        return fudge + Math.min(Math.max(...lengths), MaxSampleInfoColumnHeight)
    }

    draw({context, samples}) {

        context.clearRect(0, 0, context.canvas.width, context.canvas.height);

        context.fillStyle = appleCrayonRGB('snow');
        context.fillRect(0, 0, context.canvas.width, context.canvas.height);

        const viewportHeight = this.viewport.getBoundingClientRect().height;
        const viewportWidth = this.viewport.getBoundingClientRect().width;

        if (samples && samples.names.length > 0) {


            const attributeNames = this.browser.sampleInfo.attributeNames;

            let shim = 1;

            const tileHeight = samples.height;
            shim = tileHeight - 2 * shim <= 1 ? 0 : 1;

            let y = samples.yOffset - this.contentTop;

            let rowIndex = 0;
            this.hitList = {};

            for (const sampleName of samples.names) {

                const attributes = this.browser.sampleInfo.getAttributes(sampleName);
                if (attributes) {

                    let yy = y + shim;
                    if (samples.groupIndeces && samples.groups.size > 0) {
                        yy += (samples.groupIndeces[rowIndex] + 1) * GROUP_MARGIN_HEIGHT;
                    }
                    if (yy > viewportHeight) {
                        break
                    }
                    if (yy + tileHeight > 0) {

                        const hh = tileHeight - (2 * shim);

                        const attributeEntries = Object.entries(attributes);
                        for (const attributeEntry of attributeEntries) {

                            const [attribute, value] = attributeEntry;

                            const index = attributeNames.indexOf(attribute);
                            const x = sampleInfoTileXShim + index * sampleInfoTileWidth;

                            context.fillStyle = this.browser.sampleInfo.getAttributeColor(attribute, value);
                            context.fillRect(x, yy, sampleInfoTileWidth - 1, hh);

                            const key = `${Math.floor(x)}#${Math.floor(yy)}#${sampleInfoTileWidth}#${Math.ceil(hh)}`;
                            this.hitList[key] = `${attribute}#${value}`;
                        }
                    } // for (attributeEntries)

                } // if (attributes)

                y += tileHeight;
                rowIndex++;

            } // for (sample.names)

            drawGroupDividers(context,
                0,
                viewportWidth,
                viewportHeight,
                samples.yOffset - this.contentTop,
                samples.height,
                samples.groups);
        }

    }

    renderSampleInfoColumns(context) {

        const drawRotatedText = (ctx, text, x, y, width, height) => {

            const xShim = 2;
            const yShim = 2;

            ctx.save();
            ctx.font = '10px verdana';

            ctx.translate(x + width / 2, y + height);
            ctx.rotate(-Math.PI / 2);
            ctx.textAlign = 'left';
            ctx.fillStyle = appleCrayonRGB('lead');
            ctx.fillText(text, xShim, yShim);

            ctx.restore();
        };

        const attributeNames = this.browser.sampleInfo.attributeNames;
        this.hitList = {};
        for (let i = 0; i < attributeNames.length; i++) {
            const x = (sampleInfoTileXShim + i * sampleInfoTileWidth);
            const w = (sampleInfoTileWidth - 1);
            const h = Math.round(context.canvas.height / window.devicePixelRatio);

            IGVGraphics.fillRect(context, x, 0, w, h, {fillStyle: appleCrayonRGB('snow')});
            // IGVGraphics.fillRect(context, x, 0, w, h, { fillStyle: randomRGB(150,250) })
            drawRotatedText(context, attributeNames[i], x, 0, w, h);

            const key = `${Math.floor(x)}#0#${w}#${Math.ceil(h)}`;
            this.hitList[key] = `${attributeNames[i]}`;

        }

    }

    renderSVGContext(context, {deltaX, deltaY}) {

        if (typeof this.trackView.track.getSamples === 'function') {

            const samples = this.trackView.track.getSamples();

            const yScrollDelta = 0;   // This is not relevant, scrolling is handled in "draw"

            const {width, height} = this.viewport.getBoundingClientRect();

            const str = (this.trackView.track.name || this.trackView.track.id).replace(/\W/g, '');
            const id = `${str}_sample_names_guid_${guid$2()}`;

            context.saveWithTranslationAndClipRect(id, deltaX, deltaY + yScrollDelta, width, height, -yScrollDelta);

            this.draw({context, samples});

            context.restore();
        }
    }

    addMouseHandlers() {
        this.addMouseMoveHandler();
        this.addMouseClickHandler();
    }

    addMouseMoveHandler() {

        this.boundMouseMoveHandler = mouseMove.bind(this);
        this.viewport.addEventListener('mousemove', this.boundMouseMoveHandler);

        function mouseMove(event) {
            // event.stopPropagation()

            if (this.hitList) {

                const entries = Object.entries(this.hitList);

                if (this.isIdeogram) {

                    const getXY = (column, viewport) => {
                        const {marginTop} = window.getComputedStyle(viewport);
                        const {
                            x,
                            y
                        } = translateMouseCoordinates(event, this.browser.columnContainer.querySelector('.igv-sample-info-column'));
                        return {x: Math.floor(x), y: Math.floor(y - parseInt(marginTop, 10))}
                    };

                    const column = this.browser.columnContainer.querySelector('.igv-sample-info-column');
                    const {x, y} = getXY(column, this.viewport);

                    column.setAttribute('title', '');
                    for (const [bbox, value] of entries) {

                        const [xx, yy, width, height] = bbox.split('#').map(str => parseInt(str, 10));
                        if (x < xx || x > xx + width || y < yy || y > yy + height) ; else {
                            column.setAttribute('title', `${value}`);
                            break
                        }
                    }

                } else {

                    const {x, y} = translateMouseCoordinates(event, this.viewport);

                    this.viewport.setAttribute('title', '');

                    for (const [bbox, value] of entries) {
                        const [xx, yy, width, height] = bbox.split('#').map(str => parseInt(str, 10));
                        if (x < xx || x > xx + width || y < yy || y > yy + height) ; else {
                            const [a, b] = value.split('#');
                            this.viewport.setAttribute('title', `${a.split(SampleInfo.emptySpaceReplacement).join(' ')}: ${'-' === b ? '' : b}`);
                            break
                        }
                    }
                }

            }
        }
    }

    addMouseClickHandler() {

        this.boundMouseClickHandler = mouseClick.bind(this);
        this.viewport.addEventListener('click', this.boundMouseClickHandler);

        function mouseClick(event) {
            // event.stopPropagation()

            if (this.hitList) {

                const entries = Object.entries(this.hitList);

                if (this.isIdeogram) {

                    const getXY = (column, viewport) => {
                        const {marginTop} = window.getComputedStyle(viewport);
                        const {
                            x,
                            y
                        } = translateMouseCoordinates(event, this.browser.columnContainer.querySelector('.igv-sample-info-column'));
                        return {x: Math.floor(x), y: Math.floor(y - parseInt(marginTop, 10))}
                    };

                    const column = this.browser.columnContainer.querySelector('.igv-sample-info-column');
                    const {x, y} = getXY(column, this.viewport);

                    for (const [bbox, value] of entries) {

                        const [xx, yy, width, height] = bbox.split('#').map(str => parseInt(str, 10));
                        if (x < xx || x > xx + width || y < yy || y > yy + height) ; else {

                            const tracks = this.browser.findTracks(track => typeof track.sortByAttribute === 'function');
                            for (const track of tracks) {
                                track.sortByAttribute(value);
                            }

                            break
                        }
                    }

                } else {

                    const {x, y} = translateMouseCoordinates(event, this.viewport);

                    for (const [bbox, value] of entries) {
                        const [xx, yy, width, height] = bbox.split('#').map(str => parseInt(str, 10));
                        if (x < xx || x > xx + width || y < yy || y > yy + height) ; else {
                            value.split('#');
                            break
                        }
                    }
                }

            }
        }
    }

    removeMouseHandlers() {
        this.viewport.removeEventListener('mousemove', this.boundMouseMoveHandler);
        this.viewport.removeEventListener('click', this.boundMouseClickHandler);
    }

    dispose() {
        this.removeMouseHandlers();
        this.viewport.remove();
    }

    show() {
        this.viewport.style.display = 'block';
    }

    hide() {
        this.viewport.style.display = 'none';
    }

}

const maxSampleNameViewportWidth = 200;
const fudgeTextMetricWidth = 4;
const maxFontSize = 10;

class SampleNameViewport {

    constructor(trackView, column, unused, width) {

        this.guid = guid$2();
        this.trackView = trackView;

        this.browser = trackView.browser;

        this.viewport = div$2({class: 'igv-viewport'});

        column.appendChild(this.viewport);

        if (trackView.track.height) {
            this.viewport.style.height = `${trackView.track.height}px`;
        }

        this.canvas = document.createElement('canvas');
        this.viewport.appendChild(this.canvas);
        this.ctx = this.canvas.getContext("2d");

        this.contentTop = 0;
        this.hitList = undefined;

        this.setWidth(width);

        this.addMouseHandlers();
    }

    checkCanvas() {

        let width = 0;
        if (true === this.browser.showSampleNames) {
            width = undefined === this.browser.sampleNameViewportWidth ? 0 : this.browser.sampleNameViewportWidth;
        }

        this.ctx.canvas.width = width * window.devicePixelRatio;
        this.ctx.canvas.style.width = `${width}px`;

        this.ctx.canvas.height = this.viewport.clientHeight * window.devicePixelRatio;
        this.ctx.canvas.style.height = `${this.viewport.clientHeight}px`;

        this.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);

    }

    setTop(contentTop) {

        if (typeof this.trackView.track.getSamples === 'function') {
            this.contentTop = contentTop;
            const samples = this.trackView.track.getSamples();
            this.repaint(samples);
        }

    }

    setWidth(width) {
        this.viewport.innerWidth = width;
        this.checkCanvas();
    }

    async repaint(samples) {

        if (samples.names.length > 0) {
            if (true === this.browser.showSampleNames) {
                this.checkCanvas();
                this.draw({context: this.ctx, samples});

                if (undefined === this.browser.sampleNameViewportWidth) {
                    const lengths = samples.names.map(name => this.ctx.measureText(name).width);
                    this.browser.sampleNameViewportWidth = Math.min(maxSampleNameViewportWidth, fudgeTextMetricWidth + Math.ceil(Math.max(...lengths)));
                    this.browser.layoutChange();
                }

            }
        }

    }

    draw({context, samples}) {

        IGVGraphics.fillRect(context, 0, 0, context.canvas.width, samples.height, { fillStyle: appleCrayonRGB('snow') });

        if (samples && samples.names.length > 0) {

            const viewportHeight = this.viewport.getBoundingClientRect().height;
            const tileHeight = samples.height;
            const shim = tileHeight - 2 <= 1 ? 0 : 1;

            let y =  samples.yOffset - this.contentTop;

            let rowIndex = 0;
            this.hitList = {};

            for (const sampleName of samples.names) {

                const x = 0;
                let yy = y + shim;
                if (samples.groupIndeces && samples.groups.size > 0) {
                    yy += (samples.groupIndeces[rowIndex] + 1) * GROUP_MARGIN_HEIGHT;
                }

                if (yy + tileHeight > 0) {
                    const hh = tileHeight - (2 * shim);
                    drawTextInRect(context, sampleName, x + 2, yy, context.canvas.width, hh);
                }

                y += tileHeight;
                rowIndex++;

                if (y > viewportHeight) {
                    break
                }
            }

            drawGroupDividers(context, 0, context.canvas.width, context.canvas.height,  samples.yOffset - this.contentTop, samples.height, samples.groups);
        }
    }

    renderSVGContext(context, {deltaX, deltaY}) {

        if (typeof this.trackView.track.getSamples === 'function') {

            const samples = this.trackView.track.getSamples();

            const yScrollDelta = 0;   // This is not relevant, scrolling is handled in "draw"

            const {width, height} = this.viewport.getBoundingClientRect();

            const str = (this.trackView.track.name || this.trackView.track.id).replace(/\W/g, '');
            const id = `${str}_sample_names_guid_${guid$2()}`;

            context.saveWithTranslationAndClipRect(id, deltaX, deltaY + yScrollDelta, width, height, -yScrollDelta);

            this.draw({context, samples});

            context.restore();
        }
    }

    addMouseHandlers() {

        this.boundClickHandler = clickHandler.bind(this);
        this.viewport.addEventListener('contextmenu', this.boundClickHandler);

        function clickHandler(event) {

            event.preventDefault();
            // event.stopPropagation()

            const config =
                {
                    label: 'Name Panel Width',
                    value: this.browser.sampleNameViewportWidth,
                    callback: newWidth => {
                        this.browser.sampleNameViewportWidth = parseInt(newWidth);
                        // for (let {sampleNameViewport} of this.browser.trackViews) {
                        //     sampleNameViewport.setWidth(this.browser.sampleNameViewportWidth)
                        // }
                        this.browser.layoutChange();
                    }
                };

            this.browser.inputDialog.present(config, event);
        }

        this.boundMouseMoveHandler = mouseMove.bind(this);
        this.viewport.addEventListener('mousemove', this.boundMouseMoveHandler);

        function mouseMove(event) {
            // event.stopPropagation()

            if (this.hitList) {

                const entries = Object.entries(this.hitList);

                const {x, y} = translateMouseCoordinates(event, this.viewport);

                this.viewport.setAttribute('title', '');

                for (const [bbox, value] of entries) {
                    const [xx, yy, width, height] = bbox.split('#').map(str => parseInt(str, 10));
                    if (x < xx || x > xx + width || y < yy || y > yy + height) ; else {
                        this.viewport.setAttribute('title', `${value}`);
                        break
                    }
                }
            }
        }

    }

    removeMouseHandlers() {
        this.viewport.removeEventListener('contextmenu', this.boundClickHandler);
        this.viewport.removeEventListener('mousemove', this.boundMouseMoveHandler);
    }

    dispose() {
        this.removeMouseHandlers();
        this.viewport.remove();
    }

}

function drawTextInRect(context, text, x, y, width, height) {

    const pixels = Math.min(height, maxFontSize);
    context.font = `${pixels}px sans-serif`;
    context.textAlign = 'start';
    context.fillStyle = appleCrayonRGB('lead');

    const textX = x;

    const metrics = context.measureText(text);
    const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
    const textY = y + height / 2 + textHeight / 2;

    context.fillText(text, textX, textY);
}

class MenuPopup {
    constructor(parent) {
        this.popover = div$2({class: 'igv-menu-popup'});

        parent.appendChild(this.popover);
        this.parent = parent;

        const header = div$2({class: 'igv-menu-popup-header'});
        this.popover.appendChild(header);

        attachDialogCloseHandlerWithParent$1(header, () => this.popover.style.display = 'none');

        this.popoverContent = div$2();
        this.popover.appendChild(this.popoverContent);

        makeDraggable$1(this.popover, header);

        // absorb click to prevent it leaking through to parent DOM element
        header.addEventListener('click', e => {
            e.stopPropagation();
            e.preventDefault();
        });

        this.popover.style.display = 'none';

    }

    presentMenuList(trackView, menuList) {

        hideAllMenuPopups(this.parent);

        if (menuList.length > 0) {

            this.popoverContent.innerHTML = '';

            const parsedList = this.parseMenuList(trackView, menuList);

            for (let item of parsedList) {

                if (item.init) {
                    item.init();
                }

                if (0 === parsedList.indexOf(item)) {
                    item.element.classList.remove('igv-track-menu-border-top');
                }

                if (item.element.classList.contains('igv-track-menu-border-top') || item.element.classList.contains('igv-menu-popup-check-container')) ; else if (item.element.tagName.toLowerCase() === 'div') {
                    item.element.classList.add('igv-menu-popup-shim');
                }

                this.popoverContent.appendChild(item.element);

            }

            // NOTE: style.display must NOT be 'none' when calculating width. A display = 'none' will always
            //       yield a width of zero (0).
            this.popover.style.display = 'flex';

            const { width } = this.popover.getBoundingClientRect();

            this.popover.style.left = `${-width}px`;
            this.popover.style.top = `${0}px`;

        }
    }

    parseMenuList(trackView, menuList) {

        return menuList.map((item, i) => {

            let element;

            // name and element fields checked for backward compatibility
            if (item.name) {
                element = document.createElement('div');
                element.textContent = item.name;
            } else if (item.element) {
                element = item.element;
            } else if (typeof item.label === 'string') {
                element = document.createElement('div');
                element.innerHTML = item.label;
            } else if (typeof item === 'string') {

                if (item.startsWith("<")) {
                    const tempDiv = document.createElement('div');
                    tempDiv.innerHTML = item;
                    element = tempDiv.firstChild;
                } else {
                    element = document.createElement('div');
                    element.textContent = item;
                }
            }

            if (0 === i) {
                element.classList.add('igv-track-menu-border-top');
            }

            if (item.click || item.dialog) {

                const handleClick = e => {
                    e.preventDefault();
                    e.stopPropagation();

                    if (item.click) {

                        if (trackView.track.selected) {

                            const multiSelectedTrackViews = trackView.browser.getSelectedTrackViews();

                            if (true === item.doAllMultiSelectedTracks) {
                                item.click.call(trackView.track, e);
                            } else {

                                if ('removeTrack' === item.menuItemType) {

                                    const callback = () => {

                                        trackView.browser.overlayTrackButton.setVisibility(false);

                                        for (const { track } of multiSelectedTrackViews) {
                                            item.click.call(track, e);
                                        }
                                    };

                                    const count = multiSelectedTrackViews.length;

                                    const config =
                                        {
                                            html: `Are you sure you want to delete ${count} tracks?`,
                                            callback
                                        };

                                    trackView.browser.menuUtils.dialog.present(config, e);

                                } else {

                                    for (const { track } of multiSelectedTrackViews) {
                                        item.click.call(track, e);
                                    }

                                }
                            }

                        } else {
                            item.click.call(trackView.track, e);
                        }

                    } else if (item.dialog) {
                        item.dialog.call(trackView.track, e);
                    }

                    this.popover.style.display = 'none';
                };

                element.addEventListener('click', handleClick);
                element.addEventListener('touchend', e => handleClick(e));
                element.addEventListener('mouseup', function (e) {
                    e.preventDefault();
                    e.stopPropagation();
                });

            }

            return {element, init: (item.init || undefined)};
        });

    }

    presentTrackContextMenu(e, menuItems) {

        this.popoverContent.innerHTML = '';

        const menuElements = createMenuElements(menuItems, this.popover);
        for (let {el} of menuElements) {
            this.popoverContent.appendChild(el);
        }

        present(e, this.popover);

    }

    hide() {
        this.popover.style.display = 'none';
    }

    dispose() {

        this.popoverContent.innerHTML = '';
        this.popover.innerHTML = '';

        Object.keys(this).forEach(function (key) {
            this[key] = undefined;
        });
    }
}

function createMenuElements(itemList, popover) {

    return itemList.map(item => {

        let el;

        if (typeof item === 'string' && '<hr/>' === item) {
            el = document.createElement('hr');
        } else if (typeof item === 'string') {
            el = div$2({class: 'context-menu'});
            el.innerHTML = item;
        } else if (typeof item === 'Node') {
            el = item;
        } else {
            if (typeof item.init === 'function') {
                item.init();
            }

            if ("checkbox" === item.type) {
                el = createCheckbox("Show all bases", item.value);
            }

            // TODO: I can't find any use of this and should probably be deleted - data
            // else if ("color" === item.type) {
            //
            //     const colorPicker = new GenericColorPicker(popover.parentElement)
            //     colorPicker.configure({color: 'grey'})
            //
            //     el = DOMUtils.div({class: 'context-menu'})
            //     if (typeof item.label === 'string') {
            //         el.innerHTML = item.label
            //     }
            //     const clickHandler = e => {
            //         colorPicker.show()
            //         DOMUtils.hide(popover)
            //         e.preventDefault()
            //         e.stopPropagation()
            //     }
            //     el.addEventListener('click', clickHandler)
            //     el.addEventListener('touchend', clickHandler)
            //     el.addEventListener('mouseup', function (e) {
            //         e.preventDefault()
            //         e.stopPropagation()
            //     })
            // }

            else {
                el = div$2({class: 'context-menu'});
                if (typeof item.label === 'string') {
                    el.innerHTML = item.label;
                }
            }

            if (item.click && "color" !== item.type) {
                el.addEventListener('click', handleClick);
                el.addEventListener('touchend', handleClick);
                el.addEventListener('mouseup', function (e) {
                    e.preventDefault();
                    e.stopPropagation();
                });

                // eslint-disable-next-line no-inner-declarations
                function handleClick(e) {
                    item.click();
                    hide$1(popover);
                    e.preventDefault();
                    e.stopPropagation();
                }
            }
        }

        return {el, init: item.init}
    })

}

function present(e, popover) {

    // NOTE: style.display most NOT be 'none' when calculating width. a display = 'none' will always
    //       yield a width of zero (0).
    popover.style.display = 'flex';

    const {x, y} = translateMouseCoordinates(e, popover.parentNode);
    const {width} = popover.getBoundingClientRect();
    const xmax = x + width;

    const {width: parentWidth} = popover.parentNode.getBoundingClientRect();

    popover.style.left = `${xmax > parentWidth ? (x - (xmax - parentWidth)) : x}px`;
    popover.style.top = `${y}px`;

}

const hideAllMenuPopups = parent => {

    const menus = parent.querySelectorAll('.igv-menu-popup');
    for (const menu of menus) {
        menu.style.display = 'none';
    }

};

class NavbarButton {

    constructor(parent, browser, title, buttonLabel, imageSVG, imageHoverSVG, initialButtonState) {

        this.browser = browser;

        this.button = div$2({class: 'igv-navbar-text-button'});
        parent.appendChild(this.button);

        if (Array.isArray(title)) {
            this.textContent = title[ 0 ];
            this.title = title[ 1 ];
        } else {
            this.textContent = this.title = title;
        }

        this.buttonLabel = buttonLabel;

        this.imageDictionary =
            {
                image: `url("data:image/svg+xml,${ encodeURIComponent(imageSVG) }")`,
                imageHover: `url("data:image/svg+xml,${ encodeURIComponent(imageHoverSVG) }")`,
            };

        this.responsiveKey = 'text';

        this.configureButton(this.textContent, this.title);

        this.setState(initialButtonState);

        browser.on('navbar-resize', navbarButtonCSSClass => {
            this.navbarResizeHandler(navbarButtonCSSClass);
        });

    }

    navbarResizeHandler(navbarButtonCSSClass) {
        const key = 'igv-navbar-icon-button' === navbarButtonCSSClass ? 'image' : 'text';
        if (key !== this.responsiveKey) {
            this.responsiveKey = key;
            this.configureButton(this.textContent, this.title);
            this.setState(undefined);
        }
    }

    configureButton(textContent, title) {

        this.groupElement = undefined;
        this.button.title = title;
        this.button.innerHTML = '';
        this.button.style.backgroundImage = 'none';
        this.button.classList.remove('igv-navbar-icon-button');
        this.button.classList.remove('igv-navbar-text-button');

        'text' === this.responsiveKey ? this.configureTextButton(textContent) : this.configureIconButton();

    }

    configureTextButton(textContent) {

        this.button.classList.add('igv-navbar-text-button');

        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = this.buttonLabel;
        const svgRoot = tempDiv.firstChild;
        this.button.appendChild(svgRoot);

        this.groupElement = svgRoot.querySelector('#igv-navbar-button-group');

        const tspanElement = svgRoot.querySelector('#igv-navbar-button-label');
        tspanElement.textContent = textContent;
    }

    configureIconButton() {
        // console.log(`icon ${this.title}`)
        this.button.classList.add('igv-navbar-icon-button');
    }

    setState(doHover) {

        if (undefined !== doHover) {
            this.doHover = doHover;
        }

        'text' === this.responsiveKey ? this.setTextButtonState(this.doHover) : this.setIconButtonState(this.doHover);

    }

    setTextButtonState(doHover) {
        this.groupElement.classList.remove(...this.groupElement.classList);
        const className = true === doHover ? 'igv-navbar-text-button-svg-hover' : 'igv-navbar-text-button-svg-inactive';
        this.groupElement.classList.add(className);
    }

    setIconButtonState(doHover) {
        this.button.style.backgroundImage = true === doHover ? this.imageDictionary.imageHover : this.imageDictionary.image;
    }

    show() {
        this.button.style.display = '';
    }

    hide() {
        this.button.style.display = 'none';
    }

    setVisibility(isVisible) {
        if (true === isVisible) {
            this.show();
        } else {
            this.hide();
        }
    }
}

const overlayTrackImage =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>Overlay Tracks</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="Overlay-Tracks">
            <rect id="backdrop" stroke="#737373" stroke-width="12" fill="#FFFFFF" x="6" y="6" width="613" height="613" rx="135"></rect>
            <g id="layer-group" transform="translate(3, 127)">
                <rect id="a" stroke="#737373" stroke-width="24" fill="#A1A1A1" x="12" y="12" width="332" height="139"></rect>
                <rect id="a---hold-out" fill="#A1A1A1" x="9" y="25" width="324" height="115"></rect>
                <rect id="b" stroke="#737373" stroke-width="24" fill="#C9C9C9" x="81" y="103" width="474" height="139"></rect>
                <rect id="c" stroke="#737373" stroke-width="24" fill="#ECECEC" x="238" y="214" width="372" height="139"></rect>
                <rect id="c---hold-out" fill="#ECECEC" x="250" y="226" width="372" height="115"></rect>
            </g>
            <rect id="over-border" stroke="#737373" stroke-width="12" x="6" y="6" width="613" height="613" rx="135"></rect>
        </g>
    </g>
</svg>`;

const overlayTrackImageHover =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>Overlay Tracks Hover</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="Overlay-Tracks-Hover">
            <rect id="backdrop-copy" stroke="#737373" stroke-width="12" fill="#737373" x="6" y="6" width="613" height="613" rx="135"></rect>
            <g id="layer-group" transform="translate(3, 127)">
                <rect id="a" stroke="#FFFFFF" stroke-width="24" fill="#A1A1A1" x="12" y="12" width="332" height="139"></rect>
                <rect id="a---hold-out" fill="#A1A1A1" x="9" y="25" width="324" height="115"></rect>
                <rect id="b" stroke="#FFFFFF" stroke-width="24" fill="#C9C9C9" x="81" y="103" width="474" height="139"></rect>
                <rect id="c" stroke="#FFFFFF" stroke-width="24" fill="#ECECEC" x="238" y="214" width="372" height="139"></rect>
                <rect id="c---hold-out" fill="#ECECEC" x="250" y="226" width="372" height="115"></rect>
            </g>
            <rect id="over-border-copy" stroke="#737373" stroke-width="12" x="6" y="6" width="613" height="613" rx="135"></rect>
        </g>
    </g>
</svg>`;

const buttonLabel =
    `<svg width="80px" height="18px" viewBox="0 0 80 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="igv-navbar-button-group">
            <rect id="Rectangle" x="0.5" y="0.5" width="79" height="17" rx="6"></rect>
            <text id="igv-text-button-label" x="50%" y="50%" dy=".1em" font-family="Helvetica" font-size="12" font-weight="normal" letter-spacing="-0.372">
                <tspan id="igv-navbar-button-label"></tspan>
            </text>
        </g>
    </g>
</svg>`;

const sampleNameButtonLabel =
    `<svg width="90px" height="20px" viewBox="0 0 90 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="igv-navbar-button-group">
            <rect id="Rectangle" x="0.5" y="0.5" width="89" height="18" rx="6"></rect>
            <text id="igv-text-button-label" x="50%" y="50%" dy=".1em" font-family="Helvetica" font-size="12" font-weight="normal" letter-spacing="-0.372">
                <tspan id="igv-navbar-button-label"></tspan>
            </text>
        </g>
    </g>
</svg>`;

/**
 * Represents 2 or more  tracks overlaid on a common viewport.
 */
class MergedTrack extends TrackBase {

    static defaults = {
        autoscale: undefined,
        alpha: 0.5,
        height: 50
    }

    constructor(config, browser, tracks) {
        super(config, browser);
        this.type = "merged";
        this.paintAxis = paintAxis;
        this.graphType = config.graphType;
        if (tracks) {
            this.tracks = tracks;   // Dynamic creation, actual track objects (not configurations)
        } else {
            this.tracks = [];
        }
    }


    async postInit() {

        if (this.config.tracks) {
            // Track configurations, this indicates a configured merged track as opposed to dynamic merge through the UI
            // Actual track objects need to be created.
            for (let tconf of this.config.tracks) {
                const t = await this.browser.createTrack(tconf);
                if (t) {
                    this.tracks.push(t);
                } else {
                    console.warn("Could not create track " + tconf);
                }
                if (typeof t.postInit === 'function') {
                    await t.postInit();
                }
            }
            // Default to autoscale unless scale if range or autoscale is not otherwise defined
            const allTracksSpecified = this.config.tracks.every(config => config.autoscale !== undefined || config.max !== undefined);
            if (!allTracksSpecified) {
                this.config.autoscale = this.config.max === undefined;
            }
        }

        // Mark constitutive tracks as merged.
        for (let t of this.tracks) t.isMergedTrack = true;

        // Explicit settings -- these will override any individual track settings
        if(this.config.autoscale) {
            this.autoscale = this.config.autoscale;
        } else if (this.config.max !== undefined) {
            this.setDataRange ({
                min: this.config.min || 0,
                max: this.config.max
            });
        }


        if (this.config.flipAxis !== undefined) {
            for (let t of this.tracks) t.flipAxis = this.config.flipAxis;
        }

        if (this.config.logScale !== undefined) {
            for (let t of this.tracks) t.logScale = this.config.logScale;
        }

        this.resolutionAware = this.tracks.some(t => t.resolutionAware);
    }

    set flipAxis(b) {
        this.config.flipAxis = b;
        for (let t of numericTracks(this.tracks)) {
            t.flipAxis = b;
        }
    }

    get flipAxis() {
        return numericTracks(this.tracks).every(t => t.flipAxis)
    }

    set logScale(b) {
        this.config.logScale = b;
        for (let t of numericTracks(this.tracks)) {
            t.logScale = b;
        }
    }

    get logScale() {
        return numericTracks(this.tracks).every(t => t.logScale)
    }

    get height() {
        return this._height
    }

    set height(h) {
        this.config.height = h;
        this._height = h;
        if (this.tracks) {
            for (let t of this.tracks) {
                t.height = h;
                t.config.height = h;
            }
        }
    }

    set autoscale(b) {
        this._autoscale = b;
        if(b === false && this.tracks) {
            for(let t of this.tracks) t.autoscale = false;
        }
    }

    get autoscale() {
        return this._autoscale
    }

    set autoscaleGroup(g) {
        if(this.tracks) {
            for(let t of this.tracks) t.autoscaleGroup = g;
        }
    }

    get autoscaleGroup() {
        if(this.tracks && this.tracks.length > 0) {
            const g = this.tracks[0].autoscaleGroup;
            return (this.tracks.some(t => g !== t.autoscaleGroup)) ? undefined : g
        }
    }

    /**
     * Set the data range of all constitutive numeric tracks.  This method is called from the menu item, i.e. an explicit
     * setting, so it should disable autoscale as well.
     *
     * @param min
     * @param max
     */

    setDataRange({min, max}) {
        this.autoscale = false;
        for (const track of numericTracks(this.tracks)) {
            track.dataRange = {min, max};
            track.autoscale = false;
            track.autoscaleGroup = false;
        }
    }

    set dataRange({min, max}) {
        for (const track of numericTracks(this.tracks)) {
            track.dataRange = {min, max};
        }
    }

    /**
     * Return a DataRang {min, max} if all constitutive numeric tracks have identical range.  A numeric track is defined
     * as a track with a data range.  Otherwise return undefined.
     *
     * @returns {{min: any, max: any}|undefined}
     */
    get dataRange() {
        if(this.tracks) {
            const num = numericTracks(this.tracks);
            if (num.length > 0) {
                const firstRange = num[0].dataRange;
                if (num.every(t => t.dataRange && t.dataRange.min === firstRange.min && t.dataRange.max === firstRange.max)) {
                    return firstRange
                }
            }
        }
        return undefined
    }


    menuItemList() {
        const items = [];
        if (numericTracks(this.tracks).length > 0) {

            if (this.flipAxis !== undefined) {
                items.push({
                    label: "Flip y-axis",
                    click: function flipYAxisHandler() {
                        this.flipAxis = !this.flipAxis;
                        this.trackView.repaintViews();
                    }
                });
            }

            items.push(...this.numericDataMenuItems());
        }

        items.push('<hr/>');
        items.push(this.overlayTrackAlphaAdjustmentMenuItem());
        items.push(this.trackSeparationMenuItem());

        return items
    }

    /**
     * Returns a MergedFeatureCollection containing an array of features for the specified range, 1 for each track.
     * In addition it contains track names in the same order.
     */
    async getFeatures(chr, bpStart, bpEnd, bpPerPixel) {

        const promises = this.tracks.map((t) => t.getFeatures(chr, bpStart, bpEnd, bpPerPixel));
        const featureArrays = await Promise.all(promises);

        if (featureArrays.every((arr) => arr.length === 0)){
            return new MergedFeatureCollection([], [])
        }
        else {
            const trackNames = this.tracks.map((t) => t.name);
            return new MergedFeatureCollection(featureArrays, trackNames)
        }
    }

    draw(options) {

        const mergedFeatures = options.features;    // A MergedFeatureCollection

        for (let i = 0, len = this.tracks.length; i < len; i++) {
            const trackOptions = Object.assign({}, options);
            trackOptions.features = mergedFeatures.featureArrays[i];
            trackOptions.alpha = this.alpha;

            if (this.graphType) {
                this.tracks[i].graphType = this.graphType;
            }
            this.tracks[i].draw(trackOptions);
        }
    }

    popupData(clickState) {

        const clickedFeaturesArray = this.clickedFeatures(clickState);

        if (clickedFeaturesArray && clickedFeaturesArray.length === this.tracks.length) {
            // Array of feature arrays, 1 for each track
            const popupData = [];
            // Flag used to check if there is at least one track with data
            let noData = true;
            for (let i = 0; i < clickedFeaturesArray.length; i++) {


                if (i > 0) popupData.push('<hr/>');
                popupData.push(`<div style=background-color:rgb(245,245,245);border-bottom-style:dashed;border-bottom-width:1px;padding-bottom:5px;padding-top:10px;font-weight:bold;font-size:larger >${clickedFeaturesArray[i].trackName}</div>`);
                if (clickedFeaturesArray[i].features.length > 0) {
                    noData = false;

                    const trackPopupData = this.tracks[i].popupData(clickState, clickedFeaturesArray[i].features);
                    popupData.push(...trackPopupData);
                } else {
                    // Notify user if there is no data or all values are 0 for a specific track
                    popupData.push("Missing or 0 value(s)");
                }
            }

            // We don't want to display popup if no track has data.
            // If at least one does, we want to display the popup.
            if (noData === true) {
                return []
            } else {
                return popupData
            }
        }
    }

    clickedFeatures(clickState) {


        // We use the cached features rather than method to avoid async load.  If the
        // feature is not already loaded this won't work,  but the user wouldn't be mousing over it either.
        const mergedFeaturesCollection = clickState.viewport.cachedFeatures;

        if (!mergedFeaturesCollection || !mergedFeaturesCollection.featureArrays || !Array.isArray(mergedFeaturesCollection.featureArrays) || mergedFeaturesCollection.featureArrays.length === 0) {
            return []
        }

        const genomicLocation = clickState.genomicLocation;
        const clickedFeatures = [];

        // When zoomed out we need some tolerance around genomicLocation
        const tolerance = (clickState.referenceFrame.bpPerPixel > 0.2) ? 3 * clickState.referenceFrame.bpPerPixel : 0.2;
        const ss = genomicLocation - tolerance;
        const ee = genomicLocation + tolerance;
        for (let i = 0; i < mergedFeaturesCollection.featureArrays.length; i++) {
            const tmp = (FeatureUtils.findOverlapping(mergedFeaturesCollection.featureArrays[i], ss, ee));
            clickedFeatures.push({
                trackName: mergedFeaturesCollection.trackNames[i],
                features: tmp
            });
        }

        return clickedFeatures
    }

    get supportsWholeGenome() {
        return this.tracks.every(track => track.supportsWholeGenome)
    }

    /**
     * Return json like object for creating a session
     */
    getState() {
        const state = super.getState();
        const trackStates = [];
        for (let t of this.tracks) {
            trackStates.push(t.getState());
        }
        state.tracks = trackStates;
        return state
    }

    updateScales(visibleViewports) {

        let scaleChange;

        if (this.autoscale) {
            // Overrides any specific track scale settings
            scaleChange = true;
            let allFeatures = [];
            for (let visibleViewport of visibleViewports) {
                if (visibleViewport.featureCache && visibleViewport.featureCache.features) {
                    const referenceFrame = visibleViewport.referenceFrame;
                    const start = referenceFrame.start;
                    const end = start + referenceFrame.toBP(visibleViewport.getWidth());
                    const mergedFeatureCollection = visibleViewport.featureCache.features;

                    if (this.autoscale) {
                        allFeatures.push({value: mergedFeatureCollection.getMax(start, end)});
                        allFeatures.push({value: mergedFeatureCollection.getMin(start, end)});
                    }
                }
                const dataRange = doAutoscale(allFeatures);
                for (const track of numericTracks(this.tracks)) {
                    // Do not use this.dataRange, as that has side effects
                    track.dataRange = dataRange;
                }

            }
        } else {
            // Individual track scaling
            let idx = -1;
            for (let track of this.tracks) {
                ++idx;
                if (track.autoscale) {
                    scaleChange = true;
                    let allFeatures = [];
                    for (let visibleViewport of visibleViewports) {
                        if (visibleViewport.featureCache && visibleViewport.featureCache.features) {
                            const referenceFrame = visibleViewport.referenceFrame;
                            const start = referenceFrame.start;
                            const end = start + referenceFrame.toBP(visibleViewport.getWidth());
                            const mergedFeatureCollection = visibleViewport.featureCache.features;
                            const featureList = mergedFeatureCollection.featureArrays[idx];
                            if (featureList) {
                                for (let f of featureList) {
                                    if (f.end < start) continue
                                    if (f.start > end) break
                                    allFeatures.push(f);
                                }
                            }
                        }
                    }
                    track.dataRange = doAutoscale(allFeatures);
                }
            }
        }
        return scaleChange
    }

    overlayTrackAlphaAdjustmentMenuItem() {

        const element = div$2();
        element.innerText = 'Set transparency';

        function dialogPresentationHandler(e) {
            const callback = alpha => {
                this.alpha = Math.max(0.001, alpha);
                this.repaintViews();
            };

            const config =
                {
                    label: 'Transparency',
                    value: this.alpha,
                    min: 0.0,
                    max: 1.0,
                    scaleFactor: 1000,
                    callback
                };

            this.browser.sliderDialog.present(config, e);
        }

        return {element, dialog: dialogPresentationHandler}
    }

    trackSeparationMenuItem() {

        let element = document.createElement('div');
        element.textContent = 'Separate tracks';

        async function click(e) {

            // Capture state which will be nulled when track is removed
            const groupAutoscale = this.autoscale;
            const name = this.name;
            const tracks = this.tracks;
            const browser = this.browser;
            const order = this.order;

            browser.removeTrack(this);
            for (let track of tracks) {
                track.order = order;
                if (groupAutoscale) {
                    track.autoscaleGroup = name;
                }
                track.isMergedTrack = false;
                browser.addTrack(track);
            }
            await browser.updateViews();
            browser.reorderTracks();
        }

        return {element, click}
    }

}


class MergedFeatureCollection {

    constructor(featureArrays,trackNames) {
        this.featureArrays = featureArrays;
        //trackNames is needed for the popup data to populate track names
        //preserving the order of the actual tracks
        this.trackNames = trackNames;
    }

    getMax(start, end) {
        let max = -Number.MAX_VALUE;

        for (let a of this.featureArrays) {
            if (Array.isArray(a)) {
                for (let f of a) {
                    if (typeof f.value === 'undefined' || Number.isNaN(f.value)) {
                        continue
                    }
                    if (f.end < start) {
                        continue
                    }
                    if (f.start > end) {
                        break
                    }
                    max = Math.max(max, f.value);
                }
            }
        }

        return max !== -Number.MAX_VALUE ? max : 100
    }

    // Covers cases in which a track has negative values.
    getMin(start, end) {
        let min = 0;
        for (let a of this.featureArrays) {
            if (Array.isArray(a)) {
                for (let f of a) {
                    if (typeof f.value !== 'undefined' && !Number.isNaN(f.value)) {
                        if (f.end < start) {
                            continue
                        }
                        if (f.start > end) {
                            break
                        }
                        min = Math.min(min, f.value);
                    }
                }
            }
        }
        return min
    }
}

/**
 * Heuristic for finding numeric tracks.
 *
 * @param tracks
 * @returns {*}
 */
const numericTracks = (tracks) => {
    return tracks ? tracks.filter(track => undefined !== track.dataRange || undefined !== track.autoscale || undefined !== track.autoscaleGroup) : []
};

class OverlayTrackButton extends NavbarButton {
    constructor(parent, browser) {

        super(parent, browser, 'Overlay Tracks', buttonLabel, overlayTrackImage, overlayTrackImageHover, false);

        this.button.addEventListener('mouseenter', () => this.setState(true));
        this.button.addEventListener('mouseleave', () => this.setState(false));

        const mouseClickHandler = () => {
            this.setVisibility(false);
            trackOverlayClickHandler.call(this);
        };

        this.boundMouseClickHandler = mouseClickHandler.bind(this);

        this.button.addEventListener('click', this.boundMouseClickHandler);

        this.setVisibility(true);

    }
}

async function trackOverlayClickHandler(e) {

    if (true === isOverlayTrackCriteriaMet(this.browser)) {

        const tracks = this.browser.getSelectedTrackViews().map(({track}) => track);
        for (const track of tracks) {
            track.selected = false;
        }

        // Flatten any merged tracks.  Must do this before their removal
        const flattenedTracks = [];
        for (let t of tracks) {
            if ("merged" === t.type) {
                flattenedTracks.push(...t.tracks);
            } else {
                flattenedTracks.push(t);
            }
        }

        const config =
            {
                name: 'Overlay',
                type: 'merged',
                autoscale: false,
                alpha: 0.5, //fudge * (1.0/tracks.length),
                height: Math.max(...tracks.map(({height}) => height)),
                order: Math.min(...tracks.map(({order}) => order)),
            };

        const mergedTrack = new MergedTrack(config, this.browser, flattenedTracks);

        for (const track of tracks) {
            const idx = this.browser.trackViews.indexOf(track.trackView);
            this.browser.trackViews.splice(idx, 1);
            track.trackView.dispose();
        }

        this.browser.addTrack(mergedTrack);
        await mergedTrack.trackView.updateViews();
        this.browser.reorderTracks();
    }

}

function isOverlayTrackCriteriaMet(browser) {

    const selected = browser.getSelectedTrackViews();

    if (selected && selected.length > 1) {

        const criteriaSet = new Set(['wig', 'merged']);

        const list = selected.filter(({track}) => criteriaSet.has(track.type));

        return list.length > 1

    } else {
        return false
    }

}

const igv_axis_column_width = 50;
const scrollbarExclusionTypes = new Set(['sequence', 'ruler', 'ideogram']);
const colorPickerExclusionTypes = new Set(['ruler', 'sequence', 'ideogram']);

class TrackView {

    constructor(browser, columnContainer, track) {

        this.namespace = `trackview-${guid$2()}`;

        this.browser = browser;
        this.track = track;
        track.trackView = this;

        this.addDOMToColumnContainer(browser, columnContainer, browser.referenceFrameList);

    }

    /**
     * Start a spinner for the track on any of its viewports.  In practice this is called during initialization
     * when there is only one.
     */
    startSpinner() {
        if (this.viewports && this.viewports.length > 0) {
            this.viewports[0].startSpinner();
        }
    }

    stopSpinner() {
        if (this.viewports && this.viewports.length > 0) {
            this.viewports[0].stopSpinner();
        }
    }

    addDOMToColumnContainer(browser, columnContainer, referenceFrameList) {

        // Axis
        this.axis = this.createAxis(browser, this.track);

        this.createViewports(browser, columnContainer, referenceFrameList);

        // Sample Info
        this.sampleInfoViewport = new SampleInfoViewport(this, browser.columnContainer.querySelector('.igv-sample-info-column'), browser.getSampleInfoViewportWidth());

        // SampleName Viewport
        this.sampleNameViewport = new SampleNameViewport(this, browser.columnContainer.querySelector('.igv-sample-name-column'), undefined, browser.getSampleNameViewportWidth());

        // Track Scrollbar
        this.createTrackScrollbar(browser);

        // Track Drag
        this.createTrackDragHandle(browser);

        // Track Gear
        this.createTrackGearPopup(browser);

    }

    /**
     * Create a viewport object for each reference frame.  It is assumed that viewport DOM objects have already been
     * created.   Obviously this is rather fragile, refactoring here would be a good idea.
     *
     * @param browser
     * @param columnContainer
     * @param referenceFrameList
     */
    createViewports(browser, columnContainer, referenceFrameList) {

        this.viewports = [];
        const viewportWidth = browser.calculateViewportWidth(referenceFrameList.length);
        const viewportColumns = columnContainer.querySelectorAll('.igv-column');
        for (let i = 0; i < viewportColumns.length; i++) {
            const viewport = createViewport(this, viewportColumns[i], referenceFrameList[i], viewportWidth);
            this.viewports.push(viewport);
        }
        if (typeof this.track.createGroupLabels === 'function') {
            this.track.createGroupLabels();
        }
    }

    /**
     * Return the last (rightmost) track viewport.  Normally this is the only one, but in multilocus view there may be
     * several
     * @returns {undefined|*} The last viewport, or undefined if there are no viewports
     */
    getLastViewport() {
        if (this.viewports && this.viewports.length > 0) {
            return this.viewports[this.viewports.length - 1]
        } else {
            return undefined
        }
    }

    createAxis(browser, track) {

        const axis = div$2();
        this.axis = axis;

        browser.columnContainer.querySelector('.igv-axis-column').appendChild(axis);

        axis.dataset.tracktype = track.type;

        axis.style.height = `${track.height}px`;

        if (typeof track.paintAxis === 'function') {

            const {width, height} = axis.getBoundingClientRect();
            this.axisCanvas = document.createElement('canvas');
            this.axisCanvas.style.width = `${width}px`;
            this.axisCanvas.style.height = `${height}px`;
            axis.appendChild(this.axisCanvas);
        }

        if (false === multiTrackSelectExclusionTypes.has(this.track.type)) {

            this.trackSelectionContainer = div$2();
            axis.appendChild(this.trackSelectionContainer);

            const html = `<input type="checkbox" name="track-select">`;
            const input = document.createRange().createContextualFragment(html).firstChild;
            this.trackSelectionContainer.appendChild(input);
            input.checked = this.track.selected || false;

            input.addEventListener('change', event => {
                event.preventDefault();
                event.stopPropagation();
                this.track.selected = event.target.checked;
                this.setDragHandleSelectionState(event.target.checked);
                this.browser.overlayTrackButton.setVisibility(isOverlayTrackCriteriaMet(this.browser));
            });

            this.enableTrackSelection(false);

        }

        return axis

    }

    resizeAxisCanvas(width, height) {

        this.axis.style.width = `${width}px`;
        this.axis.style.height = `${height}px`;

        if (typeof this.track.paintAxis === 'function') {
            // Size the canvas in CSS (logical) pixels.  The buffer size will be set when painted.
            this.axisCanvas.style.width = `${width}px`;
            this.axisCanvas.style.height = `${height}px`;

        }
    }

    renderSVGContext(context, {deltaX, deltaY}) {

        renderSVGAxis(context, this.track, this.axisCanvas, deltaX, deltaY);

        const {width: axisWidth} = this.axis.getBoundingClientRect();

        const {y} = this.viewports[0].viewportElement.getBoundingClientRect();

        let delta =
            {
                deltaX: axisWidth + deltaX,
                deltaY: y + deltaY
            };

        for (let viewport of this.viewports) {
            viewport.renderSVGContext(context, delta);
            const {width} = viewport.viewportElement.getBoundingClientRect();
            delta.deltaX += width;
        }

        if (true === this.browser.sampleInfo.isInitialized() && true === this.browser.sampleInfoControl.showSampleInfo) {
            this.sampleInfoViewport.renderSVGContext(context, delta);
            const {width} = this.sampleInfoViewport.viewport.getBoundingClientRect();
            delta.deltaX += width;
        }

        if (true === this.browser.showSampleNames) {
            this.sampleNameViewport.renderSVGContext(context, delta);
        }
    }

    presentColorPicker(colorSelection, event) {

        if (false === colorPickerExclusionTypes.has(this.track.type)) {

            let initialTrackColor;

            if (colorSelection === 'color') {
                initialTrackColor = this.track._initialColor || this.track.constructor.defaultColor;
            } else {
                initialTrackColor = this.track._initialAltColor || this.track.constructor.defaultColor;
            }

            let colorHandlers;
            const selected = this.browser.getSelectedTrackViews();
            if (selected.length > 0 && new Set(selected).has(this)) {

                colorHandlers =
                    {
                        color: rgbString => {
                            for (const trackView of selected) {
                                trackView.track.color = rgbString;
                                trackView.repaintViews();
                            }
                        },
                        altColor: rgbString => {
                            for (const trackView of selected) {
                                trackView.track.altColor = rgbString;
                                trackView.repaintViews();
                            }
                        },
                    };
            } else {
                colorHandlers =
                    {
                        color: hex => {
                            this.track.color = hexToRGB(hex);
                            this.repaintViews();
                        },
                        altColor: hex => {
                            this.track.altColor = hexToRGB(hex);
                            this.repaintViews();
                        }
                    };
            }

            const moreColorsPresentationColor = 'color' === colorSelection ? (this.track.color || this.track.constructor.defaultColor) : (this.track.altColor || this.track.constructor.defaultColor);
            this.browser.genericColorPicker.configure(initialTrackColor, colorHandlers[colorSelection], moreColorsPresentationColor);
            this.browser.genericColorPicker.present(event);

        }

    }

    setTrackHeight(newHeight, force) {

        if (!force) {
            if (this.track.minHeight) {
                newHeight = Math.max(this.track.minHeight, newHeight);
            }
            if (this.track.maxHeight) {
                newHeight = Math.min(this.track.maxHeight, newHeight);
            }
        }

        this.track.height = newHeight;

        this.resizeAxisCanvas(this.axis.clientWidth, this.track.height);

        if (typeof this.track.paintAxis === 'function') {
            this.paintAxis();
        }

        for (let vp of this.viewports) {
            vp.setHeight(newHeight);
        }

        this.sampleInfoViewport.setHeight(newHeight);

        this.sampleNameViewport.viewport.style.height = `${newHeight}px`;

        // If the track does not manage its own content height set it equal to the viewport height here
        if (typeof this.track.computePixelHeight !== "function") {
            for (let vp of this.viewports) {
                vp.setContentHeight(newHeight);
            }
        }

        this.repaintViews();

        this.updateScrollbar();

        this.dragHandle.style.height = `${newHeight}px`;
        this.gearContainer.style.height = `${newHeight}px`;

    }

    updateScrollbar() {

        const viewportHeight = this.viewports[0].viewportElement.clientHeight;
        this.outerScroll.style.height = `${viewportHeight}px`;

        if (false === scrollbarExclusionTypes.has(this.track.type)) {

            const viewportContentHeight = this.maxViewportContentHeight();
            const innerScrollHeight = Math.round((viewportHeight / viewportContentHeight) * viewportHeight);

            if (viewportContentHeight > viewportHeight) {
                this.innerScroll.style.display = 'block';
                this.innerScroll.style.height = `${innerScrollHeight}px`;

            } else {
                this.innerScroll.style.display = 'none';
            }
        }
    }

    setTop(contentTop) {
        for (let viewport of this.viewports) {
            viewport.setTop(contentTop);
        }
        this.sampleInfoViewport.setTop(contentTop);
        this.sampleNameViewport.setTop(contentTop);
    }

    moveScroller(delta) {

        const y = this.innerScroll.offsetTop + delta;
        const top = Math.min(Math.max(0, y), this.outerScroll.clientHeight - this.innerScroll.clientHeight);
        this.innerScroll.style.top = `${top}px`;

        const contentHeight = this.maxViewportContentHeight();
        const contentTop = Math.round(top * (contentHeight / this.viewports[0].viewportElement.clientHeight));
        this.setTop(contentTop);
    }

    /**
     * Scroll the content vertically by 'delta' pixels in track view coordinates.
     * @param {number} delta - The number of pixels to scroll vertically.
     */
    scrollByPixels(delta) {


        const currentTop = this.viewports[0].getContentTop();  // Bit of a hack, contentTop is the same for all viewports

        const contentHeight = this.maxViewportContentHeight();
        const maxContentTop = contentHeight - this.viewports[0].viewportElement.clientHeight;
        const newTop = Math.min(Math.max(0, currentTop + delta), maxContentTop);

        this.setTop(newTop);

        if (this.innerScroll) {
            const viewportHeight = this.viewports[0].viewportElement.clientHeight;
            const top = Math.round(newTop * (viewportHeight / contentHeight));
            this.innerScroll.style.top = `${top}px`;
        }
    }


    /**
     * Repaint all viewports without loading any new data.   Use this for events that change visual aspect of data,
     * e.g. color, sort order, etc, but do not change the genomic state.
     */
    repaintViews() {

        for (let viewport of this.viewports) {
            if (viewport.isVisible()) {
                viewport.repaint();
            }
        }

        if (typeof this.track.paintAxis === 'function') {
            this.paintAxis();
        }

        this.repaintSampleInfo();

        this.repaintSamples();
    }

    repaintSampleInfo() {

        this.sampleInfoViewport.repaint();
    }

    repaintSamples() {

        if (typeof this.track.getSamples === 'function') {
            const samples = this.track.getSamples();
            if (samples.names && samples.names.length > 0) {
                this.sampleNameViewport.repaint(samples);
            }

        }
    }

    // track labels
    setTrackLabelName(name) {
        this.viewports.forEach(viewport => viewport.setTrackLabel(name));
    }

    /**
     * Called in response to a window resize event, change in # of multilocus panels, or other event that changes
     * the width of the track view.
     *
     * @param viewportWidth  The width of each viewport in this track view.
     */
    resize(viewportWidth) {
        for (let viewport of this.viewports) {
            viewport.setWidth(viewportWidth);
        }
    }

    /**
     * Update viewports to reflect current genomic state, possibly loading additional data.
     *
     * @param force - if true, force a repaint even if no new data is loaded
     * @returns {Promise<void>}
     */
    async updateViews() {

        if (!(this.browser && this.browser.referenceFrameList)) return

        const visibleViewports = this.viewports.filter(viewport => viewport.isVisible());

        // Shift viewports left/right to current genomic state (pans canvas)
        visibleViewports.forEach(viewport => viewport.shift());

        // If dragging (panning) return
        if (this.browser.dragObject) {
            return
        }

        // Filter zoomed out views.  This has the side effect or turning off or no the zoomed out notice
        const viewportsToRepaint = visibleViewports.filter(vp => vp.needsRepaint()).filter(viewport => viewport.checkZoomIn());

        // Get viewports that require a data load
        const viewportsToReload = visibleViewports.filter(viewport => viewport.checkZoomIn()).filter(viewport => viewport.needsReload());

        // Trigger viewport to load features needed to cover current genomic range
        // NOTE: these must be loaded synchronously, do not user Promise.all,  not all file readers are thread safe
        for (let viewport of viewportsToReload) {
            await viewport.loadFeatures();
        }

        if (this.disposed) return   // Track was removed during load

        // Special case for variant tracks in multilocus view.  The # of rows to allocate to the variant (site)
        // section depends on data from all the views.  We only need to adjust this however if any data was loaded
        // (i.e. reloadableViewports.length > 0)
        if (this.track && typeof this.track.variantRowCount === 'function' && viewportsToReload.length > 0) {
            let maxRow = 0;
            for (let viewport of this.viewports) {
                if (viewport.featureCache && viewport.featureCache.features) {
                    maxRow = Math.max(maxRow, viewport.featureCache.features.reduce((a, f) => Math.max(a, f.row || 0), 0));
                }
            }
            const current = this.track.nVariantRows;
            if (current !== maxRow + 1) {
                this.track.variantRowCount(maxRow + 1);
                for (let viewport of this.viewports) {
                    viewport.checkContentHeight();
                }
            }
        }

        // Autoscale
        let mergeAutocale;
        if ("merged" === this.track.type) {
            // Merged tracks handle their own scaling
            mergeAutocale = this.track.updateScales(visibleViewports);

        } else if (this.track.autoscale) {
            let allFeatures = [];
            for (let visibleViewport of visibleViewports) {
                const referenceFrame = visibleViewport.referenceFrame;
                const start = referenceFrame.start;
                const end = start + referenceFrame.toBP(visibleViewport.getWidth());

                if (visibleViewport.featureCache && visibleViewport.featureCache.features) {

                    // If the "features" object has a getMax function use it.  Currently alignmentContainer
                    // implements this for coverage and Merged track for its wig tracks.
                    if (typeof visibleViewport.featureCache.features.getMax === 'function') {
                        const max = visibleViewport.featureCache.features.getMax(start, end);
                        allFeatures.push({value: max});

                        // If the "features" object also has a getMin function use it.  Currently Merged track implements
                        // this for its wig tracks.
                        if (typeof visibleViewport.featureCache.features.getMin === 'function') {
                            const min = visibleViewport.featureCache.features.getMin(start, end);
                            allFeatures.push({value: min});
                        }

                    } else {
                        const viewFeatures = FeatureUtils.findOverlapping(visibleViewport.featureCache.features, start, end);
                        for (let f of viewFeatures) {
                            allFeatures.push(f);
                        }
                    }
                }
            }


            if (typeof this.track.doAutoscale === 'function') {
                this.track.dataRange = this.track.doAutoscale(allFeatures);
            } else {
                this.track.dataRange = doAutoscale(allFeatures);
            }
        }

        const refreshView = (this.track.autoscale || this.track.autoscaleGroup || this.track.type === 'ruler' || mergeAutocale || this.track.groupBy);
        for (let vp of visibleViewports) {
            if (viewportsToRepaint.includes(vp)) {
                vp.repaint();
            } else if (refreshView) {
                vp.refresh();
            }
        }

        this.adjustTrackHeight();

        this.repaintSampleInfo();

        this.repaintSamples();

        this.updateRulerViewportLabels();
    }

    clearCachedFeatures() {
        for (let viewport of this.viewports) {
            viewport.clearCache();
        }
    }

    updateRulerViewportLabels() {

        const viewportWidth = this.browser.calculateViewportWidth(this.viewports.length);

        for (let viewport of this.viewports) {
            if ('ruler' === this.track.type) {
                if (this.viewports.length > 1) {
                    viewport.presentLocusLabel(viewportWidth);
                } else {
                    viewport.dismissLocusLabel();
                }
            }
        }

    }

    /**
     * Return a promise to get all in-view features across all viewports.  Used for group autoscaling.
     */
    async getInViewFeatures() {

        if (!(this.browser && this.browser.referenceFrameList)) {
            return []
        }

        let allFeatures = [];
        const visibleViewports = this.viewports.filter(viewport => viewport.isVisible());
        for (let vp of visibleViewports) {

            const referenceFrame = vp.referenceFrame;
            const {chr, start, bpPerPixel} = vp.referenceFrame;
            const end = start + referenceFrame.toBP(vp.getWidth());
            const needsReload = !vp.featureCache || !vp.featureCache.containsRange(chr, start, end, bpPerPixel);

            if (needsReload) {
                await vp.loadFeatures();
            }
            if (vp.featureCache && vp.featureCache.features) {

                if (typeof vp.featureCache.features.getMax === 'function') {
                    const max = vp.featureCache.features.getMax(start, end);
                    allFeatures.push({value: max});
                } else {
                    const vpFeatures = typeof vp.featureCache.queryFeatures === 'function' ?
                        vp.featureCache.queryFeatures(chr, start, end) :
                        FeatureUtils.findOverlapping(vp.featureCache.features, start, end);
                    allFeatures = allFeatures.concat(vpFeatures);
                }
            }
        }
        return allFeatures
    }

    checkContentHeight() {

        for (let viewport of this.viewports) {
            viewport.checkContentHeight();
        }
        this.adjustTrackHeight();

    }

    adjustTrackHeight() {

        var contentHeight = this.maxViewportContentHeight();
        if (this.track.autoHeight) {
            this.setTrackHeight(contentHeight, false);
        } else if (this.track.paintAxis) {   // Avoid duplication, paintAxis is already called in setTrackHeight
            this.paintAxis();
        }

        if (false === scrollbarExclusionTypes.has(this.track.type)) {

            // Adjust top, if needed, to insure content is in view
            const currentTop = this.viewports[0].getContentTop();
            const viewportHeight = this.viewports[0].viewportElement.clientHeight;
            const minTop = Math.min(0, viewportHeight - contentHeight);
            if (currentTop < minTop) {
                this.setTop(minTop);
            }

            this.updateScrollbar();
            this.moveScroller(0);

        }

    }

    createTrackScrollbar(browser) {

        const outerScroll = div$2();
        browser.columnContainer.querySelector('.igv-scrollbar-column').appendChild(outerScroll);
        outerScroll.style.height = `${this.track.height}px`;
        this.outerScroll = outerScroll;

        if (false === scrollbarExclusionTypes.has(this.track.type)) {
            const innerScroll = div$2();
            outerScroll.appendChild(innerScroll);
            this.innerScroll = innerScroll;

            this.addTrackScrollMouseHandlers(browser);
        }

    }

    createTrackDragHandle(browser) {

        if ('sequence' !== this.track.type && true === multiTrackSelectExclusionTypes.has(this.track.type)) {
            this.dragHandle = div$2({class: 'igv-track-drag-shim'});
        } else {
            this.dragHandle = div$2({class: 'igv-track-drag-handle'});
            this.dragHandle.classList.add('igv-track-drag-handle-color');
        }

        browser.columnContainer.querySelector('.igv-track-drag-column').appendChild(this.dragHandle);
        this.dragHandle.style.height = `${this.track.height}px`;
        this.addTrackDragMouseHandlers(browser);
    }

    createTrackGearPopup(browser) {

        this.gearContainer = div$2();
        browser.columnContainer.querySelector('.igv-gear-menu-column').appendChild(this.gearContainer);
        this.gearContainer.style.height = `${this.track.height}px`;

        if (true === this.track.ignoreTrackMenu) ; else {

            this.gear = div$2();
            this.gearContainer.appendChild(this.gear);
            this.gear.appendChild(createIcon$1('cog'));

            this.trackGearPopup = new MenuPopup(this.gear);

            this.boundTrackGearClickHandler = trackGearClickHandler.bind(this);
            this.gear.addEventListener('click', this.boundTrackGearClickHandler);

            function trackGearClickHandler(event) {
                event.preventDefault();
                event.stopPropagation();

                if ('none' === this.trackGearPopup.popover.style.display) {

                    for (const otherTrackView of browser.trackViews.filter(t => t !== this && undefined !== t.trackGearPopup)) {
                        otherTrackView.trackGearPopup.popover.style.display = 'none';
                    }

                    this.trackGearPopup.presentMenuList(this, browser.menuUtils.trackMenuItemList(this));
                } else {
                    this.trackGearPopup.popover.style.display = 'none';
                }
            }

        }

    }

    addTrackScrollMouseHandlers(browser) {

        // Mouse Down
        this.boundTrackScrollMouseDownHandler = trackScrollMouseDownHandler.bind(this);
        this.innerScroll.addEventListener('mousedown', this.boundTrackScrollMouseDownHandler);

        function trackScrollMouseDownHandler(event) {

            event.stopPropagation();

            const {y} = pageCoordinates(event);

            this.innerScroll.dataset.yDown = y.toString();

            this.boundColumnContainerMouseMoveHandler = columnContainerMouseMoveHandler.bind(this);
            browser.columnContainer.addEventListener('mousemove', this.boundColumnContainerMouseMoveHandler);

            function columnContainerMouseMoveHandler(event) {

                event.stopPropagation();

                const {y} = pageCoordinates(event);

                this.moveScroller(y - parseInt(this.innerScroll.dataset.yDown));

                this.innerScroll.dataset.yDown = y.toString();


            }
        }

        this.boundColumnContainerMouseUpHandler = columnContainerMouseUpHandler.bind(this);
        browser.columnContainer.addEventListener('mouseup', this.boundColumnContainerMouseUpHandler);
        browser.columnContainer.addEventListener('mouseleave', this.boundColumnContainerMouseUpHandler);

        function columnContainerMouseUpHandler(event) {
            browser.columnContainer.removeEventListener('mousemove', this.boundColumnContainerMouseMoveHandler);
        }

    }

    removeTrackScrollMouseHandlers() {
        if (false === scrollbarExclusionTypes.has(this.track.type)) {
            this.innerScroll.removeEventListener('mousedown', this.boundTrackScrollMouseDownHandler);
            this.browser.columnContainer.removeEventListener('mouseup', this.boundColumnContainerMouseUpHandler);
            this.browser.columnContainer.removeEventListener('mousemove', this.boundColumnContainerMouseMoveHandler);
            this.browser.columnContainer.removeEventListener('mouseleave', this.boundColumnContainerMouseMoveHandler);
        }
    }

    addTrackDragMouseHandlers(browser) {

        if ('sequence' === this.track.type || false === multiTrackSelectExclusionTypes.has(this.track.type)) {

            let currentDragHandle = undefined;

            // Mouse Down
            this.boundTrackDragMouseDownHandler = trackDragMouseDownHandler.bind(this);
            this.dragHandle.addEventListener('mousedown', this.boundTrackDragMouseDownHandler);

            function trackDragMouseDownHandler(event) {

                event.preventDefault();

                currentDragHandle = event.target;
                if (false === this.track.selected || 'sequence' === this.track.type) {
                    currentDragHandle.classList.remove('igv-track-drag-handle-color');
                    currentDragHandle.classList.add('igv-track-drag-handle-hover-color');
                }

                browser.startTrackDrag(this);

            }

            // Mouse Up
            this.boundDocumentTrackDragMouseUpHandler = documentTrackDragMouseUpHandler.bind(this);
            document.addEventListener('mouseup', this.boundDocumentTrackDragMouseUpHandler);

            function documentTrackDragMouseUpHandler(event) {

                browser.endTrackDrag();

                if (currentDragHandle && event.target !== currentDragHandle) {

                    if (false === this.track.selected || 'sequence' === this.track.type) {
                        currentDragHandle.classList.remove('igv-track-drag-handle-hover-color');
                        currentDragHandle.classList.add('igv-track-drag-handle-color');
                    }

                }

                currentDragHandle = undefined;
            }

            // Mouse Enter
            this.boundTrackDragMouseEnterHandler = trackDragMouseEnterHandler.bind(this);
            this.dragHandle.addEventListener('mouseenter', this.boundTrackDragMouseEnterHandler);

            function trackDragMouseEnterHandler(event) {
                event.preventDefault();

                if (undefined === currentDragHandle) {
                    if (false === this.track.selected || 'sequence' === this.track.type) {
                        event.target.classList.remove('igv-track-drag-handle-color');
                        event.target.classList.add('igv-track-drag-handle-hover-color');
                    }
                }

                browser.updateTrackDrag(this);

            }

            // Mouse Out
            this.dragHandle.addEventListener('mouseout', event => {
                event.preventDefault();

                if (undefined === currentDragHandle) {
                    if (false === this.track.selected || 'sequence' === this.track.type) {
                        event.target.classList.remove('igv-track-drag-handle-hover-color');
                        event.target.classList.add('igv-track-drag-handle-color');
                    }
                }
            });

            this.boundTrackDragMouseOutHandler = trackDragMouseOutHandler.bind(this);
            this.dragHandle.addEventListener('mouseout', this.boundTrackDragMouseOutHandler);

            function trackDragMouseOutHandler(event) {
                event.preventDefault();

                if (undefined === currentDragHandle) {
                    if (false === this.track.selected || 'sequence' === this.track.type) {
                        event.target.classList.remove('igv-track-drag-handle-hover-color');
                        event.target.classList.add('igv-track-drag-handle-color');
                    }
                }
            }

        }

    }

    removeTrackDragMouseHandlers() {

        if ('ideogram' === this.track.type || 'ruler' === this.track.type) ; else {
            this.dragHandle.removeEventListener('mousedown', this.boundTrackDragMouseDownHandler);
            document.removeEventListener('mouseup', this.boundDocumentTrackDragMouseUpHandler);
            this.dragHandle.removeEventListener('mouseup', this.boundTrackDragMouseEnterHandler);
            this.dragHandle.removeEventListener('mouseout', this.boundTrackDragMouseOutHandler);
        }

    }

    removeTrackGearMouseHandlers() {
        if (true === this.track.ignoreTrackMenu) ; else {
            this.gear.removeEventListener('click', this.boundTrackGearClickHandler);
        }

    }

    removeDOMFromColumnContainer() {

        // Axis
        this.axis.remove();
        this.removeViewportsFromColumnContainer();

        // Sample Info Viewport
        this.sampleInfoViewport.dispose();

        // SampleName Viewport
        this.sampleNameViewport.dispose();

        // empty trackScrollbar Column
        this.removeTrackScrollMouseHandlers();
        this.outerScroll.remove();

        // empty trackDrag Column
        this.removeTrackDragMouseHandlers();
        this.dragHandle.remove();

        // empty trackGear Column
        this.removeTrackGearMouseHandlers();
        this.gearContainer.remove();

    }

    removeViewportsFromColumnContainer() {
        // Track Viewports
        for (let viewport of this.viewports) {
            viewport.viewportElement.remove();
        }
    }

    /**
     * Do any cleanup here
     */
    dispose() {

        this.axis.remove();

        for (let viewport of this.viewports) {
            viewport.dispose();
        }

        this.sampleInfoViewport.dispose();

        this.sampleNameViewport.dispose();

        this.removeTrackScrollMouseHandlers();
        this.outerScroll.remove();

        this.removeTrackDragMouseHandlers();
        this.dragHandle.remove();

        this.removeTrackGearMouseHandlers();
        this.gearContainer.remove();

        if (typeof this.track.dispose === "function") {
            this.track.dispose();
        }

        for (let key of Object.keys(this)) {
            this[key] = undefined;
        }

        if (this.alert) {
            this.alert.container.remove();    // This is quite obviously a hack, need a "dispose" method on AlertDialog
        }

        this.disposed = true;
    }

    paintAxis() {

        if (typeof this.track.paintAxis === 'function') {

            // Set the canvas buffer size, this is the resolution it is drawn at.  This is done here in case the browser
            // has been drug between screens at different dpi resolutions since the last repaint
            const {width, height} = this.axisCanvas.getBoundingClientRect();
            const dpi = window.devicePixelRatio || 1;
            this.axisCanvas.height = dpi * height;
            this.axisCanvas.width = dpi * width;

            // Get a scaled context to draw aon
            const axisCanvasContext = this.axisCanvas.getContext('2d');
            axisCanvasContext.scale(dpi, dpi);

            if (this.track.autoscaleGroup) {

                if (undefined === autoScaleGroupColorHash[this.track.autoscaleGroup]) {
                    const colorPalette = colorPalettes['Dark2'];
                    const randomIndex = Math.floor(Math.random() * colorPalettes['Dark2'].length);
                    autoScaleGroupColorHash[this.track.autoscaleGroup] = colorPalette[randomIndex];
                }
                const rgba = IGVColor.addAlpha(autoScaleGroupColorHash[this.track.autoscaleGroup], 0.75);
                this.track.paintAxis(axisCanvasContext, width, height, rgba);
            } else {
                this.track.paintAxis(axisCanvasContext, width, height, undefined);
            }
        }
    }

    maxViewportContentHeight() {
        return Math.max(...this.viewports.map(viewport => viewport.getContentHeight()))
    }

    enableTrackSelection(doEnableMultiSelection) {

        const container = this.trackSelectionContainer;

        if (!container || multiTrackSelectExclusionTypes.has(this.track.type)) {
            return
        }

        if (false !== doEnableMultiSelection) {
            container.style.display = 'grid';
        } else {
            // If disabling selection set track selection state to false
            this.track.selected = false;

            const trackSelectInput = container.querySelector('[name=track-select]');
            trackSelectInput.checked = this.track.selected;

            if (this.dragHandle) {
                this.setDragHandleSelectionState(false);
            }

            container.style.display = 'none';
        }
    }

    setDragHandleSelectionState(isSelected) {

        const dragHandle = this.dragHandle;

        if (isSelected) {
            dragHandle.classList.remove('igv-track-drag-handle-color');
            dragHandle.classList.remove('igv-track-drag-handle-hover-color');
            dragHandle.classList.add('igv-track-drag-handle-selected-color');
        } else {
            dragHandle.classList.remove('igv-track-drag-handle-hover-color');
            dragHandle.classList.remove('igv-track-drag-handle-selected-color');
            dragHandle.classList.add('igv-track-drag-handle-color');
        }
    }

}

function renderSVGAxis(context, track, axisCanvas, deltaX, deltaY) {

    if (typeof track.paintAxis === 'function') {

        const {y, width, height} = axisCanvas.getBoundingClientRect();

        const str = (track.name || track.id).replace(/\W/g, '');
        const id = `${str}_axis_guid_${guid$2()}`;

        context.saveWithTranslationAndClipRect(id, deltaX, y + deltaY, width, height, 0);

        track.paintAxis(context, width, height);

        context.restore();
    }

}

const colorPickerTrackTypeSet = new Set(['bedtype', 'alignment', 'annotation', 'variant', 'wig', 'interact', 'shoebox']);

const vizWindowTypes = new Set(['alignment', 'annotation', 'variant', 'eqtl', 'qtl', 'snp', 'shoebox', 'wig']);

const multiTrackSelectExclusionTypes = new Set(['sequence', 'ruler', 'ideogram']);

const autoScaleGroupColorHash =
    {};

class MenuUtils {
    constructor(browser) {
        this.browser = browser;
        this.initialize();
    }

    initialize() {

        const panel = new Panel();
        panel.add('...');

        const config =
            {
                parent: this.browser.root,
                content: panel
            };

        this.dialog = new Dialog(config);
        this.browser.root.appendChild(this.dialog.elem);
        hide$1(this.dialog.elem);
    }

    trackMenuItemList(trackView) {

        const list = [];

        if (trackView.track.config.type !== 'sequence') {
            list.push(trackHeightMenuItem());
        }

        if (true === didMultiSelect(trackView)) {
            list.push(...this.multiSelectMenuItems(trackView));
        } else {
            if (trackView.track.config.type !== 'sequence') {
                list.push(trackRenameMenuItem());
            }
            list.push(...this.defaultMenuItems(trackView));
        }

        if (trackView.track.removable !== false) {
            list.push('<hr/>');
            list.push(trackRemovalMenuItem(trackView));
        }

        return list
    }

    defaultMenuItems(trackView) {

        const list = [];

        if (canShowColorPicker(trackView.track)) {

            list.push('<hr/>');
            list.push(colorPickerMenuItem(trackView, "Set track color", "color"));
            list.push(unsetColorMenuItem(trackView, "Unset track color"));

            if (trackView.track.config.type === 'wig' || trackView.track.config.type === 'annotation') {
                list.push(colorPickerMenuItem(trackView, "Set alt color", "altColor"));
                list.push(unsetAltColorMenuItem(trackView, "Unset alt color"));
            }

        }

        if (trackView.track.menuItemList) {
            list.push(...trackView.track.menuItemList());
        }

        if (isVisibilityWindowType(trackView)) {
            list.push('<hr/>');
            list.push(visibilityWindowMenuItem(trackView.track.type));
        }

        return list
    }

    multiSelectMenuItems(trackView) {

        const list = [];

        const selected = trackView.browser.getSelectedTrackViews();
        const isSingleTrackType = didSelectSingleTrackType(selected.map(({track}) => track.type));

        if (true === isSingleTrackType) {

            list.push(...this.defaultMenuItems(trackView));

            if ('wig' === trackView.track.type) {

                list.push('<hr/>');
                list.push(groupAutoScaleMenuItem());
            }

        } else {

            if (canShowColorPicker(trackView.track)) {

                list.push('<hr/>');
                list.push(colorPickerMenuItem(trackView, "Set track color", "color"));
                list.push(unsetColorMenuItem(trackView, "Unset track color"));

                if (trackView.track.config.type === 'wig' || trackView.track.config.type === 'annotation') {
                    list.push(colorPickerMenuItem(trackView, "Set alt color", "altColor"));
                    list.push(unsetAltColorMenuItem(trackView, "Unset alt color"));
                }

            }

        }

        return list

    }

}

function didMultiSelect(trackView) {
    const selected = trackView.browser.getSelectedTrackViews();
    return selected && selected.length > 1 && new Set(selected).has(trackView)
}

function isVisibilityWindowType(trackView) {
    const track = trackView.track;
    const hasVizWindow = track && track.config && track.config.visibilityWindow !== undefined;
    return hasVizWindow || (track && vizWindowTypes.has(track.type))
}

function groupAutoScaleMenuItem() {

    const element = document.createElement('div');
    element.textContent = 'Group autoscale';

    function click(e) {

        const colorPalette = colorPalettes['Dark2'];
        const randomIndex = Math.floor(Math.random() * colorPalette.length);

        const autoScaleGroupID = `auto-scale-group-${guid$2()}`;
        autoScaleGroupColorHash[autoScaleGroupID] = colorPalette[randomIndex];

        const multiSelectedTrackViews = this.browser.getSelectedTrackViews();
        for (const {track} of multiSelectedTrackViews) {
            track.autoscaleGroup = autoScaleGroupID;
        }

        this.browser.updateViews();
    }

    return {element, doAllMultiSelectedTracks: true, click};

}

function visibilityWindowMenuItem(trackType) {

    const element = document.createElement('div');
    element.textContent = 'Set visibility window';

    function click(e) {

        const callback = () => {

            let value = this.browser.inputDialog.value;
            value = '' === value || undefined === value ? -1 : value.trim();

            this.visibilityWindow = Number.parseInt(value);
            this.config.visibilityWindow = Number.parseInt(value);

            this.trackView.updateViews();
        };

        const label = 'wig' === trackType ?
            'Visibility window (bp). Enter 0 for whole chromosome, -1 for whole genome.' :
            'Visibility window (bp). Enter 0 for whole chromosome.';
        const config =
            {
                label,
                value: this.visibilityWindow,
                callback
            };
        this.browser.inputDialog.present(config, e);

    }

    return {element, click};

}

function trackRemovalMenuItem(trackView) {

    const str = trackView.track.selected ? 'Remove tracks' : 'Remove track';

    const element = document.createElement('div');
    element.textContent = str;

    function trackRemovalHandler(e) {
        this.trackView.browser._removeTrack(this);
    }

    return {element, click: trackRemovalHandler, menuItemType: 'removeTrack'};

}

function colorPickerMenuItem(trackView, label, option) {

    const element = document.createElement('div');
    element.textContent = label;

    const click = event => {
        trackView.presentColorPicker(option, event);
    };

    return {element, click};
}

function unsetColorMenuItem(trackView, label) {

    const element = document.createElement('div');
    element.textContent = label;

    return {
        element,
        click: () => {
            trackView.track.color = trackView.track._initialColor || trackView.track.constructor.defaultColor;
            trackView.repaintViews();
        }
    };
}

function unsetAltColorMenuItem(trackView, label) {

    const element = document.createElement('div');
    element.textContent = label;

    return {
        element,
        click: () => {
            trackView.track.altColor = trackView.track._initialAltColor || trackView.track.constructor.defaultColor;
            trackView.repaintViews();
        }
    };
}

function trackRenameMenuItem() {

    const element = document.createElement('div');
    element.textContent = 'Set track name';

    function click(e) {

        const callback = () => {
            let value = this.browser.inputDialog.value;
            value = ('' === value || undefined === value) ? 'untitled' : value.trim();
            this.name = value;
        };

        const config =
            {
                label: 'Track Name',
                value: (getTrackLabelText(this) || 'unnamed'),
                callback
            };

        this.browser.inputDialog.present(config, e);

    }

    return {element, click};
}

function trackHeightMenuItem() {

    const element = document.createElement('div');
    element.textContent = 'Set track height';

    function dialogHandler(e) {

        const callback = () => {

            if (this.browser.inputDialog.value !== undefined) {

                const number = parseInt(this.browser.inputDialog.value, 10);

                if (number > 0){

                    const tracks = [];
                    if (this.trackView.track.selected) {
                        tracks.push(...(this.trackView.browser.getSelectedTrackViews().map(({track}) => track)));
                    } else {
                        tracks.push(this);
                    }

                    for (const track of tracks) {
                        // Explicitly setting track height turns off autoHeight
                        track.trackView.autoHeight = false;

                        // If explicitly setting the height adjust min or max, if necessary
                        if (track.minHeight !== undefined && track.minHeight > number) {
                            track.minHeight = number;
                        }
                        if (track.maxHeight !== undefined && track.maxHeight < number) {
                            track.minHeight = number;
                        }
                        track.trackView.setTrackHeight(number, true);

                        track.trackView.checkContentHeight();
                        track.trackView.repaintViews();
                    } // for (tracks)

                } // if ()

            } // if ()
        };

        const config =
            {
                label: 'Track Height',
                value: this.height,
                callback
            };

        this.browser.inputDialog.present(config, e);

    }

    return {element, dialog: dialogHandler};

}

function getTrackLabelText(track) {
    return track.name
}

function canShowColorPicker(track) {
    return undefined === track.type || (colorPickerTrackTypeSet.has(track.type) && 'heatmap' !== track.graphType)
}

function didSelectSingleTrackType(types) {
    const unique = [...new Set(types)];
    return 1 === unique.length
}

class DataRangeDialog {

    constructor(browser, parent) {
        this.browser = browser;

        // Create dialog container
        this.container = document.createElement('div');
        this.container.className = 'igv-generic-dialog-container';
        parent.appendChild(this.container);

        // Create dialog header
        const header = document.createElement('div');
        header.className = 'igv-generic-dialog-header';
        this.container.appendChild(header);

        attachDialogCloseHandlerWithParent$1(header, () => {
            this.minimumInput.value = '';
            this.maximumInput.value = '';
            this.hide();
        });

        // Create minimum input
        this.minimum = document.createElement('div');
        this.minimum.className = 'igv-generic-dialog-label-input';
        this.container.appendChild(this.minimum);

        const minDiv = document.createElement('div');
        minDiv.textContent = 'Minimum';
        this.minimum.appendChild(minDiv);

        this.minimumInput = document.createElement('input');
        this.minimum.appendChild(this.minimumInput);

        // Create maximum input
        this.maximum = document.createElement('div');
        this.maximum.className = 'igv-generic-dialog-label-input';
        this.container.appendChild(this.maximum);

        const maxDiv = document.createElement('div');
        maxDiv.textContent = 'Maximum';
        this.maximum.appendChild(maxDiv);

        this.maximumInput = document.createElement('input');
        this.maximum.appendChild(this.maximumInput);

        // Create buttons container
        const buttons = document.createElement('div');
        buttons.className = 'igv-generic-dialog-ok-cancel';
        this.container.appendChild(buttons);

        // Create OK button
        this.okButton = document.createElement('div');
        this.okButton.textContent = 'OK';
        buttons.appendChild(this.okButton);

        // Create Cancel button
        this.cancelButton = document.createElement('div');
        this.cancelButton.textContent = 'Cancel';
        buttons.appendChild(this.cancelButton);

        // Attach cancel button handler
        this.cancelButton.addEventListener('click', () => {
            this.minimumInput.value = '';
            this.maximumInput.value = '';
            this.hide();
        });

        // Make the container draggable
        makeDraggable$1(this.container, header);

        // Initially hide the dialog
        this.container.style.display = 'none';
    }

    configure(trackViewOrTrackViewList) {
        let dataRange;

        // Determine the data range
        if (Array.isArray(trackViewOrTrackViewList)) {
            dataRange = { min: Number.MAX_SAFE_INTEGER, max: -Number.MAX_SAFE_INTEGER };
            for (const trackView of trackViewOrTrackViewList) {
                if (trackView.track.dataRange) {
                    dataRange.min = Math.min(trackView.track.dataRange.min, dataRange.min);
                    dataRange.max = Math.max(trackView.track.dataRange.max, dataRange.max);
                }
            }
        } else {
            dataRange = trackViewOrTrackViewList.track.dataRange;
        }

        // Populate input fields with data range
        if (dataRange) {
            this.minimumInput.value = dataRange.min;
            this.maximumInput.value = dataRange.max;
        }

        this.minimumInput.onkeyup = null;
        this.minimumInput.onkeyup = e => {
            if (e.keyCode === 13) { // Enter key
                this.processResults(trackViewOrTrackViewList);
            }
            e.stopImmediatePropagation();
        };

        this.maximumInput.onkeyup = null;
        this.maximumInput.onkeyup = e=> {
            if (e.keyCode === 13) { // Enter key
                e.stopImmediatePropagation();
                this.processResults(trackViewOrTrackViewList);
            }
        };

        this.okButton.onclick = null;
        this.okButton.onclick = () => {
            this.processResults(trackViewOrTrackViewList);
        };
    }

    processResults(trackViewOrTrackViewList) {
        const minValue = this.minimumInput.value.trim();
        const maxValue = this.maximumInput.value.trim();

        if (minValue !== '' && maxValue !== '') {
            const min = Number(minValue);
            const max = Number(maxValue);

            if (isNaN(min) || isNaN(max)) {
                this.browser.alert.present(new Error('Must input numeric values'), undefined);
            } else {
                const list = Array.isArray(trackViewOrTrackViewList)
                    ? trackViewOrTrackViewList
                    : [trackViewOrTrackViewList];

                for (const trackView of list) {
                    trackView.track.setDataRange({ min, max });
                }
            }

            // Clear the input fields
            this.minimumInput.value = '';
            this.maximumInput.value = '';
        }

        this.hide();
    }

    hide(){
        this.container.style.top = '20%';
        this.container.style.left = '75%';
        this.container.style.display = 'none';
    }

    present(e) {
        const { top} = e.currentTarget.parentElement.getBoundingClientRect();
        this.container.style.top = `${ top }px`;
        this.container.style.display = 'flex';
    }

}

class InputDialog {

    constructor(parent) {

        this.parent = parent;

        // dialog container
        this.container = div$2({class: 'igv-ui-generic-dialog-container'});
        parent.appendChild(this.container);

        // dialog header
        const header = div$2({class: 'igv-ui-generic-dialog-header'});
        this.container.appendChild(header);

        // dialog label
        this.label = div$2({class: 'igv-ui-generic-dialog-one-liner'});
        this.container.appendChild(this.label);
        this.label.textContent = 'Unlabeled';

        // input container
        this.input_container = div$2({class: 'igv-ui-generic-dialog-input'});
        this.container.appendChild(this.input_container);

        // input element.  DO NOT ACCESS THIS OUTSIDE OF THIS CLASS
        this._input = document.createElement("input");
        this.input_container.appendChild(this._input);


        // ok | cancel
        const buttons = div$2({class: 'igv-ui-generic-dialog-ok-cancel'});
        this.container.appendChild(buttons);

        // ok
        this.ok = div$2();
        buttons.appendChild(this.ok);
        this.ok.textContent = 'OK';

        // cancel
        this.cancel = div$2();
        buttons.appendChild(this.cancel);
        this.cancel.textContent = 'Cancel';

        hide$1(this.container);

        this._input.addEventListener('keyup', e => {
            if ('Enter' === e.code) {
                if (typeof this.callback === 'function') {
                    this.callback(this._input.value);
                    this.callback = undefined;
                }
                this._input.value = undefined;
                hide$1(this.container);
            }
            e.stopImmediatePropagation();   // Prevent key event to cause track keyboard navigation ("next feature")
        });

        this.ok.addEventListener('click', () => {
            if (typeof this.callback === 'function') {
                this.callback(this._input.value);
                this.callback = undefined;
            }
            this._input.value = undefined;
            hide$1(this.container);
        });

        const cancel = () => {
            this._input.value = '';
            hide$1(this.container);
        };

        this.cancel.addEventListener('click', cancel);

        attachDialogCloseHandlerWithParent$1(header, cancel);
        makeDraggable$1(this.container, header);

    }

    get value() {
        return purify.sanitize(this._input.value)
    }


    present(options, e) {
        this.label.textContent = options.label;
        this._input.value = options.value;
        this.callback = options.callback || options.click;

        this.container.style.display = '';
        
        // Get click coordinates
        const clickX = e.clientX;
        const clickY = e.clientY;
        
        // Get dialog dimensions
        const dialogWidth = this.container.offsetWidth;
        const dialogHeight = this.container.offsetHeight;
        
        // Calculate available space
        const windowWidth = window.innerWidth;
        const windowHeight = window.innerHeight;
        
        // Calculate position to keep dialog on screen
        let left = clickX;
        let top = clickY;
        
        // Adjust horizontal position if dialog would go off screen
        if (left + dialogWidth > windowWidth) {
            left = windowWidth - dialogWidth - 10; // 10px padding from edge
        }
        
        // Adjust vertical position if dialog would go off screen
        if (top + dialogHeight > windowHeight) {
            top = windowHeight - dialogHeight - 10; // 10px padding from edge
        }
        
        // Ensure minimum distance from edges
        left = Math.max(10, left);
        top = Math.max(10, top);
        
        // Apply positions
        this.container.style.left = `${left}px`;
        this.container.style.top = `${top}px`;
    }
}

class GenericContainer {

    constructor({parent,  top, left, width, height, border, closeHandler}) {

        const container = div$2({class: 'igv-ui-generic-container'});
        parent.appendChild(container);

        this.container = container;

        if (width !== undefined) {
            this.container.style.width = `${ width }px`;
        }
        if (height !== undefined) {
            this.container.style.height = `${ height }px`;
        }
        if(border) {
            this.container.style.border = border;
        }

        // header
        const header = div$2();
        this.container.appendChild(header);

        // close button
        attachDialogCloseHandlerWithParent$1(header, (e) => {
            if(typeof closeHandler === "function") {
                closeHandler(e);
            }
            this.hide();
        });

        makeDraggable$1(this.container, header);

        this.hide();
    }

    show() {
        this.container.style.display = 'flex';
    }

    hide() {
        this.container.style.display = 'none';
    }

    dispose() {
        if(this.container.parent)  {
            this.container.parent.removeChild(this.container);
        }
    }
}

class GenericColorPicker extends GenericContainer {

    static maxRecentColors = 10

    constructor({parent, width}) {
        super({parent, width, border: '1px solid gray'});

        this.container.classList.add('igv-ui-colorpicker-container');

        // nth-child(2) - Color Swatches
        this.colorSwatchContainer = div$2();
        this.container.appendChild(this.colorSwatchContainer);

        // nth-child(3) - More Colors interative color picker
        this.moreColorsContainer = div$2();
        this.container.appendChild(this.moreColorsContainer);

        // nth-child(4) - Recent Colors - swatches
        this.recentColorsSwatches = div$2();
        this.container.appendChild(this.recentColorsSwatches);

        this.recentColors = [];

        this.moreColorsPresentationColor = undefined;

    }

    configure(initialTrackColor, colorHandler, moreColorsPresentationColor) {

        this.moreColorsPresentationColor = moreColorsPresentationColor;

        this.colorSwatchContainer.innerHTML = '';

        this.recentColorsSwatches.innerHTML = '';

        // Populate ColorSwatches
        const hexColorStrings = Object.values(genericColorPickerPalette);
        for (const hexColorString of hexColorStrings) {
            const swatch = div$2({class: 'igv-ui-color-swatch'});
            this.colorSwatchContainer.appendChild(swatch);
            this.decorateSwatch(swatch, hexColorString, colorHandler);
        }

        // Populate Previous Colors
        if (this.recentColors.length > 0) {
            for (const hexColorString of this.recentColors) {
                const swatch = div$2({class: 'igv-ui-color-swatch'});
                this.recentColorsSwatches.appendChild(swatch);
                this.decorateSwatch(swatch, hexColorString, colorHandler);
            }
        }

        // Present MoreColors picker
        this.decorateMoreColorsButton(this.moreColorsContainer, colorHandler);

    }

    decorateSwatch(swatch, hexColorString, colorHandler) {

        swatch.style.backgroundColor = hexColorString;

        swatch.addEventListener('click', event => {
            event.stopPropagation();
            colorHandler(hexColorString);
            this.moreColorsPresentationColor = hexColorString;
        });

        swatch.addEventListener('touchend', event => {
            event.stopPropagation();
            colorHandler(hexColorString);
            this.moreColorsPresentationColor = hexColorString;
        });

    }

    decorateMoreColorsButton(moreColorsContainer, colorHandler) {

        moreColorsContainer.innerText = 'More Colors ...';

        moreColorsContainer.addEventListener('click', event => {
            event.stopPropagation();
            this.createAndPresentMoreColorsPicker(moreColorsContainer, hexColorString => colorHandler(hexColorString));
        });

    }

    updateRecentColorsSwatches(colorHandler){
        this.recentColorsSwatches.innerHTML = '';
        for (const hexColorString of this.recentColors) {
            const swatch = div$2({class: 'igv-ui-color-swatch'});
            this.recentColorsSwatches.appendChild(swatch);
            this.decorateSwatch(swatch, hexColorString, colorHandler);
        }
    }

    createAndPresentMoreColorsPicker(moreColorsContainer, colorHandler) {

        let picker;

        moreColorsContainer.innerHTML = '';
        moreColorsContainer.innerText = 'More Colors ...';

        const colorPickerContainer = document.createElement('div');
        colorPickerContainer.style.position = 'absolute';
        moreColorsContainer.appendChild(colorPickerContainer);

        const { width, height } = moreColorsContainer.getBoundingClientRect();
        colorPickerContainer.style.right = `${0}px`;
        colorPickerContainer.style.top = `${0}px`;
        colorPickerContainer.style.width = `${width}px`;
        colorPickerContainer.style.height = `${height}px`;

        colorPickerContainer.addEventListener('click', (event) => {
            event.stopPropagation();
        });


        picker = new Picker$1();

        const config =
            {
                parent: colorPickerContainer,
                popup: 'top',
                editor: false,
                editorFormat: 'rgb',
                alpha: false,
                color: this.moreColorsPresentationColor,
            };

        picker.setOptions(config);

        picker.setColor(this.moreColorsPresentationColor, true);

        picker.onOpen = () => {
        };
        picker.onChange = color => moreColorsContainer.style.backgroundColor = color.rgba;

        picker.onDone = color => {

            // Remove alpha from hex color string
            const hexColorString = color.hex.substring(0,7);

            this.recentColors.unshift(hexColorString);

            const src = this.recentColors.slice(0);
            this.recentColors = [...new Set(src)].slice(0, GenericColorPicker.maxRecentColors);

            colorHandler(hexColorString);

            this.updateRecentColorsSwatches(colorHandler);

            this.moreColorsPresentationColor = hexColorString;

            picker.destroy();
            colorPickerContainer.remove();
        };

        picker.show();
    }

    present(event){
        const { top} = event.currentTarget.parentElement.getBoundingClientRect();
        this.container.style.top = `${ top }px`;
        this.show();
    }

}

class Alert {
    constructor(parent) {
        this.dialog = new AlertDialog$1(parent);

    }

    present(alert, callback) {
        this.dialog.present(alert, callback);
    }
}

function inferTrackType(format) {

    if (format) {
        switch (format.toLowerCase()) {
            case "bw":
            case "bigwig":
            case "wig":
            case "bedgraph":
            case "tdf":
                return "wig"
            case "vcf":
                return "variant"
            case "seg":
                return "seg"
            case "mut":
            case "maf":
                return "mut"
            case "bam":
            case "cram":
                return "alignment"
            case "hiccups":
            case "bedpe":
            case "bedpe-loop":
            case "biginteract":
                return "interact"
            case "bp":
                return "arc"
            case "gwas":
                return "gwas"
            case "bed":
            case "bigbed":
            case "bb":
            case "biggenepred":
            case "bignarrowpeak":
                return "bedtype"
            case "fasta":
                return "sequence"
            case "pytor":
                return "cnvpytor"
            case "qtl":
                return "qtl"
            default:
                return "annotation"
        }
    }
}

function translateDeprecatedTypes(config) {

    if (config.featureType) {  // Translate deprecated "feature" type
        config.type = config.type || config.featureType;
        config.featureType = undefined;
    }
    if ("junctions" === config.type) {
        config.type = "junction";
    } else if ("bed" === config.type) {
        config.type = "annotation";
        config.format = config.format || "bed";
    } else if ("annotations" === config.type) {
        config.type = "annotation";
    } else if ("alignments" === config.type) {
        config.type = "alignment";
    } else if ("bam" === config.type) {
        config.type = "alignment";
        config.format = "bam";
    } else if ("vcf" === config.type) {
        config.type = "variant";
        config.format = "vcf";
    } else if ("t2d" === config.type) {
        config.type = "gwas";
    } else if ("FusionJuncSpan" === config.type && !config.format) {
        config.format = "fusionjuncspan";
    } else if ("aed" === config.type) {
        config.type = "annotation";
        config.format = config.format || "aed";
    }
}

var TrackUtils = /*#__PURE__*/Object.freeze({
   __proto__: null,
   inferTrackType: inferTrackType,
   translateDeprecatedTypes: translateDeprecatedTypes
});

const defaultColorScaleConfig$1 = {threshold: 2000, r: 0, g: 0, b: 255};

class HicColorScale {

    constructor(scale) {

        scale = scale || defaultColorScaleConfig$1;
        this.threshold = scale.threshold;
        this.r = scale.r;
        this.g = scale.g;
        this.b = scale.b;
        this.cache = [];
        this.nbins = 2000;
        this.binsize = this.threshold / this.nbins;
    }

    setThreshold(threshold) {
        this.threshold = threshold;
        this.cache = [];
        this.binsize = this.threshold / this.nbins;
    }

    getThreshold() {
        return this.threshold;
    }

    setColorComponents(components) {
        this.r = components.r;
        this.g = components.g;
        this.b = components.b;
        this.cache = [];
    }

    getColorComponents() {
        return {
            r: this.r,
            g: this.g,
            b: this.b
        }
    }

    equals(cs) {
        return JSON.stringify(this) === JSON.stringify(cs);
    }

    getColor(value) {
        const low = 0;
        const bin = Math.floor(Math.min(this.threshold, value) / this.binsize);
        if (undefined === this.cache[bin]) {
            const alpha = (IGVMath.clamp(value, low, this.threshold) - low) / (this.threshold - low);
            this.cache[bin] = `rgba(${this.r},${this.g},${this.b}, ${alpha})`;
        }
        return this.cache[bin]
    }

    stringify() {
        return "" + this.threshold + ',' + this.r + ',' + this.g + ',' + this.b;
    }

    static parse(string) {

        var pnstr, ratioCS;

        if (string.startsWith("R:")) {
            pnstr = string.substring(2).split(":");
            ratioCS = new RatioColorScale(Number.parseFloat(pnstr[0]));
            ratioCS.positiveScale = foo(pnstr[1]);
            ratioCS.negativeScale = foo(pnstr[2]);
            return ratioCS;
        } else {
            return foo(string);
        }

        function foo(str) {
            var cs, tokens;

            tokens = str.split(",");

            cs = {
                threshold: tokens[0],
                r: tokens[1],
                g: tokens[2],
                b: tokens[3]
            };
            return new HicColorScale(cs);
        }
    }
}

const NULL_GROUP = 'None';

class SegTrack extends TrackBase {

    static defaults =
        {
            type: 'seg',
            groupBy: NULL_GROUP,
            isLog: undefined,
            displayMode: "EXPANDED",
            height: 300,
            maxHeight: 500,
            squishedRowHeight: 2,
            expandedRowHeight: 13
        }


    constructor(config, browser) {
        super(config, browser);
        this.groups = new Map();
    }


    init(config) {

        super.init(config);

        if (this.type === 'maf') this.type = 'mut';
        this.sortDirections = new Map();
        this.sampleKeys = [];
        this.groups = new Map();
        this.sampleHeight = this.squishedRowHeight;      // Initial value, will get overwritten when rendered

        // Explicitly set samples -- used to select a subset of samples from a dataset
        if (config.samples) {
            for (let s of config.samples) {
                this.sampleKeys.push(s);
            }
            this.explicitSamples = true;  // Samples are explicitly set, do not update from features
        }

        // Color settings
        if (config.color) {
            this.color = config.color;    // Overrides defaults, can be a function
        } else if (config.colorTable) {
            this.colorTable = new ColorTable(config.colorTable);
        } else {
            if ('mut' === this.type) {
                this.colorTable = new ColorTable(MUT_COLORS);
            } else {
                // Color scales for "seg" (copy number) tracks.
                this.posColorScale = new GradientColorScale(config.posColorScale || POS_COLOR_SCALE);
                this.negColorScale = new GradientColorScale(config.negColorScale || NEG_COLOR_SCALE);
            }
        }


        // Create featureSource
        // Disable whole genome downsampling unless explicitly.
        const configCopy = Object.assign({}, this.config);
        configCopy.maxWGCount = configCopy.maxWGCount || Number.MAX_SAFE_INTEGER;

        this.featureSource = FeatureSource(configCopy, this.browser.genome);

        this.initialSort = config.sort;

    }

    async postInit() {
        if (typeof this.featureSource.getHeader === "function") {
            this.header = await this.featureSource.getHeader();
            if (this.disposed) return   // This track was removed during async load
        }
        // Set properties from track line
        if (this.header) {
            this.setTrackProperties(this.header);
        }

        this._initialColor = this.color || this.constructor.defaultColor;
        this._initialAltColor = this.altColor || this.constructor.defaultColor;

        this.didTrackDragEnd = undefined;
        this.browser.on('trackdragend', () => this.didTrackDragEnd = true);
    }

    menuItemList() {

        const menuItems = [];

        if (true === doSortByAttributes(this.browser.sampleInfo, this.sampleKeys)) {
            menuItems.push('<hr/>');
            menuItems.push("Sort by attribute:");
            for (const attribute of this.browser.sampleInfo.attributeNames) {

                const sampleNames = this.sampleKeys;
                if (sampleNames.some(s => {
                    const attrs = this.browser.sampleInfo.getAttributes(s);
                    return attrs && attrs[attribute]
                })) {

                    const element = document.createElement('div');
                    element.innerHTML = `&nbsp;&nbsp;${attribute.split(SampleInfo.emptySpaceReplacement).join(' ')}`;

                    function attributeSort() {
                        this.sortByAttribute(attribute);
                    }

                    menuItems.push({element, click: attributeSort});
                }
            }
        }

        menuItems.push('<hr/>');
        menuItems.push("Group by attribute:");

        for (const attribute of [NULL_GROUP, ...this.browser.sampleInfo.attributeNames]) {

            let initialState = false;
            if (NULL_GROUP === attribute) {
                initialState = (NULL_GROUP === this.groupBy);
            } else {
                initialState = (attribute === this.groupBy);
            }
            const element = createCheckbox(attribute, initialState);

            menuItems.push(
                {
                    element,
                    click: function groupByFunction() {
                        this.groupByAttribute(attribute);
                    }
                });
        }

        const lut =
            {
                "SQUISHED": "Squish",
                "EXPANDED": "Expand",
                "FILL": "Fill"
            };


        menuItems.push('<hr/>');
        menuItems.push("DisplayMode:");
        const displayOptions = 'seg' === this.type ? ["SQUISHED", "EXPANDED", "FILL"] : ["SQUISHED", "EXPANDED"];
        for (let displayMode of displayOptions) {

            menuItems.push(
                {
                    element: createCheckbox(lut[displayMode], displayMode === this.displayMode),
                    click: function displayModeHandler() {
                        this.displayMode = displayMode;
                        this.config.displayMode = displayMode;
                        this.trackView.checkContentHeight();
                        this.trackView.repaintViews();
                        this.createGroupLabels();
                    }
                });
        }

        return menuItems

    }

    hasSamples() {
        return true   // SEG, MUT, and MAF tracks have samples by definition
    }

    getSamples() {
        const groupIndeces = NULL_GROUP !== this.groupBy ?
            this.sampleKeys.map(sample => this.getGroupIndex(sample)) : undefined;
        return {
            names: this.sampleKeys,
            height: this.sampleHeight,
            yOffset: 0,
            groups: this.groups,
            groupIndeces,
            groupMarginHeight: GROUP_MARGIN_HEIGHT
        }
    }

    /**
     * Filter function for sample keys. Applies multiple filters in pipeline fashion.
     * Each filter must pass for a sample to be included.
     *
     * @param sampleKey
     * @returns {boolean}
     */
    filter(sampleKey) {
        const filterObjects = this._trackFilterObjects || [];

        if (filterObjects.length === 0) {
            return true
        }

        // Apply each filter in sequence - all must pass
        for (const filterObject of filterObjects) {
            const scores = filterObject.scores || {};
            const score = scores[sampleKey];

            if (this.type === 'seg') {
                if (filterObject.op === '>') {
                    if (!(score > filterObject.value)) return false
                } else if (filterObject.op === '<') {
                    if (!(score < filterObject.value)) return false
                }
            } else if (this.type === 'mut' || this.type === 'maf') {
                const hasMutations = 'HAS' === filterObject.op ? score : !score;
                if (!hasMutations) return false
            }
        }

        return true
    }


    /**
     * Return the current state of the track. Used to create sessions and bookmarks.
     * @returns {Object} - Track state including filters
     */
    getState() {
        const state = super.getState();

        // Save current filters as part of track state
        if (this._trackFilterObjects && this._trackFilterObjects.length > 0) {
            // Convert filter objects to filter specs (remove computed scores)
            const filterSpecs = this._trackFilterObjects.map(filterObj => {
                const {scores, ...filterSpec} = filterObj;
                return filterSpec
            });
            state.filters = filterSpecs;
        }

        return state
    }

    async getFeatures(chr, start, end) {
        const features = await this.featureSource.getFeatures({chr, start, end});
        // New segments could conceivably add new samples
        this.updateSampleKeys(features);

        if (this.initialSort) {
            const sort = this.initialSort;

            if (sort.option === undefined || sort.option.toUpperCase() === "VALUE") {
                const sortFeatures = (sort.chr === chr && sort.start >= start && sort.end <= end) ? features : undefined;
                this.sortByValue(sort, sortFeatures);
            } else if ("ATTRIBUTE" === sort.option.toUpperCase() && sort.attribute) {
                const sortDirection = "DESC" === sort.direction ? 1 : -1;
                this.sortByAttribute(sort.attribute, sortDirection);
            }
            this.initialSort = undefined;  // Sample order is sorted,
        }
        return features
    }

    draw({context, pixelTop, pixelWidth, pixelHeight, features, bpPerPixel, bpStart}) {

        IGVGraphics.fillRect(context, 0, pixelTop, pixelWidth, pixelHeight, {'fillStyle': "rgb(255, 255, 255)"});


        if (features && features.length > 0) {

            this.checkForLog(features);

            if (this.type === "shoebox" && !this.sbColorScale) {
                const threshold = this.featureSource.hicFile.percentile95 || 2000;
                this.sbColorScale = new HicColorScale({threshold, r: 0, g: 0, b: 255});
            }

            // Create a map for fast id -> row lookup
            const sampleRowIndeces = {};
            this.sampleKeys.forEach(function (sample, index) {
                sampleRowIndeces[sample] = index;
            });

            let border;
            switch (this.displayMode) {
                case "FILL":
                    this.sampleHeight = (pixelHeight - (this.groups.size + 1) * GROUP_MARGIN_HEIGHT) / this.sampleKeys.length;
                    border = 0;
                    break

                case "SQUISHED":
                    this.sampleHeight = this.squishedRowHeight;
                    border = 0;
                    break
                default:   // EXPANDED
                    this.sampleHeight = this.expandedRowHeight;
                    border = 1;
            }
            const rowHeight = this.sampleHeight;

            for (let segment of features) {
                segment.pixelRect = undefined;   // !important, reset this in case segment is not drawn
            }

            const pixelBottom = pixelTop + pixelHeight;
            const bpEnd = bpStart + pixelWidth * bpPerPixel + 1;
            const xScale = bpPerPixel;

            this.sampleYStart = undefined;
            for (let f of features) {

                if (f.end < bpStart || f.start > bpEnd) continue

                const sampleKey = f.sampleKey || f.sample;
                f.row = sampleRowIndeces[sampleKey];
                let y =   f.row * rowHeight;
                if( this.groups.size > 1) {
                   y += (this.getGroupIndex(sampleKey) + 1) * GROUP_MARGIN_HEIGHT;
                }

                if (undefined === this.sampleYStart) {
                    this.sampleYStart = y;
                }

                const bottom = y + rowHeight;

                if (bottom < pixelTop || y > pixelBottom) {
                    continue
                }

                const segmentStart = Math.max(f.start, bpStart);
                // const segmentStart = segment.start;
                let x = Math.round((segmentStart - bpStart) / xScale);

                const segmentEnd = Math.min(f.end, bpEnd);
                // const segmentEnd = segment.end;
                const x1 = Math.round((segmentEnd - bpStart) / xScale);
                let w = Math.max(1, x1 - x);

                let color;
                if (this.color) {
                    if (typeof this.color === "function") {
                        color = this.color(f);
                    } else {
                        color = this.color;
                    }
                } else if (this.colorTable) {
                    color = this.colorTable.getColor(f.value.toLowerCase());
                }

                let h;
                if ("mut" === this.type) {
                    h = rowHeight - 2 * border;
                    if (w < 3) {
                        w = 3;
                        x -= 1;
                    }
                } else {
                    // Assume seg track
                    let value = f.value;
                    if (!this.isLog) {
                        value = IGVMath.log2(value / 2);
                    }
                    if (value < -0.1) {
                        color = this.negColorScale.getColor(value);
                    } else if (value > 0.1) {
                        color = this.posColorScale.getColor(value);
                    } else {
                        color = "white";
                    }

                    let sh = rowHeight;
                    if (rowHeight < 0.25) {
                        const f = 0.1 + 2 * Math.abs(value);
                        sh = Math.min(1, f * rowHeight);
                    }
                    h = sh - 2 * border;
                }


                f.pixelRect = {x, y, w, h};

                // Use for diagnostic rendering
                // context.fillStyle = randomRGB(180, 240)
                context.fillStyle = color;

                context.fillRect(x, y, w, h);
            }

            if (NULL_GROUP !== this.groupBy) {
                drawGroupDividers(context,
                    pixelTop,
                    pixelWidth,
                    pixelHeight,
                    0,
                    this.sampleHeight,
                    this.groups);
            }
        }
    }


    getGroupIndex(sampleKey) {
        const attributeValue = this.browser.sampleInfo.getAttributeValue(sampleKey, this.groupBy) || "";
        if (this.groups.has(attributeValue)) {
            return this.groups.get(attributeValue).index
        } else {
            return this.groups.size
        }
    }

    checkForLog(features) {
        if (this.isLog === undefined) {
            this.isLog = false;
            for (let feature of features) {
                if (feature.value < 0) {
                    this.isLog = true;
                    return
                }
            }
        }
    }

    /**
     * Optional method to compute pixel height to accomodate the list of features.  The implementation below
     * has side effects (modifiying the samples hash).
     *
     * Note displayMode "FILL" is handled by the viewport
     *
     * @param features
     * @returns {number}
     */
    computePixelHeight(features) {
        if (!features) return 0
        const sampleHeight = ("SQUISHED" === this.displayMode) ? this.squishedRowHeight : this.expandedRowHeight;
        return this.sampleKeys.length * sampleHeight + (this.groups.size > 1 ? (this.groups.size + 1) * GROUP_MARGIN_HEIGHT : 0)
    }

    /**
     * Sort samples by the average value over the genomic range in the direction indicated (1 = ascending, -1 descending)
     */
    async sortByValue(sort, featureList) {

        const chr = sort.chr;
        const start = sort.position !== undefined ? sort.position - 1 : sort.start;
        const end = sort.end === undefined ? start + 1 : sort.end;
        const scores = await this.computeRegionScores({chr, start, end}, featureList);
        const d2 = (sort.direction === "ASC" ? 1 : -1);

        this.sampleKeys.sort(function (a, b) {
            let s1 = scores[a];
            let s2 = scores[b];
            if (!s1) s1 = d2 * Number.MAX_VALUE;
            if (!s2) s2 = d2 * Number.MAX_VALUE;
            if (s1 === s2) return 0
            else if (s1 > s2) return d2
            else return d2 * -1
        });

        if (NULL_GROUP !== this.groupBy) {
            // If grouping by attribute, we need to re-group the samples
            this.sampleKeys = this.browser.sampleInfo.sortSampleKeysByAttribute(this.sampleKeys, this.groupBy, 1);
        }

        this.config.sort = sort;
        this.trackView.repaintViews();
    }


    async computeRegionScores(filterObject, featureList) {

        const chr = filterObject.chr;
        let start, end;
        if (filterObject.position) {
            start = filterObject.position - 1;
            end = start + 1;
        } else {
            start = filterObject.start;
            end = filterObject.end;
        }

        if (!featureList) {
            featureList = await this.featureSource.getFeatures({chr, start, end});
        }
        if (!featureList) return

        this.updateSampleKeys(featureList);

        const scores = {};
        const bpLength = end - start + 1;

        for (let segment of featureList) {
            if (segment.end < start) continue
            if (segment.start > end) break
            const sampleKey = segment.sampleKey || segment.sample;

            if ("mut" === this.type) {
                // Just count features overlapping region per sample
                scores[sampleKey] = (scores[sampleKey] || 0) + 1;
            } else {

                const min = Math.max(start, segment.start);
                const max = Math.min(end, segment.end);
                const f = (max - min) / bpLength;
                scores[sampleKey] = (scores[sampleKey] || 0) + f * segment.value;
            }
        }

        return scores
    }

    sortByAttribute(attribute, sortDirection) {

        sortDirection = sortDirection || this.sortDirections.get(attribute) || 1;

        this.sampleKeys = this.browser.sampleInfo.sortSampleKeysByAttribute(this.sampleKeys, attribute, sortDirection);

        if (NULL_GROUP !== this.groupBy) {
            // If grouping by attribute, we need to re-group the samples
            this.sampleKeys = this.browser.sampleInfo.sortSampleKeysByAttribute(this.sampleKeys, this.groupBy, 1);
        }

        this.config.sort = {
            option: "ATTRIBUTE",
            attribute: attribute,
            direction: sortDirection === 1 ? "ASC" : "DESC"
        };
        this.sortDirections.set(attribute, sortDirection * -1);
        this.trackView.repaintViews();
    }

    groupByAttribute(attribute) {

        this.groupBy = attribute;

        // Group samples by the specified attribute
        this.groups.clear();
        if (NULL_GROUP !== attribute) {
            this.sampleKeys = this.browser.sampleInfo.sortSampleKeysByAttribute(this.sampleKeys, attribute, 1);
            const sampleKeys = this.sampleKeys;
            for (let sampleKey of sampleKeys) {
                const value = this.browser.sampleInfo.getAttributeValue(sampleKey, attribute) || "";
                if (value) {
                    if (!this.groups.has(value)) {
                        this.groups.set(value, {index: this.groups.size, count: 1});
                    } else {
                        this.groups.get(value).count += 1;
                    }
                }
            }
        }

        this.trackView.checkContentHeight();
        this.trackView.repaintViews();
        this.createGroupLabels();
    }

    createGroupLabels() {

        const viewport = this.trackView.getLastViewport();
        viewport.overlayElement.innerHTML = '';   // Clear previous labels

        if (this.groups.size === 0) return

        let sampleHeight;
        switch (this.displayMode) {
            case "EXPANDED":
                sampleHeight = this.expandedRowHeight;
                break
            case "SQUISHED":
                sampleHeight = this.squishedRowHeight;
                break
            default:   // FILL mode -- hopefully sample height has been set in the draw method
                const pixelHeight = viewport.viewportElement.getBoundingClientRect().height;
                sampleHeight = (pixelHeight - (this.groups.size - 1) * GROUP_MARGIN_HEIGHT) / this.sampleKeys.length;

        }

        let top = GROUP_MARGIN_HEIGHT;
        for (const bucketKey of this.groups.keys()) {
            const labelDiv = document.createElement('div');
            labelDiv.className = 'igv-attribute-group-label';
            viewport.overlayElement.appendChild(labelDiv);
            labelDiv.innerText = bucketKey;
            labelDiv.style.top = `${top}px`;
            top += this.groups.get(bucketKey).count * sampleHeight + GROUP_MARGIN_HEIGHT;
        }
    }

    clickedFeatures(clickState) {

        const allFeatures = super.clickedFeatures(clickState);
        const y = clickState.y;
        return allFeatures.filter(function (feature) {
            const rect = feature.pixelRect;
            return rect && y >= rect.y && y <= (rect.y + rect.h)
        })

    }

    hoverText(clickState) {
        const features = this.clickedFeatures(clickState);
        if (features && features.length > 0) {
            return `${features[0].sample}: ${features[0].value}`
        }
    }

    popupData(clickState, featureList) {

        if (featureList === undefined) featureList = this.clickedFeatures(clickState);

        const items = [];

        for (let feature of featureList) {

            // Double line divider between features
            if (items.length > 0) {
                items.push('<hr/>');
                items.push('<hr/>');
            }

            // hack for whole genome features, which save the original feature as "_f"
            const f = feature._f || feature;

            const data = (typeof f.popupData === 'function') ?
                f.popupData(this.type, this.browser.genome.id) :
                this.extractPopupData(f);
            Array.prototype.push.apply(items, data);

        }

        return items
    }

    contextMenuItemList(clickState) {

        const {genomicLocation, referenceFrame, viewport, event} = clickState;

        const sortHandler = (sort) => {
            const features = viewport.cachedFeatures;
            this.sortByValue(sort, features);
        };

        // We can't know genomic location intended with precision, define a buffer 5 "pixels" wide in genomic coordinates
        const bpWidth = referenceFrame.toBP(2.5);

        const menuItems = ["DESC", "ASC"].map(direction => {
            const dirLabel = direction === "DESC" ? "descending" : "ascending";
            const sortLabel = this.type === 'seg' || this.type === 'shoebox' ?
                `Sort by value (${dirLabel})` :
                `Sort by count (${dirLabel})`;
            return {
                label: sortLabel,
                click: () => {
                    const sort = {
                        option: "VALUE",   // Either VALUE or ATTRIBUTE
                        direction,
                        chr: referenceFrame.chr,
                        start: Math.floor(genomicLocation - bpWidth),
                        end: Math.floor(genomicLocation + bpWidth)
                    };
                    sortHandler(sort);
                }
            }
        });

        menuItems.push('<hr/>');

        return menuItems
    }

    get supportsWholeGenome() {
        return (this.config.indexed === false || !this.config.indexURL) && this.config.supportsWholeGenome !== false
    }

    updateSampleKeys(featureList) {
        if (this.explicitSamples) return

        let newSamplesFound = false;
        const sampleKeySet = new Set(this.sampleKeys);
        for (let feature of featureList) {
            const sampleKey = feature.sampleKey || feature.sample;
            if (!sampleKeySet.has(sampleKey)) {
                const keys = this.sampleKeys;
                keys.push(sampleKey);
                this.sampleKeys = keys;
                sampleKeySet.add(sampleKey);
                newSamplesFound = true;
            }
        }
        if (newSamplesFound && NULL_GROUP !== this.groupBy) {
            this.groupByAttribute(this.groupBy);

        }
    }

}

// Default copy number scales
const POS_COLOR_SCALE = {low: 0.1, high: 1.5, lowColor: 'rgb(255,255,255)', highColor: 'rgb(255,0,0)'};
const NEG_COLOR_SCALE = {low: -1.5, high: -0.1, lowColor: 'rgb(0,0,255)', highColor: 'rgb(255,255,255)'};
//const POS_COLOR_SCALE = {low: 0.1, lowR: 255, lowG: 255, lowB: 255, high: 1.5, highR: 255, highG: 0, highB: 0}
//const NEG_COLOR_SCALE = {low: -1.5, lowR: 0, lowG: 0, lowB: 255, high: -0.1, highR: 255, highG: 255, highB: 255}

// Mut and MAF file default color table
const MUT_COLORS = {

    "indel": "rgb(0,200,0)",
    "targeted region": "rgb(236,155,43)",
    "truncating": "rgb(	150,0,0)",
    "non-coding transcript": "rgb(0,0,150)",

    // Colors from https://www.nature.com/articles/nature11404
    "synonymous": "rgb(109,165,95)",
    "silent": "rgb(109,135,80)",
    "missense_mutation": "rgb(72,130,187)",
    "missense": "rgb(72,130,187)",
    "splice site": "rgb(143,83,155)",
    "splice_region": "rgb(143,83,155)",
    "nonsense": "rgb(216, 57,81)",
    "nonsense_mutation": "rgb(216, 57,81)",
    "frame_shift_del": "rgb(226,135,65)",
    "frame_shift_ins": "rgb(226,135,65)",
    "in_frame_del": "rgb(247,235,94)",
    "in_frame_ins": "rgb(247,235,94)",
    "*other*": "rgb(159,91,50)"
    //
    // 3'Flank
    // 3'UTR
    // 5'Flank
    // 5'UTR
    // Frame_Shift_Del
    // Frame_Shift_Ins
    // IGR
    // In_Frame_Del
    // In_Frame_Ins
    // Intron
    // Missense_Mutation
    // Nonsense_Mutation
    // Nonstop_Mutation
    // RNA
    // Silent
    // Splice_Region
    // Splice_Site
    // Translation_Start_Site
    // Variant_Classification

};

class PairedAlignment {

    constructor(firstAlignment) {

        this.paired = true;
        this.firstAlignment = firstAlignment;
        this.chr = firstAlignment.chr;
        this.readName = firstAlignment.readName;

        if (firstAlignment.start < firstAlignment.mate.position) {
            this.start = firstAlignment.start;
            this.scStart = firstAlignment.scStart;
            this.connectingStart = firstAlignment.start + firstAlignment.lengthOnRef;
            this.connectingEnd = firstAlignment.mate.position;
        } else {
            this.start = firstAlignment.mate.position;
            this.scStart = this.start;
            this.connectingStart = firstAlignment.mate.position;
            this.connectingEnd = firstAlignment.start;
        }

        this.end = Math.max(firstAlignment.mate.position, firstAlignment.start + firstAlignment.lengthOnRef);  // Approximate
        this.lengthOnRef = this.end - this.start;

        let scEnd = Math.max(this.end, firstAlignment.scStart + firstAlignment.scLengthOnRef);
        this.scLengthOnRef = scEnd - this.scStart;

    }

    setSecondAlignment(secondAlignment) {

        // TODO -- check the chrs are equal,  error otherwise
        this.secondAlignment = secondAlignment;
        const firstAlignment = this.firstAlignment;

        if (secondAlignment.start > firstAlignment.start) {
            this.connectingEnd = secondAlignment.start;
        } else {
            this.connectingStart = secondAlignment.start + secondAlignment.lengthOnRef;
        }

        this.start = Math.min(firstAlignment.start, secondAlignment.start);
        this.end = Math.max(firstAlignment.start + firstAlignment.lengthOnRef, secondAlignment.start + secondAlignment.lengthOnRef);
        this.lengthOnRef = this.end - this.start;

        this.scStart = Math.min(firstAlignment.scStart, secondAlignment.scStart);
        const scEnd = Math.max(firstAlignment.scStart + firstAlignment.scLengthOnRef, secondAlignment.scStart + secondAlignment.scLengthOnRef);
        this.scLengthOnRef = scEnd - this.scStart;

    }

    containsLocation(genomicLocation, showSoftClips) {
        const s = showSoftClips ? this.scStart : this.start;
        const l = showSoftClips ? this.scLengthOnRef : this.lengthOnRef;
        return (genomicLocation >= s && genomicLocation <= (s + l))
    }

    alignmentContaining(genomicLocation, showSoftClips) {
        if (this.firstAlignment.containsLocation(genomicLocation, showSoftClips)) {
            return this.firstAlignment
        } else if (this.secondAlignment && this.secondAlignment.containsLocation(genomicLocation, showSoftClips)) {
            return this.secondAlignment
        } else {
            return undefined
        }
    }

    popupData(genomicLocation, hiddenTags, showTags) {

        let nameValues = this.firstAlignment.popupData(genomicLocation, hiddenTags, showTags);

        if (this.secondAlignment) {
            nameValues.push("-------------------------------");
            nameValues = nameValues.concat(this.secondAlignment.popupData(genomicLocation, hiddenTags, showTags));
        }
        return nameValues
    }

    isPaired() {
        return true // By definition
    }

    isMateMapped() {
        return true // By definition
    }

    isProperPair() {
        return this.firstAlignment.isProperPair()
    }

    get fragmentLength() {
        return Math.abs(this.firstAlignment.fragmentLength)
    }

    get firstOfPairStrand() {
        return this.firstAlignment.firstOfPairStrand
    }

    get pairOrientation() {
        return this.firstAlignment.pairOrientation
    }

    hasTag(str) {
        return this.firstAlignment.hasTag(str) || (this.secondAlignment && this.secondAlignment.hasTag(str))
    }

    getGroupValue({option, tag}) {
        switch (option) {
            case "strand":
                return this.isNegativeStrand() ? '-' : '+'
            case "FIRST_IN_PAIR_STRAND":
                if (this.isPaired()) {
                    if (this.isFirstOfPair()) {
                        return this.isNegativeStrand() ? '-' : '+'
                    } else if (this.isSecondOfPair()) {
                        return this.isNegativeStrand() ? '+' : '-'
                    } else {
                        return
                    }
                } else {
                    return
                }
            case "START":
                return this.start
            case "INSERT_SIZE":
                return this.fragmentLength
            case "MATE_CHR":
                return this.mate ? this.mate.chr : undefined
            case "MQ":
                return this.mq
            case "ALIGNED_READ_LENGTH":
                return this.lengthOnRef
            case "TAG": {
                return this.tags()[tag]
            }
            case 'PHASE':
                return this.tags()["HP"]
            case 'READ_ORDER':
                if (this.isPaired() && this.isFirstOfPair()) {
                    return "FIRST"
                } else if (this.isPaired() && this.isSecondOfPair()) {
                    return "SECOND"
                } else {
                    return ""
                }


            default:
                return
        }
    }
}

class BaseModificationKey {

    static instances = new Map()

    static getKey(base, strand, modification) {

        const s = "" + base + strand + modification;
        if (!BaseModificationKey.instances.has(s)) {
            BaseModificationKey.instances.set(s, new BaseModificationKey(base, strand, modification));
        }
        return BaseModificationKey.instances.get(s)
    }

    // char base;
    // char strand;
    // String modification;
    constructor(base, strand, modification) {
        this.base = base;
        this.strand = strand;
        this.modification = modification;
        this.canonicalBase = this.strand === '+' ? this.base : complementBase(this.base);
    }

    getCanonicalBase() {
        return
    }


    toString() {
        return "" + this.base + this.strand + this.modification
    }

    static compare(a, b) {
        const mod1 = a.modification;
        const mod2 = b.modification;

        if (mod1 === mod2) {
            return a.strand.charAt(0) - b.strand.charAt(0)
        }

        if (modificationRankOrder.has(mod1) & modificationRankOrder.has(mod2)) {
            return modificationRankOrder.get(mod1) - modificationRankOrder.get(mod2)
        } else if (modificationRankOrder.has(mod1)) {
            return 1
        } else if (modificationRankOrder.has(mod2)) {
            return -1
        } else {
            return mod1 > mod2 ? 1 : -1
        }
    }
}

const modificationRankOrder = new Map(
    ["NONE_C", "NONE_T", "NONE_G", "NONE_A", "m", "h", "f", "c", "C", "g", "e", "b", "T", "U", "a", "A", "o", "G", "n", "N"].map((elem, idx) => [elem, idx])
);

class BaseModificationSet {

    /**
     *
     * @param base
     * @param strand
     * @param modification
     * @param likelihoods  - map of position to likelihood
     */
    constructor(base, strand, modification, likelihoods) {
        this.base = base;
        this.modification = modification;
        this.strand = strand;
        this.likelihoods = likelihoods;
        this.canonicalBase = this.strand == '+' ? this.base : complementBase(this.base);
        this.key = BaseModificationKey.getKey(base, strand, modification);
    }

    containsPosition(pos) {
        return this.likelihoods.has(pos)
    }

    is5mC() {
        return this.modification.equals("m") && ((this.base === 'C' && this.strand === '+') || (this.base === 'G' && this.strand === '-'))
    }

    fullName() {
        return codeValues$1.get(this.modification) || this.modification
    }
}

const codeValues$1 = new Map();
codeValues$1.set("m", "5mC");
codeValues$1.set("h", "5hmC");
codeValues$1.set("f", "5fC");
codeValues$1.set("c", "5caC");
codeValues$1.set("g", "5hmU");
codeValues$1.set("e", "5fU");
codeValues$1.set("b", "5caU");
codeValues$1.set("a", "6mA");
codeValues$1.set("o", "8xoG");
codeValues$1.set("n", "Xao");
codeValues$1.set("C", "Unknown C");
codeValues$1.set("T", "Unknown T");
codeValues$1.set("A", "Unknown A");
codeValues$1.set("G", "Unknown G");
codeValues$1.set("N", "Unknown");

const codeValues = new Map([
    ["m", "5mC"],
    ["h", "5hmC"],
    ["f", "5fC"],
    ["c", "5caC"],
    ["g", "5hmU"],
    ["e", "5fU"],
    ["b", "5caU"],
    ["a", "6mA"],
    ["o", "8xoG"],
    ["n", "Xao"],
    ["C", "Unknown C"],
    ["T", "Unknown T"],
    ["A", "Unknown A"],
    ["G", "Unknown G"],
    ["N", "Unknown"]
]);


function modificationName(modification) {
    return  (codeValues.has(modification)) ? codeValues.get(modification) : "Uknown"
}


/**
 * Parse the mm tag creating a base modification set for each modification listed.
 *
 * @param mm       MM tag value, string, examples
 *                 C+m?,5,12,0; :   A single modification, 1 set is returned
 *                 C+mh,5,12,0; :   Two modifications, 2 sets are returned
 *                 C+m,5,12,0;C+h,5,12,0;   Two modifications, 2 sets are returned
 * @param ml  byte[]
 * @param sequence
 * @return List<BaseModificationSet>
 */
function getBaseModificationSets(mm, ml, sequence, isNegativeStrand) {
    if (isNegativeStrand) {
        sequence = reverseComplementSequence(sequence);
    }

    const modificationSets = [];


    const mmTokens = mm.split(";");
    let mlIdx = 0;      // likelihood array index

    for (let mmi of mmTokens) {

        const tokens = mmi.split(","); //Globals.commaPattern.split(mm);
        const base = tokens[0].charAt(0);
        const strand = tokens[0].charAt(1);
        const skippedBasesCalled = tokens[0].endsWith(".");    // False by default.

        if (tokens.length == 1) ; else {

            const modificationString = tokens[0].endsWith(".") || tokens[0].endsWith("?") ?
                tokens[0].substring(2, tokens[0].length - 1) :
                tokens[0].substring(2);

            // Parse modifications, this is rather complex, commensurate with the spec.  Unless a chebi code, modifications
            // are restricted to single characters, a multi-character string that is not a chebi code indicates
            // multiple modifications
            let modifications = [];
            if (modificationString.length > 1) {
                if (isChEBI(modificationString)) {
                    modifications.push(modificationString);
                } else {
                    for (let i = 0; i < modificationString.length; i++) {
                        modifications.push(modificationString.substring(i, i + 1));
                    }
                }
            } else {
                modifications.push(modificationString);
            }


            // Create a positions -> likelihood map for each modification
            //Map<String, Map<Integer, Byte>> likelihoodMap = new HashMap<>();
            const likelihoodMap = new Map();
            for (let m of modifications) {
                likelihoodMap.set(m, new Map());
            }

            let idx = 1;  // position array index,  positions start at index 1
            let skip = Number.parseInt(tokens[idx++]);
            let p = 0;
            let matchCount = 0;

            while (p < sequence.length) {

                if (base === 'N' || sequence[p] === base) {
                    const position = isNegativeStrand ? sequence.length - 1 - p : p;
                    if (matchCount === skip) {
                        for (let modification of modifications) {
                            const likelihood = !ml ? 255 : ml[mlIdx++];
                            likelihoodMap.get(modification).set(position, likelihood);
                        }
                        if (idx < tokens.length) {
                            skip = Number.parseInt(tokens[idx++]);
                            matchCount = 0;
                        } else {
                            if (skippedBasesCalled) {
                                // MM tag is exhausted, but continue scanning for skipped bases
                                skip = -1;
                            } else {
                                // If skipped bases are not called unmodified we are done.
                                break;
                            }
                        }
                    } else {
                        if (skippedBasesCalled) {
                            // Skipped bases are assumed be called "modification present with 0% probability",
                            // i.e modification has been called to be not present (as opposed to unknown)
                            for (let modification of modifications) {
                                likelihoodMap.get(modification).set(position, 0);
                            }
                        }
                        matchCount++;
                    }
                }
                p++;
            }

            for (let m of modifications) {
                modificationSets.push(new BaseModificationSet(base, strand, m, likelihoodMap.get(m)));
            }
        }
    }

    return modificationSets
}

/**
 * If a string can be converted to a positive integer assume its a ChEBI ID
 *
 * @param str
 * @return
 */
function isChEBI(str) {
    if (!str) {
        return false
    }
    const length = str.length;
    if (length == 0) {
        return false
    }
    for (let i = 0; i < length; i++) {
        const c = str.charAt(i);
        if (c < '0' || c > '9') {
            return false
        }
    }
    return true
}

/**
 * Mimics Java's Byte.toUnsignedInt(). This is not a general function, only works for "byte" range -128 -> 128  Useful for Java port*
 * @param b
 */
function byteToUnsignedInt(b) {
    return b < 0 ? b + 256 : b
}

class BaseModificationCounts {

    /**
     * Set of all modification seen.
     */
    allModifications = new Set()  //LinkedHashSet<Key>

    simplexModifications = new Set()

    /**
     * Maxixum likelihood (i.e. maximum of all modifications present) for each position and base moodification key*
     */
    maxLikelihoods = new Map()

    /**
     * Maximum likelihood including no-modification (1 - sum(likelihoods)) for each position and base moodification key*
     */
    nomodLikelihoods = new Map()

    lastThreshold


    /**
     * Increment modification counts for each position spanned by the supplied alignments.  Currently both thresholded
     * and total counts are tallied to support different coloring schemes.
     *
     * @param alignment
     */
    incrementCounts(alignment) {

        // Only works with block formats
        if (!alignment.blocks) return

        const baseModificationSets = alignment.getBaseModificationSets();    //List<BaseModificationSet>
        if (baseModificationSets) {

            for (let block of alignment.blocks) {

                //        /*
                //          start: scPos,
                //                 seqOffset: seqOffset,
                //                 len: c.len,
                //                 type: 'S'
                //          */

                if(block.type === 'S') continue // Soft clip

                for (let blockIdx = 0; blockIdx < block.len; blockIdx++) {

                    let readIdx = blockIdx + block.seqOffset;
                    let canonicalBase = 0;
                    let maxLH = -1;
                    let maxKey;
                    let noModLH = 255;

                    for (let bmset of baseModificationSets) {

                        //String modification = bmset.getModification();
                        const key = BaseModificationKey.getKey(bmset.base, bmset.strand, bmset.modification);
                        this.allModifications.add(key);
                        const likelihoods = bmset.likelihoods;   //List<BaseModificationSet>

                        if (bmset.containsPosition(readIdx)) {

                            const lh = byteToUnsignedInt(likelihoods.get(readIdx));
                            noModLH -= lh;
                            if (lh > maxLH) {
                                canonicalBase = bmset.canonicalBase;
                                maxLH = lh;
                                maxKey = key;
                            }
                        }
                    }
                    // Count the modification with highest likelihood, which might be the likelihood of no-modification
                    if (canonicalBase != 0) {
                        const position = block.start + blockIdx;

                        const noModKey = BaseModificationKey.getKey(canonicalBase, '+', "NONE_" + canonicalBase);
                        this.allModifications.add(noModKey);

                        const pushLikelihood = (position, byteLikelihood, modKey, likelihoods) => {
                            let t = likelihoods.get(modKey);
                            if (!t) {
                                t = new Map();
                                likelihoods.set(modKey, t);
                            }
                            let byteArrayList = t.get(position);
                            if (!byteArrayList) {
                                byteArrayList = [];
                                t.set(position, byteArrayList);
                            }
                            byteArrayList.push(byteLikelihood);
                        };

                        // mono color counts -- does not include no-modification
                        pushLikelihood(position, maxLH, maxKey, this.maxLikelihoods);

                        // 2-color counts, which include no-modification
                        if (noModLH > maxLH) {
                            pushLikelihood(position, noModLH, noModKey, this.nomodLikelihoods);
                        } else {
                            pushLikelihood(position, maxLH, maxKey, this.nomodLikelihoods);
                        }

                    }

                }
            }
        }
    }


    getCount(position, key, threshold, includeNoMods) {

        this.lastThreshold = threshold;
        const scaledThreshold = threshold * 255;

        const t = includeNoMods ? this.nomodLikelihoods.get(key) : this.maxLikelihoods.get(key);
        if (!t) {
            return 0
        }

        const byteArrayList = t.get(position);
        if (!byteArrayList) {
            return 0
        } else {
            let count = 0;
            for (let byteLikelihood of byteArrayList) {
                const lh = byteToUnsignedInt(byteLikelihood);
                if (lh >= scaledThreshold) {
                    count++;
                }
            }
            return count
        }
    }

    getLikelihoodSum(position, key, threshold, includeNoMods) {
        this.lastThreshold = threshold;
        const scaledThreshold = threshold * 255;
        const t = includeNoMods ? this.nomodLikelihoods.get(key) : this.maxLikelihoods.get(key);
        const byteArrayList = t.get(position);
        if (!byteArrayList) {
            return 0
        } else {
            let count = 0;
            for (let byteLikelihood of byteArrayList) {
                const lh = byteToUnsignedInt(byteLikelihood);
                if (lh >= scaledThreshold) {
                    count += lh;
                }
            }
            return count
        }
    }


    popupData(position, colorOption) {
        const nameValues = [];
        nameValues.push("<b>Modifications with likelihood > " + (this.lastThreshold * 100) + "%</b>");

        for (let key of this.maxLikelihoods.keys()) {
            const t = this.maxLikelihoods.get(key);
            if (t.has(position)) {
                let includeNoMods = colorOption === "basemod2";
                const count = this.getCount(position, key, this.lastThreshold, includeNoMods);
                if (count > 0) {
                    const likelihoodSum = this.getLikelihoodSum(position, key, this.lastThreshold, includeNoMods);
                    const averageLikelihood = (likelihoodSum / count) * .3921568;      // => 100/255
                    const modName = modificationName(key.modification);
                    nameValues.push(modName + " (" + key.base + key.strand + "): " + count + "  @ average likelihood " + Math.round(averageLikelihood) + "%");
                }
            }
        }
        return nameValues
    }

    // Search modification keys for "simplex" data,  e.g. C+m without corresponding G-m, indicating only 1 strand of molecule was read or recorded
    computeSimplex() {
        const minusStranMods = new Set(Array.from(this.allModifications)
            .filter(key => key.strand === "-")
            .map(key => key.modification));
        for (let key of this.allModifications) {
            if (key.strand === "+" && !minusStranMods.has(key.modification)) {
                this.simplexModifications.add(key.modification);
                this.simplexModifications.add("NONE_" + key.getCanonicalBase());  // Mix of simplex & duplex keys for same base not supported.
            }
        }
    }
}

class BamAlignmentRow {

    constructor() {

        this.alignments = [];
        this.score = undefined;
    }

    findAlignment(genomicLocation, sortAsPairs = false) {

        const alignmentContains = (a, genomicLocation) => {
            return genomicLocation >= a.start && genomicLocation < a.start + (sortAsPairs ? a.fragmentLength : a.lengthOnRef)
        };

        // find single alignment that overlaps sort location
        let centerAlignment;
        for (let i = 0; i < this.alignments.length; i++) {
            const a = this.alignments[i];
            if (genomicLocation >= a.start && genomicLocation < a.start + (sortAsPairs ? a.fragmentLength : a.lengthOnRef)) {
                if (a.paired) {
                    if (a.firstAlignment && alignmentContains(a.firstAlignment, genomicLocation)) {
                        centerAlignment = a.firstAlignment;
                    } else if (a.secondAlignment && alignmentContains(a.secondAlignment, genomicLocation)) {
                        centerAlignment = a.secondAlignment;
                    }
                } else {
                    centerAlignment = a;
                }
                break
            }
        }

        return centerAlignment

    }

    getSortValue({ position, option, tag, sortAsPairs }, alignmentContainer) {

        if (!option) option = "BASE";

        const alignment = this.findAlignment(position, sortAsPairs);
        if (undefined === alignment) {  // This condition should never occur
            return Number.MAX_VALUE
        }

        switch (option) {
            case "NUCLEOTIDE":
            case "BASE": {
                return calculateBaseScore(alignment, alignmentContainer, position)
            }
            case "strand":
                return alignment.strand ? 1 : -1
            case "START":
                return alignment.start
            case "TAG": {
                return alignment.tags()[tag]
            }
            case "READ_NAME":
                return alignment.readName
            case "INSERT_SIZE":
                return -Math.abs(alignment.fragmentLength)
            case "GAP_SIZE":
                return -alignment.gapSizeAt(position)
            case "MATE_CHR":
                return alignment.mate ? alignment.mate.chr : Number.MAX_VALUE
            case "MQ":
                return alignment.mq === undefined ? Number.MAX_VALUE : -alignment.mq
            case "ALIGNED_READ_LENGTH":
                return -alignment.lengthOnRef
            default:
                return Number.MAX_VALUE
        }


        function calculateBaseScore(alignment, alignmentContainer, genomicLocation) {

            let reference;
            const idx = Math.floor(genomicLocation) - alignmentContainer.start;
            if (idx < alignmentContainer.sequence.length) {
                reference = alignmentContainer.sequence.charAt(idx);
            }
            if (!reference) {
                return 0
            }
            const base = alignment.readBaseAt(genomicLocation);
            const quality = alignment.readBaseQualityAt(genomicLocation);

            const coverageMap = alignmentContainer.coverageMap;
            const coverageMapIndex = Math.floor(genomicLocation - coverageMap.bpStart);
            const coverage = coverageMap.coverage[coverageMapIndex];

            // Insertions.  These are additive with base scores as they occur between bases, so you can have a
            // base mismatch AND an insertion
            let baseScore = 0;
            if (alignment.insertions) {
                for (let ins of alignment.insertions) {
                    if (ins.start === genomicLocation) {
                        baseScore = -coverage.ins;
                    }
                }
            }


            if (!base) {
                // Either deletion or skipped (splice junction)
                const delCount = coverage.del;
                if (delCount > 0) {
                    baseScore -= delCount;
                } else if (baseScore === 0) {    // Don't modify insertion score, if any
                    baseScore = 1;
                }
            } else {
                reference = reference.toUpperCase();
                if ('N' === base && baseScore === 0) {
                    baseScore = 2;
                } else if ((reference === base || '=' === base) && baseScore === 0) {
                    baseScore = 4 - quality / 1000;
                } else if ("X" === base || reference !== base) {
                    const count = coverage["pos" + base] + coverage["neg" + base];
                    baseScore -= (count + (quality / 1000));
                }
            }


            return baseScore
        }
    }


}

var orientationTypes = {

    "fr": {
        "F1R2": "LR",
        "F2R1": "LR",
        "F1F2": "LL",
        "F2F1": "LL",
        "R1R2": "RR",
        "R2R1": "RR",
        "R1F2": "RL",
        "R2F1": "RL"
    },

    "rf": {
        "R1F2": "LR",
        "R2F1": "LR",
        "R1R2": "LL",
        "R2R1": "LL",
        "F1F2": "RR",
        "F2F1": "RR",
        "F1R2": "RL",
        "F2R1": "RL"
    },

    "ff": {
        "F2F1": "LR",
        "R1R2": "LR",
        "F2R1": "LL",
        "R1F2": "LL",
        "R2F1": "RR",
        "F1R2": "RR",
        "R2R1": "RL",
        "F1F2": "RL"
    }
};

const alignmentSpace = 2;


/**
 * AlignmentContainer contains alignments for a genomic region and manages downsampling,  packing into rows,
 * as well as computation of coverage and base modification counts.   Coverage and base modification counts are
 * calculated prior to downsampling.  After initialization an AlignmentContainer exposes 3 properties used
 * by BamTrack
 *    - coverageMap
 *    - sequence
 *    - packedAlignments
 */
class AlignmentContainer {

    #unpacked = []
    baseModificationKeys = new Set()

    constructor(chr, start, end,
                {
                    samplingWindowSize,
                    samplingDepth,
                    alleleFreqThreshold,
                    colorBy
                }) {

        this.alleleFreqThreshold = alleleFreqThreshold === undefined ? 0.2 : alleleFreqThreshold;
        this.samplingWindowSize = samplingWindowSize || 100;
        this.samplingDepth = samplingDepth || 1000;

        this.chr = chr;
        this.start = Math.floor(start);
        this.end = Math.ceil(end);
        this.length = (end - start);
        this.coverageMap = new CoverageMap(chr, start, end, this.alleleFreqThreshold);
        this.downsampledIntervals = [];


        // Enable basemods
        if (colorBy && colorBy.startsWith("basemod")) {
            this.baseModCounts = new BaseModificationCounts();
        }

        // Transient members -- used during downsampling and prior to packing
        this.alignments = [];
        this.pairsCache = new Map();  // working cache of paired alignments by read name
        this.downsampledReads = new Set();
        this.currentBucket = new DownsampleBucket(this.start, this.start + this.samplingWindowSize, this);

        this.hasPairs = false; // until proven otherwise
    }

    pack({viewAsPairs, showSoftClips, expectedPairOrientation, groupBy, displayMode}) {

        let alignments = this.allAlignments();
        if (viewAsPairs) {
            alignments = pairAlignments(alignments);
        } else {
            alignments = unpairAlignments(alignments);
        }
        this.packAlignmentRows(alignments, showSoftClips, expectedPairOrientation, groupBy, displayMode);
        if (this.alignments) {
            delete this.alignments;
        }
    }

    packAlignmentRows(alignments, showSoftClips, expectedPairOrientation, groupBy, displayMode) {

        this.#unpacked = [];

        /**
         * Pack alignments densely, filling each row before proceeding to the next.  This is the packing code for
         * all display modes other than "FULL"
         *
         * @param groupedAlignments
         * @param groupName
         * @param packed
         */
        const packDense = (alignments, groupName) => {

            alignments.sort(function (a, b) {
                return showSoftClips ? a.scStart - b.scStart : a.start - b.start
            });

            const group = new Group$1(groupName);
            let alignmentRow;
            let nextStart = 0;
            let nextIDX = 0;
            const allocated = new Set();
            const startNewRow = () => {
                alignmentRow = new BamAlignmentRow();
                group.push(alignmentRow);
                nextStart = 0;
                nextIDX = 0;
                allocated.clear();
            };
            startNewRow();

            while (alignments.length > 0) {
                if (nextIDX >= 0 && nextIDX < alignments.length) {
                    const alignment = alignments[nextIDX];
                    allocated.add(alignment);
                    alignmentRow.alignments.push(alignment);
                    nextStart = showSoftClips ?
                        alignment.scStart + alignment.scLengthOnRef + alignmentSpace :
                        alignment.start + alignment.lengthOnRef + alignmentSpace;
                    nextIDX = binarySearch$1(alignments, (a) => (showSoftClips ? a.scStart : a.start) > nextStart, nextIDX);
                } else {
                    // Remove allocated alignments and start new row
                    alignments = alignments.filter(a => !allocated.has(a));
                    startNewRow();
                }
            }
            return group
        };

        const packFull = (alignments, groupName) => {

            alignments.sort(function (a, b) {
                return a.start - b.start
            });
            const group = new Group$1(groupName);
            const {start, end} = this.viewport.genomicRange();
            for (let a of alignments) {
                if (a.end < start || a.start > end) {
                    this.#unpacked.push(a);
                } else {
                    const alignmentRow = new BamAlignmentRow();
                    alignmentRow.alignments.push(a);
                    group.push(alignmentRow);
                }
            }
            return group
        };


        if (!alignments || alignments.length === 0) {
            return new Map()
        } else {

            // Separate alignments into groups
            const groupedAlignments = new Map();
            if (groupBy) {
                for (let a of alignments) {
                    const group = getGroupValue(a, groupBy, expectedPairOrientation) || "";
                    if (!groupedAlignments.has(group)) {
                        groupedAlignments.set(group, []);
                    }
                    groupedAlignments.get(group).push(a);
                }
            } else {
                groupedAlignments.set("", alignments);
            }

            const packed = new Map();
            const orderedGroupNames = Array.from(groupedAlignments.keys()).sort(getGroupComparator(groupBy));
            for (let groupName of orderedGroupNames) {
                const alignments = groupedAlignments.get(groupName);
                const group = "FULL" === displayMode ?
                    packFull(alignments, groupName) :
                    packDense(alignments, groupName);
                packed.set(groupName, group);
            }

            this.packedGroups = packed;
        }
    }


    push(alignment) {

        this.hasPairs = this.hasPairs || alignment.isPaired();

        this.coverageMap.incCounts(alignment);   // Count coverage before any downsampling

        const baseModificationSets = alignment.getBaseModificationSets();
        if(baseModificationSets) {
            for(let bms of baseModificationSets) {
                this.baseModificationKeys.add(bms.key);
            }
        }

        if (this.baseModCounts) {
            this.baseModCounts.incrementCounts(alignment);
        }

        if (this.downsampledReads.has(alignment.readName)) {
            this.currentBucket.downsampledCount++;
            return   // Mate already downsampled -- pairs and chimeric alignments are treated as a single alignment for downsampling
        }

        if (alignment.start >= this.currentBucket.end) {
            this.finishBucket();
            this.currentBucket = new DownsampleBucket(alignment.start, alignment.start + this.samplingWindowSize, this);
        }

        this.currentBucket.addAlignment(alignment);

    }

    finish() {

        if (this.currentBucket !== undefined) {
            this.finishBucket();
        }

        this.hasAlignments = this.alignments.length > 0;

        this.alignments.sort(function (a, b) {
            return a.start - b.start
        });

        if (this.baseModCounts) {
            this.baseModCounts.computeSimplex();
        }

        delete this.currentBucket;
        delete this.pairsCache;
        delete this.downsampledReads;

    }

    contains(chr, start, end) {
        return this.chr === chr &&
            this.start <= start &&
            this.end >= end
    }

    hasDownsampledIntervals() {
        return this.downsampledIntervals && this.downsampledIntervals.length > 0
    }

    finishBucket() {
        this.alignments = this.alignments.concat(this.currentBucket.alignments);
        if (this.currentBucket.downsampledCount > 0) {
            this.downsampledIntervals.push(new DownsampledInterval(
                this.currentBucket.start,
                this.currentBucket.end,
                this.currentBucket.downsampledCount));
        }
    }

    allAlignments() {
        if (this.alignments) {
            return this.alignments
        } else if (this.packedGroups) {
            const all = Array.from(this.packedGroups.values()).flatMap(group => group.rows.flatMap(row => row.alignments));
            if (this.#unpacked && this.#unpacked.length > 0) {
                for (let a of this.#unpacked) {
                    all.push(a);
                }
            }
            return all
        } else {
            return []
        }
    }

    getMax(start, end) {
        return this.coverageMap.getMax(start, end)
    }

    sortRows(options) {
        if(this.packedGroups) {
            for (let group of this.packedGroups.values()) {
                group.sortRows(options, this);
            }
        }
    }
}


class DownsampleBucket {

    constructor(start, end, {samplingDepth, downsampledReads, pairsCache}) {

        this.start = start;
        this.end = end;
        this.alignments = [];
        this.downsampledCount = 0;
        this.samplingDepth = samplingDepth;
        this.downsampledReads = downsampledReads;
        this.pairsCache = pairsCache;
        this.hasPairs = false; // until proven otherwise
    }

    addAlignment(alignment) {


        this.hasPairs = this.hasPairs || alignment.isPaired();

        const samplingDepth = this.hasPairs ? Math.ceil(this.samplingDepth / 2) : this.samplingDepth;

        if (canBePaired(alignment)) {
            const pairedAlignment = this.pairsCache.get(alignment.readName);
            if (pairedAlignment) {
                // Not subject to downsampling, just update the existing paired alignment
                pairedAlignment.setSecondAlignment(alignment);
                this.pairsCache.delete(alignment.readName);
                return
            }
        }

        if (this.alignments.length < samplingDepth) {

            if (canBePaired(alignment)) {
                // First alignment of a pair
                const pairedAlignment = new PairedAlignment(alignment);
                this.pairsCache.set(alignment.readName, pairedAlignment);
                this.alignments.push(pairedAlignment);
            } else {
                this.alignments.push(alignment);
            }

        } else {
            // Alignment count has reached sampling depth, use resevoir sampling

            const idx = Math.floor(Math.random() * (samplingDepth + this.downsampledCount - 1));

            if (idx < samplingDepth) {

                // Select an alignment to replace
                const replacedAlignment = this.alignments[idx];
                if (this.pairsCache.has(replacedAlignment.readName)) {
                    this.pairsCache.delete(replacedAlignment.readName);
                }

                if (canBePaired(alignment)) {
                    const pairedAlignment = new PairedAlignment(alignment);
                    this.pairsCache.set(alignment.readName, pairedAlignment);
                    this.alignments[idx] = pairedAlignment;
                } else {
                    this.alignments[idx] = alignment;
                }
                this.downsampledReads.add(replacedAlignment.readName);

            } else {
                this.downsampledReads.add(alignment.readName);
            }
            this.downsampledCount++;
        }
    }
}

class CoverageMap {

    constructor(chr, start, end, alleleFreqThreshold) {

        this.chr = chr;
        this.bpStart = start;
        this.length = (end - start);

        this.coverage = new Array(this.length);
        this.maximum = 0;

        this.threshold = alleleFreqThreshold;
        this.qualityWeight = true;
    }

    /**
     * Return the maximum coverage value between start and end.  This is used for autoscaling.
     * @param start
     * @param end
     */
    getMax(start, end) {
        let max = 0;
        const len = this.coverage.length;
        for (let i = 0; i < len; i++) {
            const pos = this.bpStart + i;
            if (pos > end) break
            const cov = this.coverage[i];
            if (pos >= start && cov) {
                max = Math.max(max, cov.total);
            }
        }
        return max
    }

    incCounts(alignment) {

        var self = this;

        if (alignment.blocks === undefined) {
            incBlockCount(alignment);
        } else {
            alignment.blocks.forEach(function (block) {
                incBlockCount(block);
            });
        }

        if (alignment.gaps) {
            for (let del of alignment.gaps) {
                if (del.type === 'D') {
                    const offset = del.start - self.bpStart;
                    for (let i = offset; i < offset + del.len; i++) {
                        if (i < 0) continue
                        if (!this.coverage[i]) {
                            this.coverage[i] = new Coverage(self.threshold);
                        }
                        this.coverage[i].del++;
                    }
                }
            }
        }

        if (alignment.insertions) {
            for (let del of alignment.insertions) {
                const i = del.start - this.bpStart;
                if (i < 0) continue
                if (!this.coverage[i]) {
                    this.coverage[i] = new Coverage(self.threshold);
                }
                this.coverage[i].ins++;
            }
        }

        function incBlockCount(block) {

            if ('S' === block.type) return

            const seq = alignment.seq;
            const qual = alignment.qual;
            const seqOffset = block.seqOffset;

            for (let i = block.start - self.bpStart, j = 0; j < block.len; i++, j++) {

                if (!self.coverage[i]) {
                    self.coverage[i] = new Coverage(self.threshold);
                }

                const base = (seq == undefined) ? "N" : seq.charAt(seqOffset + j);
                const key = (alignment.strand) ? "pos" + base : "neg" + base;
                const q = qual && seqOffset + j < qual.length ? qual[seqOffset + j] : 30;

                self.coverage[i][key] += 1;
                self.coverage[i]["qual" + base] += q;

                self.coverage[i].total += 1;
                self.coverage[i].qual += q;

                self.maximum = Math.max(self.coverage[i].total, self.maximum);

            }
        }
    }

    getPosCount(pos, base) {

        const offset = pos - this.bpStart;
        if (offset < 0 || offset >= this.coverage.length) return 0
        const c = this.coverage[offset];

        switch (base) {
            case 'A':
            case 'a':
                return c.posA
            case 'C':
            case 'c':
                return c.posC
            case 'T':
            case 't':
                return c.posT
            case 'G':
            case 'g':
                return c.posG
            case 'N':
            case 'n':
                return c.posN
            default:
                return 0
        }
    }

    getNegCount(pos, base) {
        const offset = pos - this.bpStart;
        if (offset < 0 || offset >= this.coverage.length) return 0
        const c = this.coverage[offset];

        switch (base) {
            case 'A':
            case 'a':
                return c.negA
            case 'C':
            case 'c':
                return c.negC
            case 'T':
            case 't':
                return c.negT
            case 'G':
            case 'g':
                return c.negG
            case 'N':
            case 'n':
                return c.negN
            default:
                return 0
        }

    }

    getCount(pos, base) {
        return this.getPosCount(pos, base) + this.getNegCount(pos, base)
    }

    getTotalCount(pos) {
        const offset = pos - this.bpStart;
        return (offset >= 0 && offset < this.coverage.length) ? this.coverage[offset].total : 0
    }
}


class Coverage {

    constructor(alleleThreshold) {

        this.qualityWeight = true;

        this.posA = 0;
        this.negA = 0;

        this.posT = 0;
        this.negT = 0;

        this.posC = 0;
        this.negC = 0;
        this.posG = 0;

        this.negG = 0;

        this.posN = 0;
        this.negN = 0;

        this.pos = 0;
        this.neg = 0;

        this.qualA = 0;
        this.qualT = 0;
        this.qualC = 0;
        this.qualG = 0;
        this.qualN = 0;

        this.qual = 0;

        this.total = 0;
        this.del = 0;
        this.ins = 0;

        this.threshold = alleleThreshold;
    }

    hoverText() {
        const pos = this.posA + this.posT + this.posC + this.posG + this.posN;
        const neg = this.negA + this.negT + this.negC + this.negG + this.negN;
        return `${this.total} (${pos}+, ${neg}-)`
    }

    isMismatch(refBase) {
        const threshold = this.threshold * ((this.qualityWeight && this.qual) ? this.qual : this.total);
        let mismatchQualitySum = 0;
        for (let base of ["A", "T", "C", "G"]) {
            if (base !== refBase) {
                mismatchQualitySum += ((this.qualityWeight && this.qual) ? this["qual" + base] : (this["pos" + base] + this["neg" + base]));
            }
        }
        return mismatchQualitySum >= threshold
    }
}

class DownsampledInterval {

    constructor(start, end, counts) {
        this.start = start;
        this.end = end;
        this.counts = counts;
    }

    popupData(genomicLocation) {
        return [
            {name: "start", value: Math.floor(this.start + 1)},
            {name: "end", value: this.end},
            {name: "# downsampled:", value: this.counts}]
    }
}

class Group$1 {

    pixelTop = 0
    pixelBottom = 0
    rows = []


    constructor(name) {
        this.name = this.name;
    }

    push(row) {
        this.rows.push(row);
    }

    get length() {
        return this.rows.length
    }

    sortRows(options, alignmentContainer) {

        const newRows = [];
        const undefinedRow = [];
        for (let row of this.rows) {
            const alignment = row.findAlignment(options.position, options.sortAsPairs);
            if (undefined !== alignment) {
                newRows.push(row);
            } else {
                undefinedRow.push(row);
            }
        }

        newRows.sort((rowA, rowB) => {
            const direction = options.direction;
            const rowAValue = rowA.getSortValue(options, alignmentContainer);
            const rowBValue = rowB.getSortValue(options, alignmentContainer);

            if (rowBValue === undefined && rowBValue !== undefined) return 1
            else if (rowAValue !== undefined && rowBValue === undefined) return -1

            const i = rowAValue > rowBValue ? 1 : (rowAValue < rowBValue ? -1 : 0);
            return true === direction ? i : -i
        });

        for (let row of undefinedRow) {
            newRows.push(row);
        }

        this.rows = newRows;

    }

}


function canBePaired(alignment) {
    return alignment.isPaired() &&
        alignment.mate &&
        alignment.isMateMapped() &&
        alignment.chr === alignment.mate.chr &&
        (alignment.isFirstOfPair() || alignment.isSecondOfPair()) && !(alignment.isSecondary() || alignment.isSupplementary())
}


function pairAlignments(alignments) {

    const pairCache = new Map();
    const result = alignments.map(alignment => {
        if (canBePaired(alignment)) {
            let pairedAlignment = pairCache.get(alignment.readName);
            if (pairedAlignment) {
                pairedAlignment.setSecondAlignment(alignment);
                pairCache.delete(alignment.readName);
                return pairedAlignment
            } else {
                pairedAlignment = new PairedAlignment(alignment);
                pairCache.set(alignment.readName, pairedAlignment);
                return pairedAlignment
            }
        } else {
            return alignment
        }
    });
    return result
}

function unpairAlignments(alignments) {
    return alignments.flatMap(alignment => alignment instanceof PairedAlignment ?
        [alignment.firstAlignment, alignment.secondAlignment].filter(Boolean) :
        [alignment])
}

/**
 * Return 0 <= i <= array.length such that !pred(array[i - 1]) && pred(array[i]).
 *
 * returns an index 0 ≤ i ≤ array.length such that the given predicate is false for array[i - 1] and true for array[i]* *
 */
function binarySearch$1(array, pred, min) {
    let lo = min - 1, hi = array.length;
    while (1 + lo < hi) {
        const mi = lo + ((hi - lo) >> 1);
        if (pred(array[mi])) {
            hi = mi;
        } else {
            lo = mi;
        }
    }
    return hi
}

function getGroupValue(al, groupBy, expectedPairOrientation) {

    let tag, chr, pos;
    if (groupBy.startsWith("tag:")) {
        tag = groupBy.substring(4);
        groupBy = "tag";
    } else if (groupBy.startsWith("base:")) {
        const tokens = groupBy.split(":");
        if (tokens.length === 3) {
            groupBy = "base";
            chr = tokens[1];
            pos = Number.parseInt(tokens[2].replaceAll(",", "")) - 1;
        }
    }

    switch (groupBy) {
        // case 'HAPLOTYPE':
        //     return al.getHaplotypeName();
        case 'strand':
            return al.strand ? '+' : '-'
        case 'firstOfPairStrand':
            const strand = al.firstOfPairStrand;
            return strand === undefined ? "" : strand ? '+' : '-'
        case 'mateChr':
            return (al.mate && al.isMateMapped()) ? al.mate.chr : ""
        case 'pairOrientation':
            return orientationTypes[expectedPairOrientation][al.pairOrientation] || ""
        case 'chimeric':
            return al.tags()['SA'] ? "chimeric" : ""
        case 'supplementary':
            return al.isSupplementary ? "supplementary" : ""
        case 'readOrder':
            if (al.isPaired() && al.isFirstOfPair()) {
                return "first"
            } else if (al.isPaired() && al.isSecondOfPair()) {
                return "second"
            } else {
                return ""
            }
        case 'phase':
            return al.tags()['HP'] || ""
        case 'tag':
            return al.tags()[tag] || ""
        // Add cases for other options as needed
        case 'base':

            //    1: alignments with a base at the position
            //    2: alignments with a gap at the position
            //    3: alignment that do not overlap the position (or are on a different chromosome)

            if (al.chr === chr &&
                al.start <= pos &&
                al.end > pos) {

                const baseAtPos = al.readBaseAt(pos);
                if (baseAtPos) {
                    return baseAtPos
                } else {
                    return "GAP"
                }
            } else { // does not overlap position
                return ""
            }

        default:
            return undefined
    }
}

function getGroupComparator(groupName, expectedPairOrientation) {
    switch (groupName) {
        case "pairOrientation":
            return pairOrientationComparator()
        case 'strand':
        case 'firstOfPairStrand':
            return groupStrandComparator
        default:
            return groupName && groupName.startsWith("base:") ?
                baseComparator :
                groupNameComparator
    }
}

const baseRank = new Map([["A", 1], ["T", 2], ["C", 3], ["G", 4], ["N", 5], ["GAP", 5], ["", 7]]);

function baseComparator(o1, o2) {
    if (baseRank.has(o1) && baseRank.has(o2)) {
        return baseRank.get(o1) - baseRank.get(o2)
    } else {
        return o1.localeCompare(o2, undefined, {sensitivity: 'base'})
    }
}

function groupStrandComparator(o1, o2) {
    if (o1 === o2) {
        return 0
    } else if (o1 && o2) {
        return -o1.localeCompare(o2)
    } else {
        return o1 ? 1 : -1
    }
}

function groupNameComparator(o1, o2) {
    if (!o1 && !o2) {
        return 0
    } else if (!o1) {
        return 1
    } else if (!o2) {
        return -1
    } else {
        // no nulls
        if (o1 === o2) {
            return 0
        } else {
            if (isNumber(o1) && typeof isNumber(o2)) {
                return Number.parseFloat(o1) - Number.parseFloat(o2)
            } else {
                let s1 = o1.toString();
                let s2 = o2.toString();
                return s1.localeCompare(s2, undefined, {sensitivity: 'base'})
            }
        }
    }
}

function pairOrientationComparator(expectedPairOrientation) {
    const orientationValues = ['LL', 'RR', 'RL', 'LR', ''];
    return (o1, o2) => orientationValues.indexOf(o1) - orientationValues.indexOf(o2)
}

class SupplementaryAlignment {

    constructor(rec) {
        const tokens = rec.split(',');
        this.chr = tokens[0];
        this.start = parseInt(tokens[1]);
        this.strand = tokens[2].charAt(0);
        this.mapQ = parseInt(tokens[4]);
        this.numMismatches = parseInt(tokens[5]);
        this.lenOnRef = computeLengthOnReference(tokens[3]);
    }

    printString() {
        return this.chr + ":" + numberFormatter$1(this.start) + "-" + numberFormatter$1(this.start + this.lenOnRef)
            + " (" + this.strand + ") = " + numberFormatter$1(this.lenOnRef) + "bp @MAPQ: " + this.mapQ + " NM: " + this.numMismatches
    }
}

function createSupplementaryAlignments(str) {
    const tokens = str.split(';');
    return tokens.filter(t => t.length > 0).map(str => new SupplementaryAlignment(str))
}


function computeLengthOnReference(cigarString) {

    let len = 0;
    let buf = '';

    for (let i = 0; i < cigarString.length; i++) {
        const c = cigarString.charCodeAt(i);
        if (c > 47 && c < 58) {
            buf += cigarString.charAt(i);
        } else {
            switch (c) {
                case 78:  // N
                case 68:  // D
                case 77:  // M
                case 61:  // =
                case 88:  // X
                    len += parseInt(buf.toString());
            }
            buf = '';
        }
    }
    return len
}

const READ_PAIRED_FLAG = 0x1;
const PROPER_PAIR_FLAG = 0x2;
const READ_UNMAPPED_FLAG = 0x4;
const MATE_UNMAPPED_FLAG = 0x8;
const READ_STRAND_FLAG$2 = 0x10;
const MATE_STRAND_FLAG$2 = 0x20;
const FIRST_OF_PAIR_FLAG = 0x40;
const SECOND_OF_PAIR_FLAG = 0x80;
const SECONDARY_ALIGNMNET_FLAG = 0x100;
const READ_FAILS_VENDOR_QUALITY_CHECK_FLAG = 0x200;
const DUPLICATE_READ_FLAG = 0x400;
const SUPPLEMENTARY_ALIGNMENT_FLAG = 0x800;
const ELEMENT_SIZE$1 = {
    c: 1,
    C: 1,
    s: 2,
    S: 2,
    i: 4,
    I: 4,
    f: 4
};

const MAX_CIGAR = 50;

/**
 * readName
 * chr
 * cigar
 * lengthOnRef
 * start
 * seq
 * qual
 * mq
 * strand
 * blocks
 */

class BamAlignment {

    constructor() {
        this.hidden = false;
    }

    isMapped() {
        return (this.flags & READ_UNMAPPED_FLAG) === 0
    }

    isPaired() {
        return (this.flags & READ_PAIRED_FLAG) !== 0
    }

    isProperPair() {
        return (this.flags & PROPER_PAIR_FLAG) !== 0
    }

    isFirstOfPair() {
        return (this.flags & FIRST_OF_PAIR_FLAG) !== 0
    }

    isSecondOfPair() {
        return (this.flags & SECOND_OF_PAIR_FLAG) !== 0
    }

    isSecondary() {
        return (this.flags & SECONDARY_ALIGNMNET_FLAG) !== 0
    }

    isSupplementary() {
        return (this.flags & SUPPLEMENTARY_ALIGNMENT_FLAG) !== 0
    }

    isFailsVendorQualityCheck() {
        return (this.flags & READ_FAILS_VENDOR_QUALITY_CHECK_FLAG) !== 0
    }

    isDuplicate() {
        return (this.flags & DUPLICATE_READ_FLAG) !== 0
    }

    isMateMapped() {
        return (this.flags & MATE_UNMAPPED_FLAG) === 0
    }

    isNegativeStrand() {
        return (this.flags & READ_STRAND_FLAG$2) !== 0
    }

    isMateNegativeStrand() {
        return (this.flags & MATE_STRAND_FLAG$2) !== 0
    }

    hasTag(tag) {
        const tmpTags = this.tagDict || decodeTags(this.tagBA);
        return tmpTags.hasOwnProperty(tag)
    }

    tags() {
        return this.tagDict
    }

    getTag(key) {
        return this.tags()[key]
    }


    /**
     * @returns a boolean indicating strand of first in pair, true for forward, false for reverse, and undefined
     * if this is not paired or is not first and mate is not mapped.
     */
    get firstOfPairStrand() {
        if (this.isPaired()) {
            if (this.isFirstOfPair()) {
                return this.strand
            } else if (this.isMateMapped()) {
                return this.mate.strand
            }
        }
        return undefined
    }


    /**
     * Does alignment (or alignment extended by soft clips) contain the genomic location?
     *
     * @param genomicLocation
     * @param showSoftClips
     * @returns {boolean|boolean}
     */
    containsLocation(genomicLocation, showSoftClips) {
        const s = showSoftClips ? this.scStart : this.start;
        const l = showSoftClips ? this.scLengthOnRef : this.lengthOnRef;
        return (genomicLocation >= s && genomicLocation <= (s + l))
    }

    popupData(genomicLocation, hiddenTags, showTags) {

        // if the user clicks on a base next to an insertion, show just the
        // inserted bases in a popup (like in desktop IGV).
        const nameValues = [];

        // Consert genomic location to int
        genomicLocation = Math.floor(genomicLocation);

        if (this.insertions) {

            const seq = this.seq;

            for (let insertion of this.insertions) {
                var ins_start = insertion.start;
                if (genomicLocation === ins_start || genomicLocation === ins_start - 1) {
                    nameValues.push({name: 'Insertion', value: seq.substr(insertion.seqOffset, insertion.len)});
                    nameValues.push({name: 'Location', value: ins_start});
                    return nameValues
                }
            }
        }

        nameValues.push({name: 'Read Name', value: this.readName});

        // Sample
        // Read group
        nameValues.push('<hr/>');

        // Add 1 to genomic location to map from 0-based computer units to user-based units
        nameValues.push({name: 'Alignment Start', value: numberFormatter$1(1 + this.start), borderTop: true});
        nameValues.push({name: 'Read Strand', value: (true === this.strand ? '(+)' : '(-)'), borderTop: true});

        // Abbreviate long cigar strings, keeping the beginning and end to show cliping
        let cigar = this.cigar;
        if (cigar && cigar.length > MAX_CIGAR) {
            const half = MAX_CIGAR / 2;
            cigar = `${cigar.substring(0, half - 2)} ... ${cigar.substring(cigar.length - half + 2)}`;
        }
        nameValues.push({name: 'Cigar', value: cigar});

        nameValues.push({name: 'Mapping Quality', value: this.mq});
        nameValues.push({name: 'Secondary', value: yesNo(this.isSecondary())});
        nameValues.push({name: 'Supplementary', value: yesNo(this.isSupplementary())});
        nameValues.push({name: 'Duplicate', value: yesNo(this.isDuplicate())});
        nameValues.push({name: 'Failed QC', value: yesNo(this.isFailsVendorQualityCheck())});

        if (this.isPaired()) {
            nameValues.push('<hr/>');
            nameValues.push({name: 'First in Pair', value: !this.isSecondOfPair(), borderTop: true});
            nameValues.push({name: 'Mate is Mapped', value: yesNo(this.isMateMapped())});
            if (this.pairOrientation) {
                nameValues.push({name: 'Pair Orientation', value: this.pairOrientation});
            }
            if (this.isMateMapped()) {
                nameValues.push({name: 'Mate Chromosome', value: this.mate.chr});
                nameValues.push({name: 'Mate Start', value: (this.mate.position + 1)});
                nameValues.push({name: 'Mate Strand', value: (true === this.mate.strand ? '(+)' : '(-)')});
                nameValues.push({name: 'Insert Size', value: this.fragmentLength});
                // Mate Start
                // Mate Strand
                // Insert Size
            }
            // First in Pair
            // Pair Orientation

        }

        const tagDict = this.tags();

        if (tagDict.hasOwnProperty('SA')) {
            nameValues.push('<hr/>');
            nameValues.push({name: 'Supplementary Alignments', value: ''});
            const sa = createSupplementaryAlignments(tagDict['SA']);
            if (sa) {
                nameValues.push('<ul>');
                for (let s of sa) {
                    nameValues.push(`<li>${s.printString()}</li>`);
                }
                nameValues.push('</ul>');
            }
        }

        nameValues.push('<hr/>');
        for (let key in tagDict) {
            if (showTags?.has(key)) {
                nameValues.push({name: key, value: tagDict[key]});
            } else if (showTags) {
                hiddenTags.add(key);
            } else if (!hiddenTags.has(key)) {
                nameValues.push({name: key, value: tagDict[key]});
            }
        }

        if (hiddenTags && hiddenTags.size > 0) {
            nameValues.push({name: 'Hidden Tags', value: Array.from(hiddenTags).join(", ")});
        }

        nameValues.push('<hr/>');
        nameValues.push({name: 'Genomic Location: ', value: numberFormatter$1(1 + genomicLocation)});
        nameValues.push({name: 'Read Base:', value: this.readBaseAt(genomicLocation)});
        nameValues.push({name: 'Base Quality:', value: this.readBaseQualityAt(genomicLocation)});

        const bmSets = this.getBaseModificationSets();
        if (bmSets) {
            const i = this.positionToReadIndex(genomicLocation);
            if (undefined !== i) {
                let found = false;
                for (let bmSet of bmSets) {
                    if (bmSet.containsPosition(i)) {
                        if (!found) {
                            nameValues.push('<hr/>');
                            nameValues.push('<b>Base modifications:</b>');
                            found = true;
                        }
                        const lh = Math.round((100 / 255) * byteToUnsignedInt(bmSet.likelihoods.get(i)));
                        nameValues.push(`${bmSet.fullName()} @ likelihood =  ${lh}%`);
                    }
                }
            }
        }

        return nameValues


        function yesNo(bool) {
            return bool ? 'Yes' : 'No'
        }
    }

    readBaseAt(genomicLocation) {

        const block = blockAtGenomicLocation(this.blocks, genomicLocation);
        if (block) {
            if ("*" === this.seq) {
                return "*"
            } else {
                const idx = block.seqIndexAt(genomicLocation);
                // if (idx >= 0 && idx < this.seq.length) {
                return this.seq[idx]
                //  }
            }
        } else {
            return undefined
        }
    }

    readBaseQualityAt(genomicLocation) {

        const block = blockAtGenomicLocation(this.blocks, genomicLocation);
        if (block) {
            if ("*" === this.qual) {
                return 30
            } else {
                const idx = block.seqIndexAt(genomicLocation);
                if (idx >= 0 && this.qual && idx < this.qual.length) {
                    return this.qual[idx]
                } else {
                    return 30
                }
            }
        } else {
            return undefined
        }
    }

    gapSizeAt(genomicLocation) {
        if (this.gaps) {
            for (let gap of this.gaps) {
                if (genomicLocation >= gap.start && genomicLocation < gap.start + gap.len) {
                    return gap.len
                }
            }
        }
        return 0
    }

    /**
     * Return soft clipped blocks, if they exist, keyed by alignment end (left or right)
     */
    softClippedBlocks() {
        let left;
        let right;
        let interiorSeen;
        for (let b of this.blocks) {
            if ('S' === b.type) {
                if (interiorSeen) {
                    right = b;
                } else {
                    left = b;
                }
            } else if ('H' !== b.type) {
                interiorSeen = true;
            }
        }
        return {left, right}
    }

    getBaseModificationSets() {

        if (!this.baseModificationSets && (this.tagDict["MM"] || this.tagDict["Mm"])) {

            const mm = this.tagDict["MM"] || this.tagDict["Mm"];
            const ml = this.tagDict["ML"] || this.tagDict["Ml"];

            if (isString$3(mm) && (!ml || Array.isArray(ml))) { // minimal validation, 10X uses these reserved tags for something completely different
                if (mm.length === 0) {
                    this.baseModificationSets = EMPTY_SET;
                } else {
                    //getBaseModificationSets(mm, ml, sequence, isNegativeStrand)
                    this.baseModificationSets = getBaseModificationSets(mm, ml, this.seq, this.isNegativeStrand());
                }
                //}
            }
        }
        return this.baseModificationSets
    }

    getGroupValue(groupBy, tag, expectedPairOrientation) {

        const al = this;
        switch (groupBy) {
            // case 'HAPLOTYPE':
            //     return al.getHaplotypeName();
            case 'strand':
                return al.strand ? '+' : '-'
            case 'firstOfPairStrand':
                const strand = al.firstOfPairStrand;
                return strand === undefined ? "" : strand ? '+' : '-'
            case 'mateChr':
                return (al.mate && al.isMateMapped()) ? al.mate.chr : ""
            case 'pairOrientation':
                return orientationTypes[expectedPairOrientation][al.pairOrientation] || ""
            case 'chimeric':
                return al.tags()['SA'] ? "chimeric" : ""
            case 'supplementary':
                return al.isSupplementary ? "supplementary" : ""
            case 'readOrder':
                if (al.isPaired() && al.isFirstOfPair()) {
                    return "first"
                } else if (al.isPaired() && al.isSecondOfPair()) {
                    return "second"
                } else {
                    return ""
                }
            case 'phase':
                return al.tags()['HP'] || ""
            case 'tag':
                return al.tags()[tag] || ""
            // Add cases for other options as needed
            default:
                return undefined
        }
    }

    positionToReadIndex(position) {
        const block = blockAtGenomicLocation(this.blocks, position);
        if (block) {
            return (position - block.start) + block.seqOffset
        } else {
            return undefined
        }
    }


}

const EMPTY_SET = new Set();

function blockAtGenomicLocation(blocks, genomicLocation) {

    for (let i = 0; i < blocks.length; i++) {
        const block = blocks[i];
        if (genomicLocation >= block.start && genomicLocation < block.start + block.len) {
            return block
        }
    }
    return undefined
}

function decodeTags(ba) {

    let p = 0;
    const len = ba.length;
    const tags = {};

    while (p < len) {
        const tag = String.fromCharCode(ba[p]) + String.fromCharCode(ba[p + 1]);
        p += 2;

        const type = String.fromCharCode(ba[p++]);
        let value;
        if (type === 'A') {
            value = String.fromCharCode(ba[p]);
            p++;
        } else if (type === 'i' || type === 'I') {
            value = readInt$1(ba, p);
            p += 4;
        } else if (type === 'c') {
            value = readInt8(ba, p);
            p++;
        } else if (type === 'C') {
            value = readUInt8(ba, p);
            p++;
        } else if (type === 's' || type === 'S') {
            value = readShort(ba, p);
            p += 2;
        } else if (type === 'f') {
            value = readFloat(ba, p);
            p += 4;
        } else if (type === 'Z') {
            value = '';
            for (; ;) {
                var cc = ba[p++];
                if (cc === 0) {
                    break
                } else {
                    value += String.fromCharCode(cc);
                }
            }
        } else if (type === 'B') {
            //‘cCsSiIf’, corresponding to int8 , uint8 t, int16 t, uint16 t, int32 t, uint32 t and float
            const elementType = String.fromCharCode(ba[p++]);
            let elementSize = ELEMENT_SIZE$1[elementType];
            if (elementSize === undefined) {
                tags[tag] = `Error: unknown element type '${elementType}'`;
                break
            }
            const numElements = readInt$1(ba, p);
            p += 4;
            const pEnd = p + numElements * elementSize;
            value = [];
            const dataView = new DataView(ba.buffer);
            while (p < pEnd) {
                switch (elementType) {
                    case 'c':
                        value.push(dataView.getInt8(p));
                        break
                    case 'C':
                        value.push(dataView.getUint8(p));
                        break
                    case 's':
                        value.push(dataView.getInt16(p));
                        break
                    case 'S':
                        value.push(dataView.getUint16(p));
                        break
                    case 'i':
                        value.push(dataView.getInt32(p));
                        break
                    case 'I':
                        value.push(dataView.getUint32(p));
                        break
                    case 'f':
                        value.push(dataView.getFloat32(p));
                }
                p += elementSize;
            }
        } else {
            //'Unknown type ' + type;
            value = 'Error unknown type: ' + type;
            tags[tag] = value;
            break
        }
        tags[tag] = value;
    }
    return tags
}


function readInt$1(ba, offset) {
    return (ba[offset + 3] << 24) | (ba[offset + 2] << 16) | (ba[offset + 1] << 8) | (ba[offset])
}

function readShort(ba, offset) {
    return (ba[offset + 1] << 8) | (ba[offset])
}

function readFloat(ba, offset) {
    const dataView = new DataView(ba.buffer);
    return dataView.getFloat32(offset)
}

function readInt8(ba, offset) {
    const dataView = new DataView(ba.buffer);
    return dataView.getInt8(offset)
}

function readUInt8(ba, offset) {
    const dataView = new DataView(ba.buffer);
    return dataView.getUint8(offset)
}

/**
 * Created by jrobinso on 4/5/18.
 */

/**
 * Expected properties
 *   start: genomic position
 *   seqOffset: index offset to read sequence for this block's sequence
 *   len: length of block
 *   type: from CIGAR string (S, I, M, ...)
 */

class AlignmentBlock {
    constructor(b) {
        if (b) {
            Object.assign(this, b);
        }
    }

    seqIndexAt(genomicLocation) {
        return Math.floor(genomicLocation) - this.start + this.seqOffset
    }
}

class BamFilter {
    constructor(options) {
        if (!options) options = {};
        this.vendorFailed = options.vendorFailed = false !== options.vendorFailed;
        this.duplicate = options.duplicate = false !== options.duplicate;
        this.secondary = options.secondary || false;
        this.supplementary = options.supplementary || false;
        this.mq = options.mq || 0;
        if (options.readgroups) {
            this.readgroups = new Set(options.readgroups);
        }
    }

    pass(alignment) {
        if(!alignment.isMapped()) return false
        if (this.vendorFailed && alignment.isFailsVendorQualityCheck()) return false
        if (this.duplicate && alignment.isDuplicate()) return false
        if (this.secondary && alignment.isSecondary()) return false
        if (this.supplementary && alignment.isSupplementary()) return false
        if (alignment.mq < this.mq) return false
        if (this.readgroups) {
            const rg = alignment.tags()['RG'];
            return this.readgroups.has(rg)
        }
        return true
    }
}

/**
 * This code is based on the Biodalliance BAM reader by Thomas Down,  2011
 *
 * https://github.com/dasmoth/dalliance/blob/master/js/bam.js
 */
//=ACMGRSVTWYHKDBN
const BAM1_MAGIC_BYTES = new Uint8Array([0x42, 0x41, 0x4d, 0x01]); // BAM\1
const BAM1_MAGIC_NUMBER = readInt(BAM1_MAGIC_BYTES, 0);

const SEQ_DECODER = ['=', 'A', 'C', 'M', 'G', 'R', 'S', 'V', 'T', 'W', 'Y', 'H', 'K', 'D', 'B', 'N'];
const CIGAR_DECODER = ['M', 'I', 'D', 'N', 'S', 'H', 'P', '=', 'X', '?', '?', '?', '?', '?', '?', '?'];
const READ_STRAND_FLAG$1 = 0x10;
const MATE_STRAND_FLAG$1 = 0x20;
const ELEMENT_SIZE = {
    c: 1,
    C: 1,
    s: 2,
    S: 2,
    i: 4,
    I: 4,
    f: 4
};

const DEFAULT_ALLELE_FREQ_THRESHOLD = 0.2;
const DEFAULT_SAMPLING_WINDOW_SIZE = 100;
const DEFAULT_SAMPLING_DEPTH = 500;
const MAXIMUM_SAMPLING_DEPTH = 10000;

const BamUtils = {

    readHeader: async function (url, options, genome) {

        const compressedBuffer = await igvxhr$1.loadArrayBuffer(url, options);
        const uncba = unbgzf$1(compressedBuffer);
        return BamUtils.decodeBamHeader(uncba, genome)
    },

    /**
     * @param ba - UInt8Array  bytes to decode
     *
     * @returns {{size: *, chrNames: *[], magicNumber: *, chrToIndex: {}}}
     */
    decodeBamHeader: function (ba) {


        const magic = readInt(ba, 0);
        if (magic !== BAM1_MAGIC_NUMBER) {
            throw new Error('BAM header errror: bad magic number.  This could be caused by either a corrupt or missing file.')
        }

        const samHeaderLen = readInt(ba, 4);
        let samHeader = '';
        for (var i = 0; i < samHeaderLen; ++i) {
            samHeader += String.fromCharCode(ba[i + 8]);
        }

        const nRef = readInt(ba, samHeaderLen + 8);
        let p = samHeaderLen + 12;

        const chrToIndex = {};
        const chrNames = [];

        for (i = 0; i < nRef; ++i) {
            const len = readInt(ba, p);
            let name = '';
            for (var j = 0; j < len - 1; ++j) {
                name += String.fromCharCode(ba[p + 4 + j]);
            }

            chrToIndex[name] = i;
            chrNames[i] = name;

            p = p + 8 + len;
        }

        return {
            magicNumber: magic,
            size: p,
            chrNames: chrNames,
            chrToIndex: chrToIndex
        }

    },

    bam_tag2cigar: function (ba, block_end, seq_offset, lseq, al, cigarArray) {

        function type2size(x) {
            if (x === 'C' || x === 'c' || x === 'A') return 1
            else if (x === 'S' || x === 's') return 2
            else if (x === 'I' || x === 'i' || x === 'f') return 4
            else return 0
        }

        // test if the real CIGAR is encoded in a CG:B,I tag
        if (cigarArray.length !== 1 || al.start < 0) return false
        var p = seq_offset + ((lseq + 1) >> 1) + lseq;
        while (p + 4 < block_end) {
            var tag = String.fromCharCode(ba[p]) + String.fromCharCode(ba[p + 1]);
            if (tag === 'CG') break
            var type = String.fromCharCode(ba[p + 2]);
            if (type === 'B') { // the binary array type
                type = String.fromCharCode(ba[p + 3]);
                var size = type2size(type);
                var len = readInt(ba, p + 4);
                p += 8 + size * len;
            } else if (type === 'Z' || type === 'H') { // 0-terminated string
                p += 3;
                while (ba[p++] !== 0) {
                }
            } else { // other atomic types
                p += 3 + type2size(type);
            }
        }
        if (p >= block_end) return false // no CG tag
        if (String.fromCharCode(ba[p + 2]) !== 'B' || String.fromCharCode(ba[p + 3]) !== 'I') return false // not of type B,I

        // now we know the real CIGAR length and its offset in the binary array
        var cigar_len = readInt(ba, p + 4);
        var cigar_offset = p + 8; // 4 for "CGBI" and 4 for length
        if (cigar_offset + cigar_len * 4 > block_end) return false // out of bound

        // decode CIGAR
        var cigar = '';
        var lengthOnRef = 0;
        cigarArray.length = 0; // empty the old array
        p = cigar_offset;
        for (var k = 0; k < cigar_len; ++k, p += 4) {
            var cigop = readInt(ba, p);
            var opLen = (cigop >> 4);
            var opLtr = CIGAR_DECODER[cigop & 0xf];
            if (opLtr === 'M' || opLtr === 'EQ' || opLtr === 'X' || opLtr === 'D' || opLtr === 'N' || opLtr === '=')
                lengthOnRef += opLen;
            cigar = cigar + opLen + opLtr;
            cigarArray.push({len: opLen, ltr: opLtr});
        }

        // update alignment record. We are not updating bin, as apparently it is not used.
        al.cigar = cigar;
        al.lengthOnRef = lengthOnRef;
        return true
    },

    /**
     *
     * @param ba                 bytes to decode as an UInt8Array
     * @param offset             offset position of ba array to start decoding
     * @param alignmentContainer container to receive the decoded alignments
     * @param min                minimum genomic position
     * @param max                maximum genomic position
     * @param chrIdx             chromosome index
     * @param chrNames            array of chromosome names
     * @param filter             a BamFilter object
     *
     * @return true if we have moved beyond the right end of the genomic range.
     */
    decodeBamRecords: function (ba, offset, alignmentContainer, chrNames, chrIdx, min, max, filter) {

        while (offset < ba.length) {

            const blockSize = readInt(ba, offset);
            const blockEnd = offset + blockSize + 4;
            const alignment = new BamAlignment();
            const refID = readInt(ba, offset + 4);
            const pos = readInt(ba, offset + 8);

            if (blockEnd > ba.length) {
                return
            }
            if (refID < 0) {
                offset = blockEnd;
                continue   // unmapped read
            } else if (chrIdx !== undefined && (refID > chrIdx || (refID === chrIdx && pos > max))) {
                return true    // off right edge, we're done
            } else if (chrIdx !== undefined && (refID < chrIdx)) {
                offset = blockEnd;
                continue   // ref ID to left of start, not sure this is possible
            }

            const bin_mq_nl = readInt(ba, offset + 12);
            const mq = (bin_mq_nl & 0xff00) >> 8;
            const nl = bin_mq_nl & 0xff;

            const flag_nc = readInt(ba, offset + 16);
            const flag = (flag_nc & 0xffff0000) >> 16;
            const nc = flag_nc & 0xffff;

            const lseq = readInt(ba, offset + 20);
            const mateChrIdx = readInt(ba, offset + 24);
            const matePos = readInt(ba, offset + 28);
            const fragmentLength = readInt(ba, offset + 32);

            let readName = [];
            for (let j = 0; j < nl - 1; ++j) {
                readName.push(String.fromCharCode(ba[offset + 36 + j]));
            }
            readName = readName.join('');

            let lengthOnRef = 0;
            let cigar = '';
            let p = offset + 36 + nl;
            const cigarArray = [];
            // concatenate M,=,EQ,and X

            let lastCigRecord;
            for (let c = 0; c < nc; ++c) {
                var cigop = readInt(ba, p);
                var opLen = (cigop >> 4);
                var opLtr = CIGAR_DECODER[cigop & 0xf];
                if (opLtr === 'M' || opLtr === 'EQ' || opLtr === 'X' || opLtr === 'D' || opLtr === 'N' || opLtr === '=')
                    lengthOnRef += opLen;
                cigar = cigar + opLen + opLtr;
                p += 4;

                // if(mOperators.has(opLtr) && mOperators.has(lastCigRecord.ltr)) {
                //     lastCigRecord.len += opLen;
                //     lastCigRecord.ltr = 'M'
                // }
                // else {
                lastCigRecord = {len: opLen, ltr: opLtr};
                cigarArray.push(lastCigRecord);
                //}
            }

            alignment.chr = chrNames[refID];
            alignment.start = pos;
            alignment.flags = flag;
            alignment.strand = !(flag & READ_STRAND_FLAG$1);
            alignment.readName = readName;
            alignment.cigar = cigar;
            alignment.lengthOnRef = lengthOnRef;
            alignment.fragmentLength = fragmentLength;
            alignment.mq = mq;

            BamUtils.bam_tag2cigar(ba, blockEnd, p, lseq, alignment, cigarArray);

            alignment.end = alignment.start + alignment.lengthOnRef;

            if (alignment.end < min) {
                offset = blockEnd;
                continue
            }  // Record out-of-range "to the left", skip to next one


            let seq = [];
            const seqBytes = (lseq + 1) >> 1;
            for (let j = 0; j < seqBytes; ++j) {
                var sb = ba[p + j];
                seq.push(SEQ_DECODER[(sb & 0xf0) >> 4]);
                seq.push(SEQ_DECODER[(sb & 0x0f)]);
            }
            seq = seq.slice(0, lseq).join('');  // seq might have one extra character (if lseq is an odd number)
            p += seqBytes;


            const qualArray = [];
            for (let j = 0; j < lseq; ++j) {
                qualArray.push(ba[p + j]);
            }
            p += lseq;

            if (mateChrIdx >= 0) {
                alignment.mate = {
                    chr: chrNames[mateChrIdx],
                    position: matePos,
                    strand: !(flag & MATE_STRAND_FLAG$1)
                };
            }

            alignment.seq = seq;
            alignment.qual = qualArray;

            const tagBA = new Uint8Array(ba.buffer.slice(p, blockEnd));
            alignment.tagDict = decodeBamTags(tagBA);

            this.setPairOrientation(alignment);

            if ((undefined === filter || filter.pass(alignment))) {
                makeBlocks(alignment, cigarArray);
                alignmentContainer.push(alignment);
            }
            offset = blockEnd;
        }
    },

    decodeSamRecords: function (sam, alignmentContainer, chr, min, max, filter) {

        var lines, i, j, len, tokens, qualString, rnext, lengthOnRef,
            alignment, cigarArray, started;

        lines = splitLines$3(sam);
        len = lines.length;
        started = false;

        for (i = 0; i < len; i++) {

            tokens = lines[i].split('\t');

            alignment = new BamAlignment();

            alignment.chr = tokens[2];
            alignment.start = Number.parseInt(tokens[3]) - 1;
            alignment.flags = Number.parseInt(tokens[1]);
            alignment.readName = tokens[0];
            alignment.strand = !(alignment.flags & READ_STRAND_FLAG$1);
            alignment.mq = Number.parseInt(tokens[4]);
            alignment.cigar = tokens[5];
            alignment.fragmentLength = Number.parseInt(tokens[8]);
            alignment.seq = tokens[9];

            if (alignment.chr === '*' || !alignment.isMapped()) continue  // Unmapped

            if (alignment.chr !== chr) {
                if (started) break // Off the right edge, we're done
                else continue // Possibly to the left, skip but keep looping
            } else if (alignment.start > max) {
                break    // off right edge, we're done
            }

            lengthOnRef = 0;
            cigarArray = buildOperators(alignment.cigar);
            cigarArray.forEach(function (op) {
                var opLen = op.len;
                var opLtr = op.ltr;
                if (opLtr === 'M' || opLtr === 'EQ' || opLtr === 'X' || opLtr === 'D' || opLtr === 'N' || opLtr === '=')
                    lengthOnRef += opLen;
            });
            alignment.lengthOnRef = lengthOnRef;
            // TODO for lh3: parse the CG:B,I tag in SAM here

            if (alignment.start + lengthOnRef < min) {
                continue    // To the left, skip and continue
            }


            qualString = tokens[10];
            alignment.qual = [];
            for (j = 0; j < qualString.length; j++) {
                alignment.qual[j] = qualString.charCodeAt(j) - 33;
            }
            alignment.tagDict = tokens.length < 11 ? {} : decodeSamTags(tokens.slice(11));

            if (alignment.isMateMapped()) {
                rnext = tokens[6];
                alignment.mate = {
                    chr: (rnext === '=') ? alignment.chr : rnext,
                    position: Number.parseInt(tokens[7]),
                    strand: !(alignment.flags & MATE_STRAND_FLAG$1)
                };
            }

            this.setPairOrientation(alignment);

            if (undefined === filter || filter.pass(alignment)) {
                makeBlocks(alignment, cigarArray);
                alignmentContainer.push(alignment);
            }
        }
    },

    setReaderDefaults: function (reader, config) {

        reader.filter = typeof config.filter === 'function' ?
            {pass: config.filter} :
            new BamFilter(config.filter);

        if (config.readgroup) {
            reader.filter.readgroups = new Set([config.readgroup]);
        }

        reader.alleleFreqThreshold = config.alleleFreqThreshold === undefined ? DEFAULT_ALLELE_FREQ_THRESHOLD : config.alleleFreqThreshold;

        reader.samplingWindowSize = config.samplingWindowSize === undefined ? DEFAULT_SAMPLING_WINDOW_SIZE : config.samplingWindowSize;
        reader.samplingDepth = config.samplingDepth === undefined ? DEFAULT_SAMPLING_DEPTH : config.samplingDepth;

        if (reader.samplingDepth > MAXIMUM_SAMPLING_DEPTH) {
            reader.samplingDepth = MAXIMUM_SAMPLING_DEPTH;
        }
    },

    setPairOrientation: function (alignment) {

        if (alignment.isMapped() && alignment.mate && alignment.isMateMapped() && alignment.mate.chr === alignment.chr) {
            var s1 = alignment.strand ? 'F' : 'R';

            var mate = alignment.mate;
            var s2 = mate.strand ? 'F' : 'R';
            var o1 = ' ';
            var o2 = ' ';
            if (alignment.isFirstOfPair()) {
                o1 = '1';
                o2 = '2';
            } else if (alignment.isSecondOfPair()) {
                o1 = '2';
                o2 = '1';
            }

            var tmp = [];
            var isize = alignment.fragmentLength;
            var estReadLen = alignment.end - alignment.start;
            if (isize === 0) {
                //isize not recorded.  Need to estimate.  This calculation was validated against an Illumina
                // -> <- library bam.
                var estMateEnd = alignment.start < mate.position ?
                    mate.position + estReadLen : mate.position - estReadLen;
                isize = estMateEnd - alignment.start;
            }

            //if (isize > estReadLen) {
            if (isize > 0) {
                tmp[0] = s1;
                tmp[1] = o1;
                tmp[2] = s2;
                tmp[3] = o2;

            } else {
                tmp[2] = s1;
                tmp[3] = o1;
                tmp[0] = s2;
                tmp[1] = o2;
            }
            // }
            alignment.pairOrientation = tmp.join('');
        }
    }
};


/**
 * Split the alignment record into blocks as specified in the cigarArray.  Each aligned block contains
 * its portion of the read sequence and base quality strings.  A read sequence or base quality string
 * of "*" indicates the value is not recorded.  In all other cases the length of the block sequence (block.seq)
 * and quality string (block.qual) must == the block length.
 *
 * @param alignment
 * @param cigarArray
 * @returns array of blocks
 */
function makeBlocks(alignment, cigarArray) {

    const blocks = [];

    let insertions;
    let gaps;
    let seqOffset = 0;
    let pos = alignment.start;

    alignment.scStart = alignment.start;
    alignment.scLengthOnRef = alignment.lengthOnRef;

    for (let c of cigarArray) {

        let scPos;
        switch (c.ltr) {
            case 'H' :
                break // ignore hard clips
            case 'P' :
                break // ignore pads
            case 'S' :

                scPos = pos;
                alignment.scLengthOnRef += c.len;
                if (blocks.length === 0) {
                    alignment.scStart -= c.len;
                    scPos -= c.len;
                }
                blocks.push(new AlignmentBlock({
                    start: scPos,
                    seqOffset: seqOffset,
                    len: c.len,
                    type: 'S'
                }));
                seqOffset += c.len;
                break // soft clip read bases
            case 'N' :
            case 'D':
                if (gaps === undefined) {
                    gaps = [];
                }
                gaps.push({
                    start: pos,
                    len: c.len,
                    type: c.ltr
                });
                pos += c.len;
                break
            case 'I' :

                if (insertions === undefined) {
                    insertions = [];
                }
                insertions.push(new AlignmentBlock({
                    start: pos,
                    len: c.len,
                    seqOffset: seqOffset,
                    type: 'I'
                }));
                seqOffset += c.len;
                break
            case 'M' :
            case 'EQ' :
            case '=' :
            case 'X' :
                blocks.push(new AlignmentBlock({
                    start: pos,
                    seqOffset: seqOffset,
                    len: c.len,
                    type: 'M'
                }));
                seqOffset += c.len;
                pos += c.len;

                break
        }
    }

    alignment.blocks = blocks;
    alignment.insertions = insertions;
    alignment.gaps = gaps;

}

function readInt(ba, offset) {
    return (ba[offset + 3] << 24) | (ba[offset + 2] << 16) | (ba[offset + 1] << 8) | (ba[offset])
}

/**
 * Build a list of cigar operators from a cigarString.  Removes padding operators and concatenates consecutive
 * operators of the same type
 *
 * @param cigarString
 * @return
 */
function buildOperators(cigarString) {

    var operators, buffer, i, len, prevOp, next, op, nBases;

    operators = [];
    buffer = [];

    // Create list of cigar operators
    prevOp = null;
    len = cigarString.length;
    for (i = 0; i < len; i++) {
        next = cigarString.charAt(i);
        if (isDigit(next)) {
            buffer.push(next);
        } else {
            op = next;
            nBases = Number.parseInt(buffer.join(''));
            buffer = [];

            if (prevOp !== null && prevOp.ltr === op) {
                prevOp.len += nBases;
            } else {
                prevOp = {len: nBases, ltr: op};
                operators.push(prevOp);
            }
        }
    }
    return operators

}

function isDigit(a) {
    var charCode = a.charCodeAt(0);
    return (charCode >= 48 && charCode <= 57) // 0-9
}

function decodeSamTags(tags) {

    var tagDict = {};
    tags.forEach(function (tag) {
        var tokens = tag.split(':');
        tagDict[tokens[0]] = tokens[2];
    });

    return tagDict
}

/**
 * Decode bam tags from the supplied UInt8Array
 *
 * A [!-~] Printable character
 * i [-+]?[0-9]+ Signed integer16
 * f [-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)? Single-precision floating number
 * Z [ !-~]* Printable string, including space
 * H ([0-9A-F][0-9A-F])* Byte array in the Hex format17
 * B [cCsSiIf](,[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)* Integer or numeric array
 *
 * @param ba A byte array (UInt8Array)
 * @returns {{}}  Tag values
 */
function decodeBamTags(ba) {

    let p = 0;
    const len = ba.length;
    const tags = {};
    const dataView = new DataView(ba.buffer);

    while (p < len) {
        const tag = String.fromCharCode(ba[p]) + String.fromCharCode(ba[p + 1]);
        p += 2;

        const type = String.fromCharCode(ba[p++]);
        let value;
        if (type === 'A') {
            value = String.fromCharCode(ba[p]);
            p++;
        } else if (type === 'i' || type === 'I') {
            value = dataView.getInt32(p, true);
            p += 4;
        } else if (type === 'c') {
            value = dataView.getInt8(p, true);
            p++;
        } else if (type === 'C') {
            value = dataView.getUint8(p, true);
            p++;
        } else if (type === 's' || type === 'S') {
            value = dataView.getInt16(p, true);
            p += 2;
        } else if (type === 'f') {
            value = dataView.getFloat32(p, true);
            p += 4;
        } else if (type === 'Z') {
            value = '';
            for (; ;) {
                var cc = ba[p++];
                if (cc === 0) {
                    break
                } else {
                    value += String.fromCharCode(cc);
                }
            }
        } else if (type === 'B') {
            //‘cCsSiIf’, corresponding to int8 , uint8 t, int16 t, uint16 t, int32 t, uint32 t and float
            const elementType = String.fromCharCode(ba[p++]);
            let elementSize = ELEMENT_SIZE[elementType];
            if (elementSize === undefined) {
                tags[tag] = `Error: unknown element type '${elementType}'`;
                break
            }
            const numElements = dataView.getInt32(p, true);
            p += 4;
            const pEnd = p + numElements * elementSize;
            value = [];

            while (p < pEnd) {
                switch (elementType) {
                    case 'c':
                        value.push(dataView.getInt8(p, true));
                        break
                    case 'C':
                        value.push(dataView.getUint8(p, true));
                        break
                    case 's':
                        value.push(dataView.getInt16(p, true));
                        break
                    case 'S':
                        value.push(dataView.getUint16(p, true));
                        break
                    case 'i':
                        value.push(dataView.getInt32(p, true));
                        break
                    case 'I':
                        value.push(dataView.getUint32(p, true));
                        break
                    case 'f':
                        value.push(dataView.getFloat32(p, true));
                }
                p += elementSize;
            }
        } else {
            //'Unknown type ' + type;
            value = 'Error unknown type: ' + type;
            tags[tag] = value;
            break
        }
        tags[tag] = value;
    }
    return tags
}

/**
 * Class for reading a bam file
 *
 * @param config
 * @constructor
 */
class BamReaderNonIndexed {

    chrAliasTable = new Map()

    constructor(config, genome) {
        this.config = config;
        this.genome = genome;
        this.bamPath = config.url;
        this.isDataUri = isDataURL(config.url);
        BamUtils.setReaderDefaults(this, config);
    }

    /**
     *
     * @param chr
     * @param bpStart
     * @param bpEnd
     * @returns {Promise<AlignmentContainer>}
     */
    async readAlignments(chr, bpStart, bpEnd) {

        if (!this.alignmentCache) {
            // For a non-indexed BAM file all alignments are read at once and cached.
            let unc;
            if (this.isDataUri) {
                const data = decodeDataURI$2(this.bamPath);
                unc = unbgzf$1(data.buffer);
            } else {
                const arrayBuffer = await igvxhr$1.loadArrayBuffer(this.bamPath, buildOptions(this.config));
                unc = unbgzf$1(arrayBuffer);
            }
            const alignments = this.#parseAlignments(unc);
            this.alignmentCache = new FeatureCache$1(alignments);
        }

        const queryChr = this.chromAliasManager ? await this.chromAliasManager.getAliasName(chr) : chr;
        const qAlignments = this.alignmentCache.queryFeatures(queryChr, bpStart, bpEnd);
        const alignmentContainer = new AlignmentContainer(chr, bpStart, bpEnd, this.config);
        for (let a of qAlignments) {
            alignmentContainer.push(a);
        }
        alignmentContainer.finish();
        return alignmentContainer
    }

    #parseAlignments(data) {
        const alignments = [];
        this.header = BamUtils.decodeBamHeader(data);
        this.chromAliasManager = this.genome ? new ChromAliasManager(this.header.chrNames, this.genome) : null;
        BamUtils.decodeBamRecords(data, this.header.size, alignments, this.header.chrNames, undefined, 0, Number.MAX_SAFE_INTEGER, this.filter);
        return alignments
    }

    async #getQueryChr(chr) {

        const ownNames = new Set(this.header.chrNames);
        if (ownNames.has(chr)) {
            return chr
        }

        if (this.chrAliasTable.has(chr)) {
            return this.chrAliasTable.get(chr)
        }

        // Try alias

        if (this.genome) {
            const aliasRecord = await this.genome.getAliasRecord(chr);
            let alias;
            if (aliasRecord) {
                const aliases = Object.keys(aliasRecord)
                    .filter(k => k !== "start" && k !== "end")
                    .map(k => aliasRecord[k])
                    .filter(a => ownNames.has(a));
                if (aliases.length > 0) {
                    alias = aliases[0];
                }
            }
            this.chrAliasTable.set(chr, alias);  // alias may be undefined => no alias exists. Setting prevents repeated attempts
            return alias
        }

        return chr
    }

}

function decodeDataURI$2(dataURI) {

    const split = dataURI.split(',');
    const info = split[0].split(':')[1];
    let dataString = split[1];

    if (info.indexOf('base64') >= 0) {
        dataString = atob(dataString);
    } else {
        dataString = decodeURI(dataString);
    }

    const bytes = new Uint8Array(dataString.length);
    for (var i = 0; i < dataString.length; i++) {
        bytes[i] = dataString.charCodeAt(i);
    }
    return bytes
}

/**
 * Class for reading a bam file
 *
 * @param config
 * @constructor
 */
class BamReader {

    chrAliasTable = new Map()

    constructor(config, genome) {
        this.config = config;
        this.genome = genome;
        this.bamPath = config.url;
        this.baiPath = config.indexURL;
        BamUtils.setReaderDefaults(this, config);

        this._blockLoader = new BGZBlockLoader(config);
    }

    async readAlignments(chr, bpStart, bpEnd) {

        const chrId = await this.#getRefId(chr);
        const alignmentContainer = new AlignmentContainer(chr, bpStart, bpEnd, this.config);

        if (chrId === undefined) {
            return alignmentContainer

        } else {

            const bamIndex = await this.getIndex();
            const chunks = bamIndex.chunksForRange(chrId, bpStart, bpEnd);

            if (!chunks || chunks.length === 0) {
                return alignmentContainer
            }

            for (let c of chunks) {
                const ba = await this._blockLoader.getData(c.minv, c.maxv);
                const done = BamUtils.decodeBamRecords(ba, c.minv.offset, alignmentContainer, this.header.chrNames, chrId, bpStart, bpEnd, this.filter);
                if (done) {
                    break
                }
            }
            alignmentContainer.finish();
            return alignmentContainer
        }
    }

    async #getRefId(chr) {

        await this.getHeader();

        if (this.chrAliasTable.has(chr)) {
            chr = this.chrAliasTable.get(chr);
            if (chr === undefined) {
                return undefined
            }
        }

        let refId = this.header.chrToIndex[chr];

        // Try alias
        if (refId === undefined) {
            const aliasRecord = await this.genome.getAliasRecord(chr);
            let alias;
            if (aliasRecord) {
                const aliases = Object.keys(aliasRecord)
                    .filter(k => k !== "start" && k !== "end")
                    .map(k => aliasRecord[k])
                    .filter(a => undefined !== this.header.chrToIndex[a]);
                if (aliases.length > 0) {
                    alias = aliases[0];
                    refId = this.header.chrToIndex[aliases[0]];
                }
            }
            this.chrAliasTable.set(chr, alias);  // alias may be undefined => no alias exists. Setting prevents repeated attempts
        }
        return refId
    }

    /**
     *
     * @returns {Promise<{magicNumer: number, size: number, chrNames: Array, chrToIndex: ({}|*), chrAliasTable: ({}|*)}>}
     */
    async getHeader() {
        if (!this.header) {
            const genome = this.genome;
            const index = await this.getIndex();
            let len;
            if (index.firstBlockPosition) {
                const bsizeOptions = buildOptions(this.config, {range: {start: index.firstBlockPosition, size: 26}});
                const abuffer = await igvxhr$1.loadArrayBuffer(this.bamPath, bsizeOptions);
                const bsize = bgzBlockSize$1(abuffer);
                len = index.firstBlockPosition + bsize;   // Insure we get the complete compressed block containing the header
            } else {
                len = 64000;
            }

            const options = buildOptions(this.config, {range: {start: 0, size: len}});
            this.header = await BamUtils.readHeader(this.bamPath, options, genome);
        }
        return this.header
    }

    async getIndex() {
        if (!this.index) {
            this.index = await loadIndex(this.baiPath, this.config);
        }
        return this.index
    }

    async getChrIndex() {
        if (this.chrToIndex) {
            return this.chrToIndex
        } else {
            const header = await this.getHeader();
            this.chrToIndex = header.chrToIndex;
            this.indexToChr = header.chrNames;
            this.chrNames = new Set(header.chrNames);
            return this.chrToIndex

        }
    }
}

class ShardedBamReader {

    constructor(config, genome) {

        this.config = config;
        this.genome = genome;

        const bamReaders = {};

        config.sources.sequences.forEach(function (chr) {
            const queryChr = genome ? genome.getChromosomeName(chr) : chr;
            bamReaders[queryChr] = getReader(config, genome, chr);
        });

        this.bamReaders = bamReaders;

        BamUtils.setReaderDefaults(this, config);
    }

    async readAlignments(chr, start, end) {

        if (!this.bamReaders.hasOwnProperty(chr)) {
            return new AlignmentContainer(chr, start, end, this.config)
        } else {

            let reader = this.bamReaders[chr];
            const a = await reader.readAlignments(chr, start, end);
            return a
        }
    }
}

function getReader(config, genome, chr) {
    const tmp = {
        url: config.sources.url.replace("$CHR", chr)
    };
    if (config.sources.indexURL) {
        tmp.indexURL = config.sources.indexURL.replace("$CHR", chr);
    }
    const bamConfig = Object.assign(config, tmp);

    // TODO -- support non-indexed, htsget, etc
    return new BamReader(bamConfig, genome)
}

/**
 * Class for reading bam records from an igv.js-flask server backed by pysam.  Deprecated.
 *
 * @param config
 * @constructor
 */
const BamWebserviceReader = function (config, genome) {

    this.config = config;
    this.genome = genome;
    BamUtils.setReaderDefaults(this, config);

};

// Example http://localhost:5000/alignments/?reference=/Users/jrobinso/hg19mini.fa&file=/Users/jrobinso/cram_with_crai_index.cram&region=1:100-2000

BamWebserviceReader.prototype.readAlignments = function (chr, bpStart, bpEnd) {

    var self = this;

    return getHeader.call(self)

        .then(function (header) {

            var queryChr, url;

            queryChr = header.chrAliasTable.hasOwnProperty(chr) ? header.chrAliasTable[chr] : chr;

            url = self.config.url +
                "?reference=" + self.config.referenceFile +
                "&file=" + self.config.alignmentFile + "" +
                "&region=" + queryChr + ":" + bpStart + "-" + bpEnd;


            return igvxhr$1.loadString(url, buildOptions(self.config))

                .then(function (sam) {

                    var alignmentContainer;

                    header.chrToIndex[queryChr];

                    alignmentContainer = new AlignmentContainer(chr, bpStart, bpEnd, self.config);

                    BamUtils.decodeSamRecords(sam, alignmentContainer, queryChr, bpStart, bpEnd, self.filter);

                    return alignmentContainer

                })

        })
};


// Example  http://localhost:5000/alignments/?reference=/Users/jrobinso/hg19mini.fa&file=/Users/jrobinso/cram_with_crai_index.cram&options=-b%20-H
function getHeader() {

    const self = this;
    const genome = this.genome;

    if (this.header) {

        return Promise.resolve(this.header)

    } else {

        const url = this.config.url + "?file=" + this.config.alignmentFile + "&options=-b,-H";
        const options = buildOptions(this.config);

        return BamUtils.readHeader(url, options, genome)

            .then(function (header) {

                self.header = header;
                return header

            })
    }

}

class HtsgetBamReader extends HtsgetReader {

    chrNames = new Set()

    constructor(config, genome) {
        super(config, genome);
        BamUtils.setReaderDefaults(this, config);
    }


    async readAlignments(chr, start, end) {

        if('all' === chr) {
            return []    // This should never happen, but just in case
        }

        if (!this.header) {
            const compressedData = await this.readHeaderData();
            const ba = unbgzf$1(compressedData.buffer);
            this.header = BamUtils.decodeBamHeader(ba, this.genome);
            for(let name of this.header.chrNames) {
                this.chrNames.add(name);
            }
            this.chromAliasManager = this.genome ? new ChromAliasManager(this.header.chrNames, this.genome) : null;
        }

        // If the chromosome is not in the BAM header, check for an alias.
        let queryChr = chr;
        if (this.chrNames.size > 0 && !this.chrNames.has(chr) && this.chromAliasManager) {
            queryChr = await this.chromAliasManager.getAliasName(chr);
        }

        if (!this.chrNames.has(queryChr)) {
            console.warn("Chromosome " + chr + " not found in BAM header");
            return new AlignmentContainer(chr, start, end, this.config)  // Empty container
        }

        const compressedData = await this.readData(queryChr, start, end);

        // BAM decoding
        const ba = unbgzf$1(compressedData.buffer);
        this.header = BamUtils.decodeBamHeader(ba, this.genome);

        const chrIdx = this.header.chrToIndex[chr];
        const alignmentContainer = new AlignmentContainer(chr, start, end, this.config);
        BamUtils.decodeBamRecords(ba, this.header.size, alignmentContainer, this.header.chrNames, chrIdx, start, end, this.filter);
        alignmentContainer.finish();

        return alignmentContainer

    }

}

/*! For license information please see cram-bundle.js.LICENSE.txt */
var gmodCRAM = (()=>{var e={4923:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:!0});const n=r(9747),i=r(5931),a=r(8876);function A(e,t){const[r,n,i,a,A,s]=t,o=r;e[o]||(e[o]=[]),e[o].push({start:n,span:i,containerStart:a,sliceStart:A,sliceBytes:s});}t.default=class{constructor(e){this.filehandle=(0, i.open)(e.url,e.path,e.filehandle);}async parseIndex(){const e={},t=31===(r=await this.filehandle.readFile())[0]&&139===r[1]?(0, a.unzip)(r):r;var r;const i=new DataView(t.buffer);if(t.length>4&&21578050===i.getUint32(0,!0))throw new n.CramMalformedError("invalid .crai index file. note: file appears to be a .bai index. this is technically legal but please open a github issue if you need support");let s=[],o="";for(const r of t)if(r>=48&&r<=57||!o&&45===r)o+=String.fromCharCode(r);else if(9===r)s.push(Number.parseInt(o,10)),o="";else if(10===r)s.push(Number.parseInt(o,10)),o="",A(e,s),s=[];else if(13!==r&&32!==r)throw new n.CramMalformedError("invalid .crai index file");return o&&s.push(Number.parseInt(o,10)),6===s.length&&A(e,s),Object.entries(e).forEach((([t,r])=>{const n=r;e[t]=n.sort(((e,t)=>e.start-t.start||e.span-t.span));})),e}getIndex(){return this.parseIndexP||(this.parseIndexP=this.parseIndex().catch((e=>{throw this.parseIndexP=void 0,e}))),this.parseIndexP}async hasDataForReferenceSequence(e){return !!(await this.getIndex())[e]}async getEntriesForRange(e,t,r){const n=(await this.getIndex())[e];if(!n)return [];const i=e=>{const n=e.start,i=e.start+e.span;return n>r?-1:i<=t?1:0},a=[];for(const e of n)0===i(e)&&a.push(e);return a}};},9508:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.default=class{constructor(e,t){this.parameters=e,this.dataType=t;}};},368:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const i=n(r(9508)),a=r(612),A=r(9747);class s extends i.default{constructor(e,t){if(super(e,t),"int"!==this.dataType)throw new A.CramUnimplementedError(`${this.dataType} decoding not yet implemented by BETA codec`)}decode(e,t,r,n){return (0, a.getBits)(t.content,n.coreBlock,this.parameters.length)-this.parameters.offset}}t.default=s;},8635:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const i=n(r(9508)),a=r(400);class A extends i.default{constructor(e,t,r){super(e,t),this.instantiateCodec=r;}decode(e,t,r,n){const i=this._getLengthCodec().decode(e,t,r,n)||0,a=this._getDataCodec(),A=new Uint8Array(i);for(let s=0;s<i;s+=1)A[s]=a.decode(e,t,r,n)||0;return A}_getLengthCodec(){const e=this.parameters.lengthsEncoding;return this.instantiateCodec(e,"int")}_getDataCodec(){const e=this.parameters.valuesEncoding;return this.instantiateCodec(e,"byte")}}t.default=A,"_getLengthCodec _getDataCodec".split(" ").forEach((e=>{(0, a.tinyMemoize)(A,e);}));},4839:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const i=n(r(9508)),a=r(9747),A=r(612);class s extends i.default{decode(e,t,r,n){const{blockContentId:i}=this.parameters,A=r[i];if(!A)throw new a.CramMalformedError(`no block found with content ID ${i}`);const s=n.externalBlocks.getCursor(i);return this._decodeByteArray(A,s)}_decodeByteArray(e,t){const r=e.content,{stopByte:n}=this.parameters,i=t.bytePosition;let a=t.bytePosition;for(;r[a]!==n&&a<r.length;){if(a===r.length)throw new A.CramBufferOverrunError("byteArrayStop reading beyond length of data buffer?");a+=1;}return t.bytePosition=a+1,r.subarray(i,a)}}t.default=s;},8153:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const i=n(r(9508)),a=r(9747),A=r(400),s=r(612);class o extends i.default{constructor(e,t){if(super(e,t),"int"===this.dataType)this._decodeData=this._decodeInt;else {if("byte"!==this.dataType)throw new a.CramUnimplementedError(`${this.dataType} decoding not yet implemented by EXTERNAL codec`);this._decodeData=this._decodeByte;}}decode(e,t,r,n){const{blockContentId:i}=this.parameters,a=r[i],A=n.externalBlocks.getCursor(i);return a?this._decodeData(a,A):void 0}_decodeInt(e,t){const[r,n]=(0, A.parseItf8)(e.content,t.bytePosition);return t.bytePosition=t.bytePosition+n,r}_decodeByte(e,t){if(t.bytePosition>=e.content.length)throw new s.CramBufferOverrunError("attempted to read beyond end of block. this file seems truncated.");return e.content[t.bytePosition++]}}t.default=o;},8185:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const i=n(r(9508)),a=r(612),A=r(9747);class s extends i.default{constructor(e,t){if(super(e,t),"int"!==this.dataType)throw new A.CramUnimplementedError(`${this.dataType} decoding not yet implemented by GAMMA codec`)}decode(e,t,r,n){let i=1;for(;0===(0, a.getBits)(t.content,n.coreBlock,1);)i+=1;return ((0, a.getBits)(t.content,n.coreBlock,i-1)|1<<i-1)-this.parameters.offset}}t.default=s;},612:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.CramBufferOverrunError=void 0,t.getBits=function(e,t,n){let i=0;if(t.bytePosition+(7-t.bitPosition+n)/8>e.length)throw new r("read error during decoding. the file seems to be truncated.");for(let r=n;r;r--)i<<=1,i|=e[t.bytePosition]>>t.bitPosition&1,t.bitPosition-=1,t.bitPosition<0&&(t.bytePosition+=1),t.bitPosition&=7;return i};class r extends Error{}t.CramBufferOverrunError=r;},4341:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const i=n(r(9508)),a=r(612),A=r(9747);class s extends i.default{constructor(e,t){if(super(e,t),this.codes={},this.codeBook={},this.sortedCodes=[],this.sortedValuesByBitCode=[],this.sortedBitCodes=[],this.sortedBitLengthsByBitCode=[],this.bitCodeToValue=[],!["byte","int"].includes(this.dataType))throw new TypeError(`${this.dataType} decoding not yet implemented by HUFFMAN_INT codec`);this.buildCodeBook(),this.buildCodes(),this.buildCaches(),0===this.sortedCodes[0].bitLength&&(this._decode=this._decodeZeroLengthCode);}buildCodeBook(){let e=new Array(this.parameters.numCodes);for(let t=0;t<this.parameters.numCodes;t++)e[t]={symbol:this.parameters.symbols[t],bitLength:this.parameters.bitLengths[t]};e=e.sort(((e,t)=>e.bitLength-t.bitLength||e.symbol-t.symbol)),this.codeBook={},e.forEach((e=>{this.codeBook[e.bitLength]||(this.codeBook[e.bitLength]=[]),this.codeBook[e.bitLength].push(e.symbol);}));}buildCodes(){this.codes={};let e=0,t=-1;Object.entries(this.codeBook).forEach((([r,n])=>{const i=Number.parseInt(r,10);n.forEach((r=>{const n={bitLength:i,value:r,bitCode:0};t+=1;const a=i-e;if(t<<=a,n.bitCode=t,e+=a,function(e){let t=e-(e>>1)&1431655765;return t=(858993459&t)+(t>>2&858993459),16843009*(t+(t>>4)&252645135)>>24}(t)>i)throw new A.CramMalformedError("Symbol out of range");this.codes[r]=n;}));}));}buildCaches(){this.sortedCodes=Object.values(this.codes).sort(((e,t)=>e.bitLength-t.bitLength||e.bitCode-t.bitCode)),this.sortedValuesByBitCode=this.sortedCodes.map((e=>e.value)),this.sortedBitCodes=this.sortedCodes.map((e=>e.bitCode)),this.sortedBitLengthsByBitCode=this.sortedCodes.map((e=>e.bitLength));const e=Math.max(...this.sortedBitCodes);this.bitCodeToValue=new Array(e+1).fill(-1);for(let e=0;e<this.sortedBitCodes.length;e+=1)this.bitCodeToValue[this.sortedCodes[e].bitCode]=e;}decode(e,t,r,n){return this._decode(e,t,n.coreBlock)}_decodeZeroLengthCode(){return this.sortedCodes[0].value}_decode(e,t,r){const n=t.content;let i=0,s=0;for(let e=0;e<this.sortedCodes.length;e+=1){const t=this.sortedCodes[e].bitLength;s<<=t-i,s|=(0, a.getBits)(n,r,t-i),i=t;{const r=this.bitCodeToValue[s];if(r>-1&&this.sortedBitLengthsByBitCode[r]===t)return this.sortedValuesByBitCode[r];for(let r=e;this.sortedCodes[r+1].bitLength===t&&r<this.sortedCodes.length;r+=1)e+=1;}}throw new A.CramMalformedError("Huffman symbol not found.")}}t.default=s;},6524:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.instantiateCodec=function e(t,r){const n=(i="ignore"===r?0:t.codecId,f[i]);var i;if(!n)throw new l.CramUnimplementedError(`no codec implemented for codec ID ${t.codecId}`);return new n(t.parameters,r,e)};const i=n(r(368)),a=n(r(8635)),A=n(r(4839)),s=n(r(8153)),o=n(r(8185)),c=n(r(4341)),d=n(r(99)),l=r(9747),f={1:s.default,3:c.default,4:a.default,5:A.default,6:i.default,7:d.default,9:o.default};},99:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const i=n(r(9508)),a=r(612),A=r(9747);class s extends i.default{constructor(e,t){if(super(e,t),"int"!==this.dataType)throw new A.CramUnimplementedError(`${this.dataType} decoding not yet implemented by SUBEXP codec`)}decode(e,t,r,n){let i,A,s=0;for(;(0, a.getBits)(t.content,n.coreBlock,1);)s+=1;return 0===s?(i=this.parameters.K,A=(0, a.getBits)(t.content,n.coreBlock,i)):(i=s+this.parameters.K-1,A=1<<i|(0, a.getBits)(t.content,n.coreBlock,i)),A-this.parameters.offset}}t.default=s;},7295:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.default={CRAM_FLAG_PRESERVE_QUAL_SCORES:1,CRAM_FLAG_DETACHED:2,CRAM_FLAG_MATE_DOWNSTREAM:4,CRAM_FLAG_NO_SEQ:8,CRAM_FLAG_MASK:15,CRAM_M_REVERSE:1,CRAM_M_UNMAP:2,BAM_FPAIRED:1,BAM_FPROPER_PAIR:2,BAM_FUNMAP:4,BAM_FMUNMAP:8,BAM_FREVERSE:16,BAM_FMREVERSE:32,BAM_FREAD1:64,BAM_FREAD2:128,BAM_FSECONDARY:256,BAM_FQCFAIL:512,BAM_FDUP:1024,BAM_FSUPPLEMENTARY:2048,BAM_CMATCH:0,BAM_CINS:1,BAM_CDEL:2,BAM_CREF_SKIP:3,BAM_CSOFT_CLIP:4,BAM_CHARD_CLIP:5,BAM_CPAD:6,BAM_CEQUAL:7,BAM_CDIFF:8,BAM_CBACK:9,BAM_CIGAR_STR:"MIDNSHP:XB",BAM_CIGAR_SHIFT:4,BAM_CIGAR_MASK:15,BAM_CIGAR_TYPE:246183};},7219:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:!0});const n=r(9747),i=r(6524),a={BF:"int",CF:"int",RI:"int",RL:"int",AP:"int",RG:"int",MF:"int",NS:"int",NP:"int",TS:"int",NF:"int",TC:"byte",TN:"int",FN:"int",FC:"byte",FP:"int",BS:"byte",IN:"byteArray",SC:"byteArray",DL:"int",BA:"byte",BB:"byteArray",RS:"int",PD:"int",HC:"int",MQ:"int",RN:"byteArray",QS:"byte",QQ:"byteArray",TL:"int"};t.default=class{constructor(e){this.dataSeriesCodecCache={},this.tagCodecCache={},this.tagEncoding={},this.readNamesIncluded=e.preservation.RN,this.APdelta=e.preservation.AP,this.referenceRequired=!!e.preservation.RR,this.tagIdsDictionary=e.preservation.TD,this.substitutionMatrix=function(e){const t=new Array(5);for(let e=0;e<5;e+=1)t[e]=new Array(4);return t[0][e[0]>>6&3]="C",t[0][e[0]>>4&3]="G",t[0][e[0]>>2&3]="T",t[0][3&e[0]]="N",t[1][e[1]>>6&3]="A",t[1][e[1]>>4&3]="G",t[1][e[1]>>2&3]="T",t[1][3&e[1]]="N",t[2][e[2]>>6&3]="A",t[2][e[2]>>4&3]="C",t[2][e[2]>>2&3]="T",t[2][3&e[2]]="N",t[3][e[3]>>6&3]="A",t[3][e[3]>>4&3]="C",t[3][e[3]>>2&3]="G",t[3][3&e[3]]="N",t[4][e[4]>>6&3]="A",t[4][e[4]>>4&3]="C",t[4][e[4]>>2&3]="G",t[4][3&e[4]]="T",t}(e.preservation.SM),this.dataSeriesEncoding=e.dataSeriesEncoding,this.tagEncoding=e.tagEncoding;}getCodecForTag(e){const t=this.tagCodecCache[e];if(t)return t;{const t=this.tagEncoding[e];if(!t)throw new Error("Error, no tag encoding");const r=(0, i.instantiateCodec)(t,"byteArray");return this.tagCodecCache[e]=r,r}}getTagNames(e){return this.tagIdsDictionary[e]}getCodecForDataSeries(e){let t=this.dataSeriesCodecCache[e];if(void 0===t){const r=this.dataSeriesEncoding[e];if(r){const A=a[e];if(!A)throw new n.CramMalformedError(`data series name ${e} not defined in file compression header`);t=(0, i.instantiateCodec)(r,A),this.dataSeriesCodecCache[e]=t;}}return t}toJSON(){const e={};return Object.keys(this).forEach((t=>{t.endsWith("Cache")||(e[t]=this[t]);})),e}};},78:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const i=r(9747),a=n(r(6771)),A=r(400),s=n(r(7219)),o=r(3165);class c{constructor(e,t){this.file=e,this.filePosition=t;}getHeader(){return this._readContainerHeader(this.filePosition)}async getCompressionHeaderBlock(){if(!(await this.getHeader()).numRecords)return null;const{majorVersion:e}=await this.file.getDefinition(),t=(0, o.getSectionParsers)(e),r=await this.getFirstBlock();if("COMPRESSION_HEADER"!==r.contentType)throw new i.CramMalformedError(`invalid content type ${r.contentType} in compression header block`);const n=(0, A.parseItem)(r.content,t.cramCompressionHeader.parser,0,r.contentPosition);return {...r,parsedContent:n}}async getFirstBlock(){const e=await this.getHeader();return this.file.readBlock(e._endPosition)}async getCompressionScheme(){const e=await this.getCompressionHeaderBlock();if(e)return new s.default(e.parsedContent)}getSlice(e,t){return new a.default(this,e,t)}async _readContainerHeader(e){const{majorVersion:t}=await this.file.getDefinition(),r=(0, o.getSectionParsers)(t),{cramContainerHeader1:n,cramContainerHeader2:i}=r,a=await this.file.read(n.maxLength,e),s=(0, A.parseItem)(a,n.parser),c=(0, A.itf8Size)(s.numLandmarks),d=await this.file.read(i.maxLength(s.numLandmarks),e+s._size-c),l=(0, A.parseItem)(d,i.parser);return this.file.validateChecksums&&void 0!==l.crc32&&await this.file.checkCrc32(e,s._size+l._size-c-4,l.crc32,`container header beginning at position ${e}`),{...s,...l,_size:s._size+l._size-c,_endPosition:s._size+l._size-c+e}}}t.default=c,"getHeader getCompressionHeaderBlock getCompressionScheme".split(" ").forEach((e=>{(0, A.tinyMemoize)(c,e);}));},6794:function(e,t,r){var n,i=this&&this.__createBinding||(Object.create?function(e,t,r,n){void 0===n&&(n=r);var i=Object.getOwnPropertyDescriptor(t,r);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,n,i);}:function(e,t,r,n){void 0===n&&(n=r),e[n]=t[r];}),a=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t});}:function(e,t){e.default=t;}),A=this&&this.__importStar||(n=function(e){return n=Object.getOwnPropertyNames||function(e){var t=[];for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[t.length]=r);return t},n(e)},function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r=n(e),A=0;A<r.length;A++)"default"!==r[A]&&i(t,e,r[A]);return a(t,e),t}),s=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const o=s(r(86)),c=s(r(7392)),d=r(1307),l=r(9747),f=A(r(9151)),g=r(5931),h=s(r(9989)),u=r(5417),C=r(9995),I=r(8876),_=s(r(78)),m=r(3165),B=r(400);class p{constructor(e){if(this.file=(0, g.open)(e.url,e.path,e.filehandle),this.validateChecksums=!0,this.fetchReferenceSequenceCallback=e.seqFetch,this.options={checkSequenceMD5:e.checkSequenceMD5,cacheSize:e.cacheSize??2e4},this.featureCache=new c.default({maxSize:this.options.cacheSize}),function(){const e=new Uint32Array([287454020]),t=new Uint8Array(e.buffer);return 68===t[0]?0:17===t[0]?1:2}()>0)throw new Error("Detected big-endian machine, may be unable to run")}read(e,t){return this.file.read(e,t)}async getDefinition(){const{maxLength:e,parser:t}=(0, m.cramFileDefinition)(),r=t(await this.file.read(e,0)).value;if(2!==r.majorVersion&&3!==r.majorVersion)throw new l.CramUnimplementedError(`CRAM version ${r.majorVersion} not supported`);return r}async getSamHeader(){const e=await this.getContainerById(0);if(!e)throw new l.CramMalformedError("file contains no containers");const t=(await e.getFirstBlock()).content,r=new DataView(t.buffer).getInt32(0,!0),n=new TextDecoder("utf8").decode(t.subarray(4,4+r));return this.header=n,(0, u.parseHeaderText)(n)}async getHeaderText(){return await this.getSamHeader(),this.header}async getContainerById(e){const{majorVersion:t}=await this.getDefinition();let r,n=(0, m.getSectionParsers)(t).cramFileDefinition.maxLength;for(let t=0;t<=e;t++){r=this.getContainerAtPosition(n);const e=await r.getHeader();if(0===t){n=e._endPosition;for(let t=0;t<e.numBlocks;t++)n=(await this.readBlock(n))._endPosition;}else n+=e._size+e.length;}return r}async checkCrc32(e,t,r,n){const i=await this.file.read(t,e),a=(0, o.default)(i)>>>0;if(a!==r)throw new l.CramMalformedError(`crc mismatch in ${n}: recorded CRC32 = ${r}, but calculated CRC32 = ${a}`)}async containerCount(){const{majorVersion:e}=await this.getDefinition();let t=0,r=(0, m.getSectionParsers)(e).cramFileDefinition.maxLength;try{for(;;){const e=await this.getContainerAtPosition(r).getHeader();if(0===t){r=e._endPosition;for(let t=0;t<e.numBlocks;t++)r=(await this.readBlock(r))._endPosition;}else r+=e._size+e.length;t+=1;}}catch(e){t--;}return t}getContainerAtPosition(e){return new _.default(this,e)}async readBlockHeader(e){const{majorVersion:t}=await this.getDefinition(),r=(0, m.getSectionParsers)(t),{cramBlockHeader:n}=r,i=await this.file.read(n.maxLength,e);return (0, B.parseItem)(i,n.parser,0,e)}async _parseSection(e,t,r=e.maxLength,n){const i=n??await this.file.read(r,t),a=(0, B.parseItem)(i,e.parser,0,t);if(a._size!==r)throw new l.CramMalformedError(`section read error: requested size ${r} does not equal parsed size ${a._size}`);return a}async _uncompressPre(e,t,r){if("gzip"===e){const e=(0, I.unzip)(t);return e[0],e}if("bzip2"===e)return (0, C.decode)(t);if("lzma"===e){const e=new Response(new d.XzReadableStream((n=t,new ReadableStream({start(e){e.enqueue(n),e.close();}}))));return new Uint8Array(await e.arrayBuffer())}if("rans"===e){const e=new Uint8Array(r);return (0, h.default)(t,e),e}if("rans4x16"===e)return f.r4x16_uncompress(t);if("arith"===e)return f.arith_uncompress(t);if("fqzcomp"===e)return f.fqzcomp_uncompress(t);if("tok3"===e)return f.tok3_uncompress(t);throw new l.CramUnimplementedError(`${e} decompression not yet implemented`);var n;}async _uncompress(e,t,r){const n=await this._uncompressPre(e,t,r);if(n.length!==r){const e=new Uint8Array(r);return e.set(n,0),e}return n}async readBlock(e){const{majorVersion:t}=await this.getDefinition(),r=(0, m.getSectionParsers)(t),n=await this.readBlockHeader(e),i=n._endPosition,a=await this.file.read(n.compressedSize,i),A="raw"!==n.compressionMethod?await this._uncompress(n.compressionMethod,a,n.uncompressedSize):a,s={...n,_endPosition:i,contentPosition:i,content:A};if(t>=3){const t=await this._parseSection(r.cramBlockCrc32,i+n.compressedSize);s.crc32=t.crc32,this.validateChecksums&&await this.checkCrc32(e,n._size+n.compressedSize,t.crc32,"block data"),s._endPosition=t._endPosition,s._size=s.compressedSize+r.cramBlockCrc32.maxLength;}else s._endPosition=i+s.compressedSize,s._size=s.compressedSize;return s}}t.default=p,"getDefinition getSectionParsers getSamHeader".split(" ").forEach((e=>{(0, B.tinyMemoize)(p,e);}));},7390:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.CramRecord=void 0;var i=r(8401);Object.defineProperty(t,"CramRecord",{enumerable:!0,get:function(){return n(i).default}});var a=r(6794);Object.defineProperty(t,"default",{enumerable:!0,get:function(){return n(a).default}});},8401:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.MateFlagsDecoder=t.CramFlagsDecoder=t.BamFlagsDecoder=t.MateFlags=t.CramFlags=t.BamFlags=void 0;const i=n(r(7295)),a={a:0,A:0,c:1,C:1,g:2,G:2,t:3,T:3,n:4,N:4};function A(e){const t={};for(const[r,n]of e)t[`is${n}`]=e=>!!(e&r),t[`set${n}`]=e=>e|r;return t}t.BamFlags=[[1,"Paired"],[2,"ProperlyPaired"],[4,"SegmentUnmapped"],[8,"MateUnmapped"],[16,"ReverseComplemented"],[32,"MateReverseComplemented"],[64,"Read1"],[128,"Read2"],[256,"Secondary"],[512,"FailedQc"],[1024,"Duplicate"],[2048,"Supplementary"]],t.CramFlags=[[1,"PreservingQualityScores"],[2,"Detached"],[4,"WithMateDownstream"],[8,"DecodeSequenceAsStar"]],t.MateFlags=[[1,"OnNegativeStrand"],[2,"Unmapped"]],t.BamFlagsDecoder=A(t.BamFlags),t.CramFlagsDecoder=A(t.CramFlags),t.MateFlagsDecoder=A(t.MateFlags),t.default=class{constructor({flags:e,cramFlags:t,readLength:r,mappingQuality:n,lengthOnRef:i,qualityScores:a,mateRecordNumber:A,readBases:s,readFeatures:o,mateToUse:c,readGroupId:d,readName:l,sequenceId:f,uniqueId:g,templateSize:h,alignmentStart:u,tags:C}){this.flags=e,this.cramFlags=t,this.readLength=r,this.mappingQuality=n,this.lengthOnRef=i,this.qualityScores=a,s&&(this.readBases=s),this.readGroupId=d,this.readName=l,this.sequenceId=f,this.uniqueId=g,this.templateSize=h,this.alignmentStart=u,this.tags=C,o&&(this.readFeatures=o),c&&(this.mate={flags:c.mateFlags,readName:c.mateReadName,sequenceId:c.mateSequenceId,alignmentStart:c.mateAlignmentStart}),A&&(this.mateRecordNumber=A);}isPaired(){return !!(this.flags&i.default.BAM_FPAIRED)}isProperlyPaired(){return !!(this.flags&i.default.BAM_FPROPER_PAIR)}isSegmentUnmapped(){return !!(this.flags&i.default.BAM_FUNMAP)}isMateUnmapped(){return !!(this.flags&i.default.BAM_FMUNMAP)}isReverseComplemented(){return !!(this.flags&i.default.BAM_FREVERSE)}isMateReverseComplemented(){return !!(this.flags&i.default.BAM_FMREVERSE)}isRead1(){return !!(this.flags&i.default.BAM_FREAD1)}isRead2(){return !!(this.flags&i.default.BAM_FREAD2)}isSecondary(){return !!(this.flags&i.default.BAM_FSECONDARY)}isFailedQc(){return !!(this.flags&i.default.BAM_FQCFAIL)}isDuplicate(){return !!(this.flags&i.default.BAM_FDUP)}isSupplementary(){return !!(this.flags&i.default.BAM_FSUPPLEMENTARY)}isDetached(){return !!(this.cramFlags&i.default.CRAM_FLAG_DETACHED)}hasMateDownStream(){return !!(this.cramFlags&i.default.CRAM_FLAG_MATE_DOWNSTREAM)}isPreservingQualityScores(){return !!(this.cramFlags&i.default.CRAM_FLAG_PRESERVE_QUAL_SCORES)}isUnknownBases(){return !!(this.cramFlags&i.default.CRAM_FLAG_NO_SEQ)}getReadBases(){if(!this.readBases&&this._refRegion){const e=function(e,t){if(!e.lengthOnRef&&!e.readLength)return null;if(e.isUnknownBases())return null;const r=e.alignmentStart-t.start;if(!e.readFeatures)return t.seq.slice(r,r+(e.lengthOnRef||0)).toUpperCase();let n="",i=r,a=0;for(;n.length<e.readLength;)if(a<e.readFeatures.length){const r=e.readFeatures[a];if("Q"===r.code||"q"===r.code)a+=1;else if(r.pos===n.length+1)if(a+=1,"b"===r.code){const e=r.data;n+=e,i+=e.length;}else "B"===r.code?(n+=r.data[0],i+=1):"X"===r.code?(n+=r.sub,i+=1):"I"===r.code?n+=r.data:"D"===r.code?i+=r.data:"i"===r.code?n+=r.data:"N"===r.code?i+=r.data:"S"===r.code?n+=r.data:"P"===r.code||r.code;else if(a<e.readFeatures.length){const e=t.seq.slice(i,i+r.pos-n.length-1);n+=e,i+=e.length;}}else {const r=t.seq.slice(i,i+e.readLength-n.length);n+=r,i+=r.length;}return n.toUpperCase()}(this,this._refRegion);e&&(this.readBases=e);}return this.readBases}getPairOrientation(){if(!this.isSegmentUnmapped()&&this.isPaired()&&!this.isMateUnmapped()&&this.mate&&this.sequenceId===this.mate.sequenceId){const e=this.isReverseComplemented()?"R":"F",t=this.isMateReverseComplemented()?"R":"F";let r=" ",n=" ";this.isRead1()?(r="1",n="2"):this.isRead2()&&(r="2",n="1");const i=[];let a=this.templateLength||this.templateSize;if(void 0===a)throw new Error("One of templateSize and templateLength must be set");return this.alignmentStart>this.mate.alignmentStart&&a>0&&(a=-a),a>0?(i[0]=e,i[1]=r,i[2]=t,i[3]=n):(i[2]=e,i[3]=r,i[0]=t,i[1]=n),i.join("")}return null}addReferenceSequence(e,t){this.readFeatures&&this.readFeatures.forEach((r=>{"X"===r.code&&function(e,t,r,n){const i=n.refPos-t.start,A=t.seq.charAt(i);A&&(n.ref=A);let s=a[A];void 0===s&&(s=4);const o=r.substitutionMatrix[s][n.data];o&&(n.sub=o);}(0,e,t,r);})),!this.readBases&&e.start<=this.alignmentStart&&e.end>=this.alignmentStart+(this.lengthOnRef||this.readLength)-1&&(this._refRegion=e);}toJSON(){const e={};return Object.keys(this).forEach((t=>{t.startsWith("_")||(e[t]=this[t]);})),e.readBases=this.getReadBases(),e}};},3165:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.cramFileDefinition=i,t.cramBlockHeader=a,t.cramBlockCrc32=A,t.cramTagDictionary=o,t.cramPreservationMap=c,t.isMappedSliceHeader=function(e){return "number"==typeof e.refSeqId},t.getSectionParsers=function(e){return {cramFileDefinition:i(),cramBlockHeader:a(),cramBlockCrc32:A(),cramDataSeriesEncodingMap:h(),cramTagEncodingMap:u(),cramCompressionHeader:{parser:(e,t)=>{const{value:r,offset:n}=c().parser(e,t);t=n;const{value:i,offset:a}=h().parser(e,t);t=a;const{value:A,offset:s}=u().parser(e,t);return t=s,{value:{dataSeriesEncoding:d(i),preservation:d(r),tagEncoding:d(A)},offset:t}}},cramEncoding:{parser:(e,t)=>g(e,t)},cramUnmappedSliceHeader:l(e),cramMappedSliceHeader:f(e),cramContainerHeader1:C(e),cramContainerHeader2:I(e)}};const n=r(400);function i(){return {parser:(e,t=0)=>{const r=new DataView(e.buffer,e.byteOffset,e.length),n=new TextDecoder("utf8");let i=0;const a=n.decode(e.subarray(i,i+4));i+=4;const A=r.getUint8(i);i+=1;const s=r.getUint8(i);i+=1;const o=n.decode(e.subarray(i,i+20)).replaceAll("\0","");return i+=20,{value:{magic:a,majorVersion:A,minorVersion:s,fileId:o},offset:i}},maxLength:26}}function a(){return {parser:(e,t=0)=>{const r=e,i=new DataView(r.buffer,r.byteOffset,r.length);let a=0;const A=i.getUint8(a),s=["raw","gzip","bzip2","lzma","rans","rans4x16","arith","fqzcomp","tok3"][A];if(!s)throw new Error(`compression method number ${A} not implemented`);a+=1;const o=i.getUint8(a),c=["FILE_HEADER","COMPRESSION_HEADER","MAPPED_SLICE_HEADER","UNMAPPED_SLICE_HEADER","EXTERNAL_DATA","CORE_DATA"][o];if(!c)throw new Error(`invalid block content type id ${o}`);a+=1;const[d,l]=(0, n.parseItf8)(e,a);a+=l;const[f,g]=(0, n.parseItf8)(e,a);a+=g;const[h,u]=(0, n.parseItf8)(e,a);return a+=u,{offset:a,value:{uncompressedSize:h,compressedSize:f,contentId:d,contentType:c,compressionMethod:s}}},maxLength:17}}function A(){return {parser:(e,t)=>{const r=e,n=new DataView(r.buffer,r.byteOffset,r.length).getUint32(t,!0);return {offset:t+=4,value:{crc32:n}}},maxLength:4}}function s(e,t,r){const n=new TextDecoder("utf8").decode(e.subarray(t,r)),i=[];for(let e=0;e<n.length;e+=3)i.push(n.slice(e,e+3));return i}function o(){return {parser:(e,t)=>{const[r,i]=(0, n.parseItf8)(e,t);t+=i;const a=e.subarray(t,t+r);t+=r;const A=[];let o=0,c=0;for(;c<a.length;c++)a[c]||(A.push(s(a,o,c)),o=c+1);return c>o&&A.push(s(a,o,c)),{value:{size:r,ents:A},offset:t}}}}function c(){return {parser:(e,t)=>{const r=e,i=new DataView(r.buffer,r.byteOffset,r.length),[a,A]=(0, n.parseItf8)(e,t);t+=A;const[s,c]=(0, n.parseItf8)(e,t);t+=c;const d=[];for(let r=0;r<s;r++){const r=String.fromCharCode(e[t])+String.fromCharCode(e[t+1]);if(t+=2,"MI"===r||"UI"===r||"PI"===r||"RN"===r||"AP"===r||"RR"===r)d.push({key:r,value:!!i.getUint8(t)}),t+=1;else if("SM"===r)d.push({key:r,value:[i.getUint8(t),i.getUint8(t+1),i.getUint8(t+2),i.getUint8(t+3),i.getUint8(t+4)]}),t+=5;else {if("TD"!==r)throw new Error(`unknown key ${r}`);{const{offset:n,value:i}=o().parser(e,t);d.push({key:r,value:i.ents}),t=n;}}}return {value:{mapSize:a,mapCount:s,ents:d},offset:t}}}}function d(e){const t={};for(const{key:r,value:n}of e.ents)t[r]&&console.warn(`duplicate key ${r} in map`),t[r]=n;return t}function l(e){return {parser:(t,r)=>{const[i,a]=(0, n.parseItf8)(t,r);r+=a;let A=0;if(e>=3){const[e,i]=(0, n.parseLtf8)(t,r);r+=i,A=e;}else if(2===e){const[e,i]=(0, n.parseItf8)(t,r);r+=i,A=e;}else console.warn("recordCounter=0");const[s,o]=(0, n.parseItf8)(t,r);r+=o;const[c,d]=(0, n.parseItf8)(t,r);r+=d;const l=[];for(let e=0;e<c;e++){const[e,i]=(0, n.parseItf8)(t,r);r+=i,l.push(e);}let f;return e>=2&&(f=[...t.subarray(r,r+16)],r+=16),{value:{recordCounter:A,md5:f,contentIds:l,numContentIds:c,numBlocks:s,numRecords:i},offset:r}},maxLength:e=>40+5*e}}function f(e){return {parser:(t,r)=>{const[i,a]=(0, n.parseItf8)(t,r);r+=a;const[A,s]=(0, n.parseItf8)(t,r);r+=s;const[o,c]=(0, n.parseItf8)(t,r);r+=c;const[d,l]=(0, n.parseItf8)(t,r);r+=l;let f=0;if(e>=3){const[e,i]=(0, n.parseLtf8)(t,r);r+=i,f=e;}else if(2===e){const[e,i]=(0, n.parseItf8)(t,r);r+=i,f=e;}else console.warn("majorVersion is <2, recordCounter set to 0");const[g,h]=(0, n.parseItf8)(t,r);r+=h;const[u,C]=(0, n.parseItf8)(t,r);r+=C;const I=[];for(let e=0;e<u;e++){const[e,i]=(0, n.parseItf8)(t,r);r+=i,I.push(e);}const[_,m]=(0, n.parseItf8)(t,r);let B;return r+=m,e>=2&&(B=[...t.subarray(r,r+16)],r+=16),{value:{md5:B,numBlocks:g,numRecords:d,numContentIds:u,refSeqSpan:o,refSeqId:i,refSeqStart:A,recordCounter:f,refBaseBlockId:_,contentIds:I},offset:r}},maxLength:e=>60+5*e}}function g(e,t){const r=e,i=new DataView(r.buffer,r.byteOffset,r.length),[a,A]=(0, n.parseItf8)(e,t);t+=A;const[s,o]=(0, n.parseItf8)(e,t);t+=o;const c={};if(0===a);else if(1===a){const[r,i]=(0, n.parseItf8)(e,t);c.blockContentId=r,t+=i;}else if(2===a){const[r,i]=(0, n.parseItf8)(e,t);c.offset=r,t+=i;const[a,A]=(0, n.parseItf8)(e,t);c.M=a,t+=A;}else if(3===a){const r=(0, n.parseItf8)(e,t),i=r[0];t+=r[1];const a=[];for(let r=0;r<i;r++){const r=(0, n.parseItf8)(e,t);a.push(r[0]),t+=r[1];}c.symbols=a;const A=(0, n.parseItf8)(e,t),s=r[0];c.numLengths=s,c.numCodes=i,c.numLengths=s,t+=A[1];const o=[];for(let r=0;r<s;r++){const r=(0, n.parseItf8)(e,t);t+=r[1],o.push(r[0]);}c.bitLengths=o;}else if(4===a){const{value:r,offset:n}=g(e,t);c.lengthsEncoding=r,t=n;const{value:i,offset:a}=g(e,t);c.valuesEncoding=i,t=a;}else if(5===a){c.stopByte=i.getUint8(t),t+=1;const[r,a]=(0, n.parseItf8)(e,t);c.blockContentId=r,t+=a;}else if(6===a){const[r,i]=(0, n.parseItf8)(e,t);c.offset=r,t+=i;const[a,A]=(0, n.parseItf8)(e,t);c.length=a,t+=A;}else if(7===a){const[r,i]=(0, n.parseItf8)(e,t);c.offset=r,t+=i;const[a,A]=(0, n.parseItf8)(e,t);c.K=a,t+=A;}else if(8===a){const[r,i]=(0, n.parseItf8)(e,t);c.offset=r,t+=i;const[a,A]=(0, n.parseItf8)(e,t);c.log2m=a,t+=A;}else {if(9!==a)throw new Error(`unknown codecId ${a}`);{const[r,i]=(0, n.parseItf8)(e,t);c.offset=r,t+=i;}}return {value:{codecId:a,parametersBytes:s,parameters:c},offset:t}}function h(){return {parser:(e,t)=>{const[r,i]=(0, n.parseItf8)(e,t);t+=i;const[a,A]=(0, n.parseItf8)(e,t);t+=A;const s=[];for(let r=0;r<a;r++){const r=String.fromCharCode(e[t])+String.fromCharCode(e[t+1]);t+=2;const{value:n,offset:i}=g(e,t);t=i,s.push({key:r,value:n});}return {value:{mapSize:r,ents:s,mapCount:a},offset:t}}}}function u(){return {parser:(e,t)=>{const[r,i]=(0, n.parseItf8)(e,t);t+=i;const[a,A]=(0, n.parseItf8)(e,t);t+=A;const s=[];for(let r=0;r<a;r++){const[r,i]=(0, n.parseItf8)(e,t);t+=i;const a=String.fromCharCode(r>>16&255)+String.fromCharCode(r>>8&255)+String.fromCharCode(255&r),{value:A,offset:o}=g(e,t);t=o,s.push({key:a,value:A});}return {value:{mapSize:r,ents:s,mapCount:a},offset:t}}}}function C(e){return {maxLength:52,parser:(t,r)=>{const i=t,a=new DataView(i.buffer,i.byteOffset,i.length).getInt32(r,!0);r+=4;const[A,s]=(0, n.parseItf8)(t,r);r+=s;const[o,c]=(0, n.parseItf8)(t,r);r+=c;const[d,l]=(0, n.parseItf8)(t,r);r+=l;const[f,g]=(0, n.parseItf8)(t,r);r+=g;let h,u=0;if(e>=3){const[e,i]=(0, n.parseLtf8)(t,r);u=e,r+=i;}else if(2===e){const[e,i]=(0, n.parseItf8)(t,r);u=e,r+=i;}else console.warn("setting recordCounter=0");if(e>1){const[e,i]=(0, n.parseLtf8)(t,r);h=e,r+=i;}const[C,I]=(0, n.parseItf8)(t,r);r+=I;const[_,m]=(0, n.parseItf8)(t,r);return {value:{length:a,refSeqId:A,refSeqStart:o,alignmentSpan:d,numBlocks:C,numLandmarks:_,numBases:h,recordCounter:u,numRecords:f},offset:r+=m}}}}function I(e){return {parser:(t,r)=>{const i=t,a=new DataView(i.buffer,i.byteOffset,i.length),[A,s]=(0, n.parseItf8)(t,r);r+=s;const o=[];for(let e=0;e<A;e++){const[e,i]=(0, n.parseItf8)(t,r);r+=i,o.push(e);}let c;return e>=3&&(c=a.getUint32(r,!0),r+=4),{value:{...void 0===c?{}:{crc32:c},numLandmarks:A,landmarks:o},offset:r}},maxLength:e=>5+5*e+4}}},6484:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t,r,o,c,d,l,f,g){let h=t("BF");const u=t("CF");if(!(0, a.isMappedSliceHeader)(o.parsedContent))throw new Error("slice header not mapped");const C=f>1&&-2===o.parsedContent.refSeqId?t("RI"):o.parsedContent.refSeqId,I=t("RL");let _=t("AP");r.APdelta&&(_+=l.lastAlignmentStart),l.lastAlignmentStart=_;const m=t("RG");let B,p,w,E;if(r.readNamesIncluded&&(B=A(t("RN"))),i.CramFlagsDecoder.isDetached(u)){const e=t("MF");let n;r.readNamesIncluded||(n=A(t("RN")),B=n);const a=t("NS"),s=t("NP");(e||a>-1)&&(p={mateFlags:e,mateSequenceId:a,mateAlignmentStart:s,mateReadName:n}),w=t("TS"),i.MateFlagsDecoder.isUnmapped(e)&&(h=i.BamFlagsDecoder.setMateUnmapped(h)),i.MateFlagsDecoder.isOnNegativeStrand(e)&&(h=i.BamFlagsDecoder.setMateReverseComplemented(h));}else i.CramFlagsDecoder.isWithMateDownstream(u)&&(E=t("NF")+g+1);const Q=t("TL");if(Q<0)throw new n.CramMalformedError("invalid TL index");const y={},b=r.getTagNames(Q),S=b.length;for(let t=0;t<S;t++){const n=b[t],i=n.slice(0,2),a=n.slice(2,3),A=r.getCodecForTag(n).decode(e,c,d,l);y[i]=void 0===A?void 0:"number"==typeof A?A:s(a,A);}let D,k,v,R,F;if(i.BamFlagsDecoder.isSegmentUnmapped(h))if(i.CramFlagsDecoder.isDecodeSequenceAsStar(u))F=null,R=null;else {const e=new Array(I);for(let r=0;r<e.length;r++)e[r]=t("BA");if(F=String.fromCharCode(...e),i.CramFlagsDecoder.isPreservingQualityScores(u)){R=new Array(I);for(let r=0;r<e.length;r++)R[r]=t("QS");}}else {const e=t("FN");if(e&&(D=function(e,t,r,i,a){let A=0,s=e-1;const o=new Array(t);function c([e,t]){const n=r(t);if("character"===e)return String.fromCharCode(n);if("string"===e){let e="";for(let t=0;t<n.byteLength;t++)e+=String.fromCharCode(n[t]);return e}return "numArray"===e?Array.from(n):n}for(let e=0;e<t;e++){const t=String.fromCharCode(r("FC")),i=r("FP"),d={B:["character","BA"],S:["string",a>1?"SC":"IN"],X:["number","BS"],D:["number","DL"],I:["string","IN"],i:["character","BA"],b:["string","BB"],q:["numArray","QQ"],Q:["number","QS"],H:["number","HC"],P:["number","PD"],N:["number","RS"]}[t];if(!d)throw new n.CramMalformedError(`invalid read feature code "${t}"`);let l=c(d);const f={B:["number","QS"]}[t];f&&(l=[l,c(f)]),A+=i;const g=A;s+=i;const h=s;"D"===t||"N"===t?s+=l:"I"===t||"S"===t?s-=l.length:"i"===t&&(s-=1),o[e]={code:t,pos:g,refPos:h,data:l};}return o}(_,e,t,0,f)),k=I,D)for(const{code:e,data:t}of D)"D"===e||"N"===e?k+=t:"I"===e||"S"===e?k-=t.length:"i"===e&&(k-=1);if(Number.isNaN(k)&&(console.warn(`${B||`${C}:${_}`} record has invalid read features`),k=I),v=t("MQ"),i.CramFlagsDecoder.isPreservingQualityScores(u)){R=new Array(I);for(let e=0;e<R.length;e++)R[e]=t("QS");}}return {readLength:I,sequenceId:C,cramFlags:u,flags:h,alignmentStart:_,readGroupId:m,readName:B,mateToUse:p,templateSize:w,mateRecordNumber:E,readFeatures:D,lengthOnRef:k,mappingQuality:v,qualityScores:R,readBases:F,tags:y}};const n=r(9747),i=r(8401),a=r(3165);function A(e){let t="";for(let r=0;r<e.length&&0!==e[r];r++)t+=String.fromCharCode(e[r]);return t}function s(e,t){if("Z"===e)return A(t);if("A"===e)return String.fromCharCode(t[0]);if("I"===e)return new Uint32Array(t.buffer)[0];if("i"===e)return new Int32Array(t.buffer)[0];if("s"===e)return new Int16Array(t.buffer)[0];if("S"===e)return new Uint16Array(t.buffer)[0];if("c"===e)return new Int8Array(t.buffer)[0];if("C"===e)return t[0];if("f"===e)return new Float32Array(t.buffer)[0];if("H"===e)return Number.parseInt(A(t).replace(/^0x/,""),16);if("B"===e)return function(e){const t=String.fromCharCode(e[0]),r=new DataView(e.buffer).getUint32(1,!0),n=new Array(r);if(e=e.slice(5),"c"===t){const t=new Int8Array(e.buffer);for(let e=0;e<r;e++)n[e]=t[e];}else if("C"===t){const t=new Uint8Array(e.buffer);for(let e=0;e<r;e++)n[e]=t[e];}else if("s"===t){const t=new Int16Array(e.buffer);for(let e=0;e<r;e++)n[e]=t[e];}else if("S"===t){const t=new Uint16Array(e.buffer);for(let e=0;e<r;e++)n[e]=t[e];}else if("i"===t){const t=new Int32Array(e.buffer);for(let e=0;e<r;e++)n[e]=t[e];}else if("I"===t){const t=new Uint32Array(e.buffer);for(let e=0;e<r;e++)n[e]=t[e];}else {if("f"!==t)throw new Error(`unknown type: ${t}`);{const t=new Float32Array(e.buffer);for(let e=0;e<r;e++)n[e]=t[e];}}return n}(t);throw new n.CramMalformedError(`Unrecognized tag type ${e}`)}},6771:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const i=r(9747),a=r(612),A=n(r(7295)),s=n(r(6484)),o=n(r(8401)),c=r(3165),d=r(400);function l(e,t,r,n){const a=!!(n.mate||void 0!==n.mateRecordNumber&&n.mateRecordNumber!==t);r.readName||(r.readName=String(r.uniqueId),n.readName=r.readName),r.mate={sequenceId:n.sequenceId,alignmentStart:n.alignmentStart,uniqueId:n.uniqueId},n.readName&&(r.mate.readName=n.readName),n.mate||void 0!==n.mateRecordNumber||(n.mate={sequenceId:r.sequenceId,alignmentStart:r.alignmentStart,uniqueId:r.uniqueId},r.readName&&(n.mate.readName=r.readName)),r.flags|=A.default.BAM_FPAIRED,n.flags&A.default.BAM_FUNMAP&&(r.flags|=A.default.BAM_FMUNMAP),r.flags&A.default.BAM_FUNMAP&&(n.flags|=A.default.BAM_FMUNMAP),n.flags&A.default.BAM_FREVERSE&&(r.flags|=A.default.BAM_FMREVERSE),r.flags&A.default.BAM_FREVERSE&&(n.flags|=A.default.BAM_FMREVERSE),void 0===r.templateLength&&(a?function(e,t,r){const n=function t(r){const n=[r];if(void 0!==r.mateRecordNumber&&r.mateRecordNumber>=0){const a=e[r.mateRecordNumber];if(!a)throw new i.CramMalformedError("intra-slice mate record not found, this file seems malformed");n.push(...t(a));}return n}(r),a=n.map((e=>e.alignmentStart)),A=n.map((e=>e.alignmentStart+e.readLength-1)),s=Math.max(...A)-Math.min(...a)+1;s>=0&&n.forEach((e=>{if(void 0!==e.templateLength)throw new i.CramMalformedError("mate pair group has some members that have template lengths already, this file seems malformed");e.templateLength=s;}));}(e,0,r):function(e,t){const r=Math.min(e.alignmentStart,t.alignmentStart),n=Math.max(e.alignmentStart+e.readLength-1,t.alignmentStart+t.readLength-1)-r+1;e.templateLength=n,t.templateLength=n;}(r,n)),r.mateRecordNumber=void 0;}class f{constructor(e,t,r){this.container=e,this.containerPosition=t,this.sliceSize=r,this.file=e.file;}async getHeader(){const{majorVersion:e}=await this.file.getDefinition(),t=(0, c.getSectionParsers)(e),r=await this.container.getHeader(),n=await this.file.readBlock(r._endPosition+this.containerPosition);if("MAPPED_SLICE_HEADER"===n.contentType){const e=(0, d.parseItem)(n.content,t.cramMappedSliceHeader.parser,0,r._endPosition);return {...n,parsedContent:e}}if("UNMAPPED_SLICE_HEADER"===n.contentType){const e=(0, d.parseItem)(n.content,t.cramUnmappedSliceHeader.parser,0,r._endPosition);return {...n,parsedContent:e}}throw new i.CramMalformedError(`error reading slice header block, invalid content type ${n.contentType}`)}async getBlocks(){const e=await this.getHeader();let t=e._endPosition;const r=new Array(e.parsedContent.numBlocks);for(let e=0;e<r.length;e++){const n=await this.file.readBlock(t);r[e]=n,t=r[e]._endPosition;}return r}async getCoreDataBlock(){return (await this.getBlocks())[0]}async _getBlocksContentIdIndex(){const e=await this.getBlocks(),t={};return e.forEach((e=>{"EXTERNAL_DATA"===e.contentType&&(t[e.contentId]=e);})),t}async getBlockByContentId(e){return (await this._getBlocksContentIdIndex())[e]}async getReferenceRegion(){const e=new TextDecoder("utf8"),t=(await this.getHeader()).parsedContent;if(!(0, c.isMappedSliceHeader)(t))throw new Error("slice header not mapped");if(t.refSeqId<0)return;const r=await this.container.getCompressionScheme();if(void 0===r)throw new Error("compression scheme undefined");if(t.refBaseBlockId>=0){const r=await this.getBlockByContentId(t.refBaseBlockId);if(!r)throw new i.CramMalformedError("embedded reference specified, but reference block does not exist");return {seq:e.decode(r.data),start:t.refSeqStart,end:t.refSeqStart+t.refSeqSpan-1,span:t.refSeqSpan}}if(r.referenceRequired||this.file.fetchReferenceSequenceCallback){if(!this.file.fetchReferenceSequenceCallback)throw new Error("reference sequence not embedded, and seqFetch callback not provided, cannot fetch reference sequence");const e=await this.file.fetchReferenceSequenceCallback(t.refSeqId,t.refSeqStart,t.refSeqStart+t.refSeqSpan-1);if(e.length!==t.refSeqSpan)throw new i.CramArgumentError("seqFetch callback returned a reference sequence of the wrong length");return {seq:e,start:t.refSeqStart,end:t.refSeqStart+t.refSeqSpan-1,span:t.refSeqSpan}}}getAllRecords(){return this.getRecords((()=>!0))}async _fetchRecords(){const{majorVersion:e}=await this.file.getDefinition(),t=await this.container.getCompressionScheme();if(void 0===t)throw new Error("compression scheme undefined");const r=await this.getHeader(),n=await this._getBlocksContentIdIndex();if(e>1&&this.file.options.checkSequenceMD5&&(0, c.isMappedSliceHeader)(r.parsedContent)&&r.parsedContent.refSeqId>=0&&"0000000000000000"!==r.parsedContent.md5?.join("")){const e=await this.getReferenceRegion();if(e){const{seq:t,start:n,end:a}=e,A=(0, d.sequenceMD5)(t),s=r.parsedContent.md5?.map((e=>(e<16?"0":"")+e.toString(16))).join("");if(A!==s)throw new i.CramMalformedError(`MD5 checksum reference mismatch for ref ${r.parsedContent.refSeqId} pos ${n}..${a}. recorded MD5: ${s}, calculated MD5: ${A}`)}}const A=await this.getCoreDataBlock(),f={lastAlignmentStart:(0, c.isMappedSliceHeader)(r.parsedContent)?r.parsedContent.refSeqStart:0,coreBlock:{bitPosition:7,bytePosition:0},externalBlocks:{map:new Map,getCursor(e){let t=this.map.get(e);return void 0===t&&(t={bitPosition:7,bytePosition:0},this.map.set(e,t)),t}}},g=e=>{const r=t.getCodecForDataSeries(e);if(!r)throw new i.CramMalformedError(`no codec defined for ${e} data series`);return r.decode(this,A,n,f)},h=new Array(r.parsedContent.numRecords);for(let i=0;i<h.length;i+=1)try{const a=(0,s.default)(this,g,t,r,A,n,f,e,i);h[i]=new o.default({...a,uniqueId:r.contentPosition+r.parsedContent.recordCounter+i+1});}catch(e){if(e instanceof a.CramBufferOverrunError){console.warn("read attempted beyond end of buffer, file seems truncated.");break}throw e}for(let e=0;e<h.length;e+=1){const t=h[e];if(t){const{mateRecordNumber:r}=t;void 0!==r&&r>=0&&h[r]&&l(h,e,t,h[r]);}}return h}async getRecords(e){const t=this.container.filePosition+this.containerPosition;let r=this.file.featureCache.get(t.toString());r||(r=this._fetchRecords(),this.file.featureCache.set(t.toString(),r));const n=(await r).filter(e);if(n.length&&this.file.fetchReferenceSequenceCallback){const e=await this.getHeader();if((0, c.isMappedSliceHeader)(e.parsedContent)&&(e.parsedContent.refSeqId>=0||-2===e.parsedContent.refSeqId)){const t=e.parsedContent.refSeqId>=0?e.parsedContent.refSeqId:void 0,r=await this.container.getCompressionScheme();if(void 0===r)throw new Error("compression scheme undefined");const i={};for(const e of n){const r=void 0!==t?t:e.sequenceId;let n=i[r];n||(n={id:r,start:e.alignmentStart,end:Number.NEGATIVE_INFINITY,seq:null},i[r]=n);const a=e.alignmentStart+(e.lengthOnRef||e.readLength)-1;a>n.end&&(n.end=a),e.alignmentStart<n.start&&(n.start=e.alignmentStart);}await Promise.all(Object.values(i).map((async e=>{-1!==e.id&&e.start<=e.end&&this.file.fetchReferenceSequenceCallback&&(e.seq=await this.file.fetchReferenceSequenceCallback(e.id,e.start,e.end));})));for(const e of n){const n=i[void 0!==t?t:e.sequenceId];if(n?.seq){const t=n.seq;e.addReferenceSequence({...n,seq:t},r);}}}}return n}}t.default=f,"getHeader getBlocks _getBlocksContentIdIndex".split(" ").forEach((e=>{(0, d.tinyMemoize)(f,e);}));},400:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.TWO_PWR_56_DBL=t.TWO_PWR_24_DBL=t.TWO_PWR_64_DBL=t.TWO_PWR_32_DBL=t.TWO_PWR_16_DBL=void 0,t.itf8Size=function(e){return -128&e?-16384&e?-2097152&e?-268435456&e?5:4:3:2:1},t.parseItf8=function(e,t){let r=t;const n=e[r];let i;return n<128?(i=n,r+=1):n<192?(i=(63&n)<<8|e[r+1],r+=2):n<224?(i=(31&n)<<16|e[r+1]<<8|e[r+2],r+=3):n<240?(i=(15&n)<<24|e[r+1]<<16|e[r+2]<<8|e[r+3],r+=4):(i=(15&n)<<28|e[r+1]<<20|e[r+2]<<12|e[r+3]<<4|15&e[r+4],r+=5),[i,r-t]},t.parseLtf8=function(e,r){let n=r;const i=e[n];let a;return i<128?(a=i,n+=1):i<192?(a=16383&(i<<8|e[n+1]),n+=2):i<224?(a=(63&i)<<16|e[n+1]<<8|e[n+2],n+=3):i<240?(a=(31&i)<<24|e[n+1]<<16|e[n+2]<<8|e[n+3],n+=4):i<248?(a=(15&e[n])*t.TWO_PWR_32_DBL+(e[n+1]<<24|e[n+2]<<16|e[n+3]<<8|e[n+4]),n+=5):i<252?(a=((7&e[n])<<8|e[n+1])*t.TWO_PWR_32_DBL+(e[n+2]<<24|e[n+3]<<16|e[n+4]<<8|e[n+5]),n+=6):i<254?(a=((3&e[n])<<16|e[n+1]<<8|e[n+2])*t.TWO_PWR_32_DBL+(e[n+3]<<24|e[n+4]<<16|e[n+5]<<8|e[n+6]),n+=7):i<255?(a=(e[n+1]<<24|e[n+2]<<16|e[n+3]<<8|e[n+4])*t.TWO_PWR_32_DBL+(e[n+5]<<24|e[n+6]<<16|e[n+7]<<8|e[n+8]),n+=8):(a=e[n+1]*t.TWO_PWR_56_DBL+(e[n+2]<<24|e[n+3]<<16|e[n+4]<<8|e[n+5])*t.TWO_PWR_32_DBL+(e[n+6]<<24|e[n+7]<<16|e[n+8]<<8|e[n+9]),n+=9),[a,n-r]},t.parseItem=function(e,t,r=0,n=0){const{offset:i,value:a}=t(e,r);return {...a,_endPosition:i+n,_size:i-r}},t.tinyMemoize=function(e,t){const r=e.prototype[t],n=`_memo_${t}`;e.prototype[t]=function(){if(!(n in this)){const e=r.call(this);this[n]=e,Promise.resolve(e).catch((()=>{delete this[n];}));}return this[n]};},t.sequenceMD5=function(e){return (0, i.default)(e.toUpperCase().replaceAll(/[^\u0021-\u007e]/g,""))};const i=n(r(3503));t.TWO_PWR_16_DBL=65536,t.TWO_PWR_32_DBL=t.TWO_PWR_16_DBL*t.TWO_PWR_16_DBL,t.TWO_PWR_64_DBL=t.TWO_PWR_32_DBL*t.TWO_PWR_32_DBL,t.TWO_PWR_24_DBL=1<<24,t.TWO_PWR_56_DBL=t.TWO_PWR_24_DBL*t.TWO_PWR_32_DBL;},9747:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.CramArgumentError=t.CramSizeLimitError=t.CramMalformedError=t.CramUnimplementedError=t.CramError=void 0;class r extends Error{}t.CramError=r;class n extends Error{}t.CramUnimplementedError=n,t.CramMalformedError=class extends r{},t.CramSizeLimitError=class extends r{},t.CramArgumentError=class extends r{};},2884:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const i=r(9995),a=n(r(8175)),A=n(r(5871)),s=n(r(2935));t.default=class{decode(e){return this.stream=new s.default(e),this.decodeStream(this.stream)}decodeStream(e,t=0){const r=this.stream.ReadByte();16&r||(t=this.stream.ReadUint7());let n=t;const i=1&r;if(8&r)return this.decodeStripe(this.stream,t);var a;if(128&r&&([a,n]=this.decodePackMeta(this.stream)),32&r)var A=this.decodeCat(this.stream,n);else A=4&r?this.decodeExt(this.stream,n):64&r?i?this.decodeRLE1(this.stream,n):this.decodeRLE0(this.stream,n):i?this.decode1(this.stream,n):this.decode0(this.stream,n);return 128&r&&(A=this.decodePack(A,a,t)),A}decode0(e,t){const r=new Uint8Array(t);let n=e.ReadByte();0==n&&(n=256);const i=new A.default(n),s=new a.default(e);s.RangeStartDecode(e);for(let n=0;n<t;n++)r[n]=i.ModelDecode(e,s);return r}decode1(e,t){const r=new Uint8Array(t);let n=e.ReadByte();0==n&&(n=256);const i=new Array(n);for(var s=0;s<n;s++)i[s]=new A.default(n);const o=new a.default(e);o.RangeStartDecode(e);let c=0;for(s=0;s<t;s++)r[s]=i[c].ModelDecode(e,o),c=r[s];return r}decodeExt(e,t){return (0, i.decode)(e.buf.slice(e.pos))}decodeRLE0(e,t){const r=new Uint8Array(t);let n=e.ReadByte();0==n&&(n=256);const i=new A.default(n),s=new Array(258);for(var o=0;o<=257;o++)s[o]=new A.default(4);const c=new a.default(e);for(c.RangeStartDecode(e),o=0;o<t;){r[o]=i.ModelDecode(e,c);let t=s[r[o]].ModelDecode(e,c),n=t,a=256;for(;3==t;)t=s[a].ModelDecode(e,c),a=257,n+=t;for(let e=1;e<=n;e++)r[o+e]=r[o];o+=n+1;}return r}decodeRLE1(e,t){const r=new Uint8Array(t);let n=e.ReadByte();0==n&&(n=256);const i=new Array(n);for(var s=0;s<n;s++)i[s]=new A.default(n);const o=new Array(258);for(s=0;s<=257;s++)o[s]=new A.default(4);const c=new a.default(e);c.RangeStartDecode(e);let d=0;for(s=0;s<t;){r[s]=i[d].ModelDecode(e,c),d=r[s];let t=o[r[s]].ModelDecode(e,c),n=t,a=256;for(;3==t;)t=o[a].ModelDecode(e,c),a=257,n+=t;for(let e=1;e<=n;e++)r[s+e]=r[s];s+=n+1;}return r}decodePackMeta(e){this.nsym=e.ReadByte();const t=new Array(this.nsym);for(let r=0;r<this.nsym;r++)t[r]=e.ReadByte();return [t,e.ReadUint7()]}decodePack(e,t,r){const n=new Uint8Array(r);if(this.nsym<=1)for(var i=0;i<r;i++)n[i]=t[0];else if(this.nsym<=2){i=0;for(var a=0;i<r;i++){if(i%8==0)var A=e[a++];n[i]=t[1&A],A>>=1;}}else if(this.nsym<=4)for(i=0,a=0;i<r;i++)i%4==0&&(A=e[a++]),n[i]=t[3&A],A>>=2;else {if(!(this.nsym<=16))return e;for(i=0,a=0;i<r;i++)i%2==0&&(A=e[a++]),n[i]=t[15&A],A>>=4;}return n}packMeta(e){const t=new s.default("",0,1024),r=new Array(256);for(var n=0;n<e.length;n++)r[e[n]]=1;var i=0;for(n=0;n<256;n++)r[n]&&(r[n]=++i);for(t.WriteByte(i),n=0;n<256;n++)r[n]&&(t.WriteByte(n),r[n]--);return [t,r,i]}decodeStripe(e,t){const r=e.ReadByte(),n=new Array(r),i=new Array(r);for(var a=0;a<r;a++)n[a]=e.ReadUint7();const A=new Array(r);for(a=0;a<r;a++)i[a]=Math.floor(t/r)+(t%r>a),A[a]=this.decodeStream(e,i[a]);const s=new Uint8Array(t);for(a=0;a<r;a++)for(let e=0;e<i[a];e++)s[e*r+a]=A[a][e];return s}decodeCat(e,t){const r=new Uint8Array(t);for(let n=0;n<t;n++)r[n]=e.ReadByte();return r}};},8175:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.default=class{constructor(e){this.low=0,this.range=4294967295,this.code=0,this.FFnum=0,this.carry=0,this.cache=0;}RangeStartDecode(e){for(let t=0;t<5;t++)this.code=(this.code<<8)+e.ReadByte();this.code&=4294967295,this.code>>>=0;}RangeGetFrequency(e){return this.range=Math.floor(this.range/e),Math.floor(this.code/this.range)}RangeDecode(e,t,r,n){for(this.code-=t*this.range,this.range*=r;this.range<1<<24;)this.range*=256,this.code=256*this.code+e.ReadByte();}RangeShiftLow(e){if(this.low<4278190080|this.carry){for(e.WriteByte(this.cache+this.carry);this.FFnum;)e.WriteByte(this.carry-1),this.FFnum--;this.cache=this.low>>>24,this.carry=0;}else this.FFnum++;this.low<<=8,this.low>>>=0;}RangeEncode(e,t,r,n){const i=this.low;for(this.range=Math.floor(this.range/n),this.low+=t*this.range,this.low>>>=0,this.range*=r,this.low<i&&(0!=this.carry&&void 0,this.carry=1);this.range<1<<24;)this.range*=256,this.RangeShiftLow(e);}RangeFinishEncode(e){for(let t=0;t<5;t++)this.RangeShiftLow(e);}};},5871:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.default=class{constructor(e=256){this.total_freq=e,this.max_sym=e-1,this.S=[],this.F=[];for(let e=0;e<=this.max_sym;e++)this.S[e]=e,this.F[e]=1;}ModelDecode(e,t){const r=t.RangeGetFrequency(this.total_freq);let n=0,i=0;for(;n+this.F[i]<=r;)n+=this.F[i++];t.RangeDecode(e,n,this.F[i],this.total_freq),this.F[i]+=16,this.total_freq+=16,this.total_freq>65519&&this.ModelRenormalise();const a=this.S[i];if(i>0&&this.F[i]>this.F[i-1]){let e=this.F[i];this.F[i]=this.F[i-1],this.F[i-1]=e,e=this.S[i],this.S[i]=this.S[i-1],this.S[i-1]=e;}return a}ModelRenormalise(){this.total_freq=0;for(let e=0;e<=this.max_sym;e++)this.F[e]-=Math.floor(this.F[e]/2),this.total_freq+=this.F[e];}ModelEncode(e,t,r){let n=0;for(var i=0;this.S[i]!=r;i++)n+=this.F[i];if(t.RangeEncode(e,n,this.F[i],this.total_freq),this.F[i]+=16,this.total_freq+=16,this.total_freq>65519&&this.ModelRenormalise(),r=this.S[i],i>0&&this.F[i]>this.F[i-1]){let e=this.F[i];this.F[i]=this.F[i-1],this.F[i-1]=e,e=this.S[i],this.S[i]=this.S[i-1],this.S[i-1]=e;}}};},4373:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.decode=function(e,t){return function(e,t){const r=e.ReadUint7(),n=function(e){const t={max_sym:0};if(5!=e.ReadByte())return void console.error("Invalid FQZComp version number");const r=e.ReadByte(),n=r&u?e.ReadByte():1;let i=r.nparam>1?r.nparam-1:0;const a=new Array(256);if(r&C)i=e.ReadByte(),s(e,a,256);else {for(var A=0;A<n;A++)a[A]=A;for(;A<256;A++)a[A]=n-1;}t.do_rev=r&I,t.stab=a,t.max_sel=i,t.params=new Array(t.nparam);for(let r=0;r<n;r++)t.params[r]=m(e),t.max_sym<t.params[r].max_sym&&(t.max_sym=t.params[r].max_sym);return t}(e);if(!n)return;var A=n.params;const o=new Array(t.length),c=function(e){const t={};t.qual=new Array(65536);for(var r=0;r<65536;r++)t.qual[r]=new a.default(e.max_sym+1);for(t.len=new Array(4),r=0;r<4;r++)t.len[r]=new a.default(256);return t.rev=new a.default(2),t.dup=new a.default(2),e.max_sel>0&&(t.sel=new a.default(e.max_sel+1)),t}(n),d=new i.default(e);d.RangeStartDecode(e);const l=new Uint8Array(r),f={qctx:0,prevq:0,delta:0,p:0,s:0,x:0,len:0,is_dup:0,rec:0};let g=0;for(;g<r;){if(0==f.p){if(B(e,d,n,c,f,o),f.is_dup>0&&c.dup.ModelDecode(e,d)){for(let e=0;e<len;e++)l[g+e]=l[g+e-f.len];g+=f.len,f.p=0;continue}t.push(f.len);var h=(A=n.params[f.x]).context;}const r=c.qual[h].ModelDecode(e,d);l[g++]=A.qmap[r],h=_(A,f,r);}return n.do_rev&&function(e,t,r,n){let i=0,a=0;for(;a<t;){if(r[i]){let t=0,r=n[i]-1;for(;t<r;){const n=e[a+t];e[a+t]=e[a+r],e[a+r]=n,t++,r--;}}a+=n[i++];}}(l,r,o,t),l}(new A.default(e),t)};const i=n(r(8175)),a=n(r(5871)),A=n(r(2935));function s(e,t,r){let n=0,i=0,a=-1;const A=new Array(1024);for(;i<r;){const t=e.ReadByte();if(A[n++]=t,i+=t,t==a){let r=e.ReadByte();for(i+=t*r;r--;)A[n++]=t;}a=t;}let s=0;for(n=0,i=0;i<r;){let e=0;do{var o=A[n++];e+=o;}while(255==o);for(;e--;)t[i++]=s;s++;}}const o=2,c=4,d=8,l=16,f=32,g=64,h=128,u=1,C=2,I=4;function _(e,t,r){let n=e.context;return t.qctx=(t.qctx<<e.qshift)+e.qtab[r],n+=(t.qctx&(1<<e.qbits)-1)<<e.qloc,e.do_pos&&(n+=e.ptab[Math.min(t.p,1023)]<<e.ploc),e.do_delta&&(n+=e.dtab[Math.min(t.delta,255)]<<e.dloc,t.delta+=t.prevq!=r?1:0,t.prevq=r),e.do_sel&&(n+=t.s<<e.sloc),t.p--,65535&n}function m(e){const t={};t.context=e.ReadUint16(),t.pflags=e.ReadByte(),t.do_dedup=t.pflags&o,t.fixed_len=t.pflags&c,t.do_sel=t.pflags&d,t.do_qmap=t.pflags&l,t.do_pos=t.pflags&f,t.do_delta=t.pflags&g,t.do_qtab=t.pflags&h,t.max_sym=e.ReadByte();let r=e.ReadByte();if(t.qbits=r>>4,t.qshift=15&r,r=e.ReadByte(),t.qloc=r>>4,t.sloc=15&r,r=e.ReadByte(),t.ploc=r>>4,t.dloc=15&r,t.qmap=new Array(256),t.pflags&l)for(var n=0;n<t.max_sym;n++)t.qmap[n]=e.ReadByte();else for(n=0;n<256;n++)t.qmap[n]=n;if(t.qtab=new Array(1024),t.qbits>0&&t.pflags&h)s(e,t.qtab,256);else for(n=0;n<256;n++)t.qtab[n]=n;return t.ptab=new Array(1024),t.pflags&f&&s(e,t.ptab,1024),t.dtab=new Array(256),t.pflags&g&&s(e,t.dtab,256),t}function B(e,t,r,n,i,a){i.s=r.max_sel>0?n.sel.ModelDecode(e,t):0,i.x=r.stab[i.s];const A=r.params[i.x];if(A.fixed_len>=0){var s=n.len[0].ModelDecode(e,t);s|=n.len[1].ModelDecode(e,t)<<8,s|=n.len[2].ModelDecode(e,t)<<16,s|=n.len[3].ModelDecode(e,t)<<24,A.fixed_len>0&&(A.fixed_len=-s);}else s=-A.fixed_len;i.len=s,r.do_rev&&(a[i.rec]=n.rev.ModelDecode(e,t)),i.is_dup=0,A.pflags&o&&n.dup.ModelDecode(e,t)&&(i.is_dup=1),i.p=s,i.delta=0,i.qctx=0,i.prevq=0,i.rec++;}},9151:function(e,t,r){var n,i=this&&this.__createBinding||(Object.create?function(e,t,r,n){void 0===n&&(n=r);var i=Object.getOwnPropertyDescriptor(t,r);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,n,i);}:function(e,t,r,n){void 0===n&&(n=r),e[n]=t[r];}),a=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t});}:function(e,t){e.default=t;}),A=this&&this.__importStar||(n=function(e){return n=Object.getOwnPropertyNames||function(e){var t=[];for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[t.length]=r);return t},n(e)},function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r=n(e),A=0;A<r.length;A++)"default"!==r[A]&&i(t,e,r[A]);return a(t,e),t}),s=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.r4x8_uncompress=function(e){return d.decode(e)},t.r4x16_uncompress=function(e){return l.decode(e)},t.arith_uncompress=function(e){return (new o.default).decode(e)},t.fqzcomp_uncompress=function(e){return c.decode(e,[])},t.tok3_uncompress=function(e){const t=f.decode(e,0,"\0");return Uint8Array.from(Array.from(t).map((e=>e.charCodeAt(0))))};const o=s(r(2884)),c=A(r(4373)),d=A(r(9725)),l=A(r(6194)),f=A(r(680));},2935:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.default=class{constructor(e,t=0,r=0){0!==r?(this.buf=new Uint8Array(r),this.length=r):(this.buf=e,this.length=e.length),this.dataView=new DataView(this.buf.buffer),this.pos=t;}EOF(){return this.pos>=this.length}ReadData(e){const t=this.buf.slice(this.pos,this.pos+e);return this.pos+=e,t}ReadByte(){const e=this.buf[this.pos];return this.pos++,e}ReadChar(){const e=this.buf[this.pos];return this.pos++,String.fromCharCode(e)}ReadUint16(){let e=this.ReadByte();return e|=this.ReadByte()<<8,e}ReadUint32(){const e=this.dataView.getInt32(this.pos,!0);return this.pos+=4,e}ReadString(){let e,t="";do{e=this.buf[this.pos++],e&&(t+=String.fromCharCode(e));}while(e);return t}ReadUint7(){let e,t=0;do{e=this.ReadByte(),t=t<<7|127&e;}while(128&e);return t}ReadITF8(){let e=this.buf[this.pos];return this.pos++,e>=240?(e=(15&e)<<28,e+=(this.buf[this.pos+0]<<20)+(this.buf[this.pos+1]<<12)+(this.buf[this.pos+2]<<4)+(this.buf[this.pos+3]>>4),this.pos+=4):e>=224?(e=(15&e)<<24,e+=(this.buf[this.pos+0]<<16)+(this.buf[this.pos+1]<<8)+(0|this.buf[this.pos+2]),this.pos+=3):e>=192?(e=(31&e)<<16,e+=(this.buf[this.pos+0]<<8)+(0|this.buf[this.pos+1]),this.pos+=2):e>=128&&(e=(63&e)<<8,e+=this.buf[this.pos],this.pos++),e}};},9725:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.decode=function(e){const t=new i.default(e),r=t.ReadByte(),n=(t.ReadUint32(),t.ReadUint32());return 0==r?function(e,t){const r=new Array(256),n=new Array(256);d(e,r,n);const i=s(n),A=new Array(4);for(var l=0;l<4;l++)A[l]=e.ReadUint32();const f=new Uint8Array(t);for(l=0;l<t;l++){const t=l%4,s=i[a(A[t])];f[l]=s,A[t]=o(A[t],n[s],r[s]),A[t]=c(e,A[t]);}return f}(t,n):function(e,t){const r=new Array(256),n=new Array(256);!function(e,t,r){for(let e=0;e<256;e++){t[e]=new Array(256),r[e]=new Array(256);for(let r=0;r<256;r++)t[e][r]=0;}let n=e.ReadByte(),i=n,a=0;do{d(e,t[n],r[n]),a>0?(a--,n++):(n=e.ReadByte(),n==i+1&&(a=e.ReadByte())),i=n;}while(0!=n)}(e,r,n);const i=new Array(256);for(var l=0;l<256;l++)i[l]=s(n[l]);const f=new Array(4),g=new Array(4);for(var h=0;h<4;h++)f[h]=e.ReadUint32(),g[h]=0;const u=new Uint8Array(t),C=Math.floor(t/4);for(l=0;l<C;l++)for(h=0;h<4;h++){var I=a(f[h]),_=i[g[h]][I];u[l+h*C]=_,f[h]=o(f[h],n[g[h]][_],r[g[h]][_]),f[h]=c(e,f[h]),g[h]=_;}for(l*=4;l<t;)I=a(f[3]),_=A(n[g[3]],I),u[l++]=_,f[3]=o(f[3],n[g[3]][_],r[g[3]][_]),f[3]=c(e,f[3]),g[3]=_;return u}(t,n)};const i=n(r(2935));function a(e){return 4095&e}function A(e,t){let r=0;for(;t>=e[r+1];)r++;return r}function s(e){const t=new Array(4096);let r=0;for(let n=0;n<4096;n++){for(;n>=e[r+1];)r++;t[n]=r;}return t}function o(e,t,r){return r*(e>>12)+(4095&e)-t}function c(e,t){for(;t<1<<23;)t=(t<<8)+e.ReadByte();return t}function d(e,t,r){for(var n=0;n<256;n++)t[n]=0;let i=e.ReadByte(),a=i,A=0;do{const r=e.ReadITF8();t[i]=r,A>0?(A--,i++):(i=e.ReadByte(),i==a+1&&(A=e.ReadByte())),a=i;}while(0!=i);for(r[0]=0,n=0;n<=255;n++)r[n+1]=r[n]+t[n];}},6194:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.decode=function(e){return d(new i.default(e),0)};const i=n(r(2935));function a(e,t){return e&(1<<t)-1}function A(e,t){let r=0;for(;t>=e[r+1];)r++;return r}function s(e,t){const r=1<<t,n=new Array(r);let i=0;for(let t=0;t<r;t++){for(;t>=e[i+1];)i++;n[t]=i;}return n}function o(e,t,r,n){return r*(e>>n)+(e&(1<<n)-1)-t}function c(e,t){return t<32768&&(t=(t<<16)+e.ReadUint16()),t}function d(e,t){const r=e.ReadByte(),n=1&r,h=8&r,u=32&r,C=64&r,I=128&r,_=4&r?32:4;if(16&r||(t=e.ReadUint7()),h)return function(e,t){const r=e.ReadByte(),n=new Array(r),i=new Array(r);for(var a=0;a<r;a++)n[a]=e.ReadUint7();const A=new Array(r);for(a=0;a<r;a++)i[a]=Math.floor(t/r)+(t%r>a),A[a]=d(e,i[a]);const s=new Uint8Array(t);for(a=0;a<r;a++)for(let e=0;e<i[a];e++)s[e*r+a]=A[a][e];return s}(e,t);if(I)var m=t,[B,p,t]=function(e){const t=e.ReadByte(),r=new Array(t);for(let n=0;n<t;n++)r[n]=e.ReadByte();return [r,t,e.ReadUint7()]}(e);if(C)var w=t,[E,Q,t]=function(e,t){const r=e.ReadUint7(),n=e.ReadUint7();if(1&r)var a=e.ReadData((r-1)/2);else {const n=e.ReadUint7();a=e.ReadData(n),a=f(new i.default(a),r/2,t);}a=new i.default(a);const A=new Array(256);let s=a.ReadByte();0==s&&(s=256);for(let e=0;e<s;e++)A[a.ReadByte()]=1;return [A,a,n]}(e,_);if(u)var y=e.ReadData(t);else y=0==n?f(e,t,_):function(e,t,r){const n=(h=e.ReadByte())>>4;var d=e;if(1&h){const t=e.ReadUint7(),r=e.ReadUint7();var h=new i.default(e.ReadData(r));d=new i.default(f(h,t,4));}const u=new Array(256),C=new Array(256);!function(e,t,r,n){for(var i=0;i<256;i++){t[i]=new Array(256),r[i]=new Array(256);for(var a=0;a<256;a++)t[i][a]=0;}const A=l(e);for(i=0;i<256;i++){if(!A[i])continue;let s=0;for(a=0;a<256;a++)A[a]&&(s>0?s--:(t[i][a]=e.ReadUint7(),0==t[i][a]&&(s=e.ReadByte())));for(g(t[i],n),r[i][0]=0,a=0;a<256;a++)r[i][a+1]=r[i][a]+t[i][a];}}(d,u,C,n);const I=new Array(256);for(var _=0;_<256;_++)I[_]=s(C[_],n);const m=new Array(r),B=new Array(r);for(var p=0;p<r;p++)m[p]=e.ReadUint32(),B[p]=0;const w=new Uint8Array(t),E=Math.floor(t/r);for(_=0;_<E;_++)for(p=0;p<r;p++){var Q=a(m[p],n),y=I[B[p]][Q];w[_+p*E]=y,m[p]=o(m[p],C[B[p]][y],u[B[p]][y],n),m[p]=c(e,m[p]),B[p]=y;}for(_*=r;_<t;)Q=a(m[r-1],n),y=A(C[B[r-1]],Q),w[_++]=y,m[r-1]=o(m[r-1],C[B[r-1]][y],u[B[r-1]][y],n),m[r-1]=c(e,m[r-1]),B[r-1]=y;return w}(e,t,_);return C&&(y=function(e,t,r,n){new i.default(e);const a=new Uint8Array(n);let A=0;for(let i=0;A<n;i++){const n=e[i];if(t[n]){const e=r.ReadUint7();for(let t=0;t<=e;t++)a[A++]=n;}else a[A++]=n;}return a}(y,E,Q,w)),I&&(y=function(e,t,r,n){const i=new Uint8Array(n);let a=0;if(r<=1)for(var A=0;A<n;A++)i[A]=t[0];else if(r<=2)for(A=0;A<n;A++){if(A%8==0)var s=e[a++];i[A]=t[1&s],s>>=1;}else if(r<=4)for(A=0;A<n;A++)A%4==0&&(s=e[a++]),i[A]=t[3&s],s>>=2;else if(r<=16)for(A=0;A<n;A++)A%2==0&&(s=e[a++]),i[A]=t[15&s],s>>=4;return i}(y,B,p,m)),y}function l(e){const t=new Array(256);for(let e=0;e<256;e++)t[e]=0;let r=0,n=e.ReadByte(),i=n;do{t[n]=1,r>0?(r--,n++):(n=e.ReadByte(),n==i+1&&(r=e.ReadByte())),i=n;}while(0!=n);return t}function f(e,t,r){const n=new Array(256),i=new Array(256);!function(e,t,r){for(var n=0;n<256;n++)t[n]=0;const i=l(e);for(n=0;n<256;n++)i[n]>0&&(t[n]=e.ReadUint7());for(g(t,12),r[0]=0,n=0;n<=255;n++)r[n+1]=r[n]+t[n];}(e,n,i);const A=s(i,12),d=new Array(r);for(var f=0;f<r;f++)d[f]=e.ReadUint32();const h=new Uint8Array(t);for(f=0;f<t;f++){const t=f&r-1,s=A[a(d[t],12)];h[f]=s,d[t]=o(d[t],i[s],n[s],12),d[t]=c(e,d[t]);}return h}function g(e,t){let r=0;for(var n=0;n<256;n++)r+=e[n];if(0==r||r==1<<t)return;let i=0;for(;r<1<<t;)r*=2,i++;for(n=0;n<256;n++)e[n]<<=i;}},680:function(e,t,r){var n,i=this&&this.__createBinding||(Object.create?function(e,t,r,n){void 0===n&&(n=r);var i=Object.getOwnPropertyDescriptor(t,r);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,n,i);}:function(e,t,r,n){void 0===n&&(n=r),e[n]=t[r];}),a=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t});}:function(e,t){e.default=t;}),A=this&&this.__importStar||(n=function(e){return n=Object.getOwnPropertyNames||function(e){var t=[];for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[t.length]=r);return t},n(e)},function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r=n(e),A=0;A<r.length;A++)"default"!==r[A]&&i(t,e,r[A]);return a(t,e),t}),s=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.decode=function(e,t,r){(e=new d.default(e)).ReadUint32();const n=e.ReadUint32(),i=e.ReadByte(),a=function(e,t,r,n){let i=-1;const a=new Array(256);for(;!e.EOF();){const t=e.ReadByte(),A=128&t,s=64&t,c=63&t;if(A&&(i++,a[i]=new Array(13)),c!=g&&A){const e=new Array(n-1).fill(w);a[i][g]=new d.default((0, o.concatUint8Array)([new Uint8Array(c),e]));}if(s){const t=e.ReadByte(),r=e.ReadByte();a[i][c]=new d.default(a[t][r].buf);}else {const t=e.ReadUint7(),n=e.ReadData(t);a[i][c]=r?f.decode(n):l.decode(n),a[i][c]=new d.default(a[i][c]);}}return a}(e,0,i,n),A=new Array(n),s=new Array(n);let c="";void 0===r&&(r="\n");for(let e=0;e<n;e++)c+=y(a,A,s,e)+r;return c};const o=r(3472),c=s(r(2884)),d=s(r(2935)),l=A(r(6194)),f=new c.default,g=0,h=1,u=2,C=3,I=4,_=5,m=7,B=8,p=9,w=10,E=12;function Q(e,t){let r=e+"";for(;r.length<t;)r="0"+r;return r}function y(e,t,r,n){let i=e[0][g].ReadByte();const a=n-e[0][i].ReadUint32();if(i==_)return t[n]=t[a],r[n]=r[a],t[n];let A=1;t[n]="",r[n]=new Array(256);do{switch(i=e[A][g].ReadByte(),i){case u:r[n][A]=e[A][u].ReadChar();break;case h:r[n][A]=e[A][h].ReadString();break;case m:r[n][A]=e[A][m].ReadUint32();break;case C:var s=e[A][C].ReadUint32(),o=e[A][I].ReadByte();r[n][A]=Q(s,o);break;case B:r[n][A]=(0|r[a][A])+e[A][B].ReadByte();break;case p:s=(0|r[a][A])+e[A][p].ReadByte(),o=r[a][A].length,r[n][A]=Q(s,o);break;case w:r[n][A]=r[a][A];break;default:r[n][A]="";}t[n]+=r[n][A++];}while(i!=E);return t[n]}},7806:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.IndexedCramFile=t.CraiIndex=t.CramFile=t.CramRecord=void 0;var i=r(7390);Object.defineProperty(t,"CramRecord",{enumerable:!0,get:function(){return i.CramRecord}}),Object.defineProperty(t,"CramFile",{enumerable:!0,get:function(){return n(i).default}});var a=r(4923);Object.defineProperty(t,"CraiIndex",{enumerable:!0,get:function(){return n(a).default}});var A=r(6014);Object.defineProperty(t,"IndexedCramFile",{enumerable:!0,get:function(){return n(A).default}});},6014:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const i=n(r(7390)),a=r(9747);t.default=class{constructor(e){if(this.cram=e.cram??new i.default({url:e.cramUrl,path:e.cramPath,filehandle:e.cramFilehandle,seqFetch:e.seqFetch,checkSequenceMD5:e.checkSequenceMD5,cacheSize:e.cacheSize}),!(this.cram instanceof i.default))throw new Error("invalid arguments: no cramfile");this.index=e.index;}async getRecordsForRange(e,t,r,n={}){if(n.viewAsPairs=n.viewAsPairs||!1,n.pairAcrossChr=n.pairAcrossChr||!1,n.maxInsertSize=n.maxInsertSize||2e5,"string"==typeof e)throw new a.CramUnimplementedError("string sequence names not yet supported");const i=e,A=await this.index.getEntriesForRange(i,t,r),s=await Promise.all(A.map((n=>this.getRecordsInSlice(n,(n=>n.sequenceId===e&&n.alignmentStart<=r&&void 0!==n.lengthOnRef&&n.alignmentStart+n.lengthOnRef-1>=t)))));let o=Array.prototype.concat(...s);if(n.viewAsPairs){const e={},t={};for(const r of o){const n=r.readName;if(void 0===n)throw new Error("readName undefined");const i=r.uniqueId;e[n]||(e[n]=0),e[n]+=1,t[i]=1;}const r={};Object.entries(e).forEach((([e,t])=>{1===t&&(r[e]=!0);}));const a=[];for(const e of o){const t=e.readName;if(void 0===t)throw new Error("readName undefined");if(r[t]&&e.mate&&(e.mate.sequenceId===i||n.pairAcrossChr)&&Math.abs(e.alignmentStart-e.mate.alignmentStart)<n.maxInsertSize){const t=this.index.getEntriesForRange(e.mate.sequenceId,e.mate.alignmentStart,e.mate.alignmentStart+1);a.push(t);}}const A=await Promise.all(a);let s=[];for(const e of A)s.push(...e);s=s.sort(((e,t)=>e.toString().localeCompare(t.toString()))).filter(((e,t,r)=>!t||e.toString()!==r[t-1].toString()));const d=[];for(const e of s){let n=this.cram.featureCache.get(e.toString());n||(n=this.getRecordsInSlice(e,(()=>!0)),this.cram.featureCache.set(e.toString(),n));const i=n.then((e=>{const n=[];for(const i of e){if(void 0===i.readName)throw new Error("readName undefined");r[i.readName]&&!t[i.uniqueId]&&n.push(i);}return n}));d.push(i);}const l=await Promise.all(d);if(l.length){const e=l.reduce(((e,t)=>e.concat(t)));o=o.concat(e);}}return o}getRecordsInSlice({containerStart:e,sliceStart:t,sliceBytes:r},n){return this.cram.getContainerAtPosition(e).getSlice(t,r).getRecords(n)}hasDataForReferenceSequence(e){return this.index.hasDataForReferenceSequence(e)}};},5931:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.RemoteFile=t.LocalFile=void 0,t.open=function(e,t,r){if(r)return r;if(e)return new n.RemoteFile(e);if(t)return new n.LocalFile(t);throw new Error("no url, path, or filehandle provided, cannot open")};const n=r(689);var i=r(689);Object.defineProperty(t,"LocalFile",{enumerable:!0,get:function(){return i.LocalFile}}),Object.defineProperty(t,"RemoteFile",{enumerable:!0,get:function(){return i.RemoteFile}});},6476:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.TOTFREQ=t.TF_SHIFT=t.RANS_BYTE_L=void 0,t.TF_SHIFT=12,t.TOTFREQ=4096,t.RANS_BYTE_L=1<<23;},997:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t,r,n){let s=e.getInt(),o=e.getInt(),c=e.getInt(),d=e.getInt();const l=n.remaining(),f=-4&l;for(let i=0;i<f;i+=4){const l=t.R[A.default.get(s,a.TF_SHIFT)],f=t.R[A.default.get(o,a.TF_SHIFT)],g=t.R[A.default.get(c,a.TF_SHIFT)],h=t.R[A.default.get(d,a.TF_SHIFT)];n.putAt(i,l),n.putAt(i+1,f),n.putAt(i+2,g),n.putAt(i+3,h),s=A.default.advanceSymbolStep(s,r[255&l],a.TF_SHIFT),o=A.default.advanceSymbolStep(o,r[255&f],a.TF_SHIFT),c=A.default.advanceSymbolStep(c,r[255&g],a.TF_SHIFT),d=A.default.advanceSymbolStep(d,r[255&h],a.TF_SHIFT),s=A.default.renormalize(s,e),o=A.default.renormalize(o,e),c=A.default.renormalize(c,e),d=A.default.renormalize(d,e);}let g;switch(n.setPosition(f),3&l){case 0:break;case 1:g=t.R[A.default.get(s,a.TF_SHIFT)],A.default.advanceSymbol(s,e,r[255&g],a.TF_SHIFT),n.put(g);break;case 2:g=t.R[A.default.get(s,a.TF_SHIFT)],A.default.advanceSymbol(s,e,r[255&g],a.TF_SHIFT),n.put(g),g=t.R[A.default.get(o,a.TF_SHIFT)],A.default.advanceSymbol(o,e,r[255&g],a.TF_SHIFT),n.put(g);break;case 3:g=t.R[A.default.get(s,a.TF_SHIFT)],A.default.advanceSymbol(s,e,r[255&g],a.TF_SHIFT),n.put(g),g=t.R[A.default.get(o,a.TF_SHIFT)],A.default.advanceSymbol(o,e,r[255&g],a.TF_SHIFT),n.put(g),g=t.R[A.default.get(c,a.TF_SHIFT)],A.default.advanceSymbol(c,e,r[255&g],a.TF_SHIFT),n.put(g);break;default:throw new i.CramMalformedError("invalid output size encountered during rANS decoding")}n.setPosition(0);};const i=r(9747),a=r(6476),A=n(r(488));},7960:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t,r,n){const A=t.remaining();let s=e.getInt(),o=e.getInt(),c=e.getInt(),d=e.getInt();const l=A>>2;let f=0,g=l,h=2*l,u=3*l,C=0,I=0,_=0,m=0;for(;f<l;f+=1,g+=1,h+=1,u+=1){const A=255&r[C].R[a.default.get(s,i.TF_SHIFT)],l=255&r[I].R[a.default.get(o,i.TF_SHIFT)],B=255&r[_].R[a.default.get(c,i.TF_SHIFT)],p=255&r[m].R[a.default.get(d,i.TF_SHIFT)];t.putAt(f,A),t.putAt(g,l),t.putAt(h,B),t.putAt(u,p),s=a.default.advanceSymbolStep(s,n[C][A],i.TF_SHIFT),o=a.default.advanceSymbolStep(o,n[I][l],i.TF_SHIFT),c=a.default.advanceSymbolStep(c,n[_][B],i.TF_SHIFT),d=a.default.advanceSymbolStep(d,n[m][p],i.TF_SHIFT),s=a.default.renormalize(s,e),o=a.default.renormalize(o,e),c=a.default.renormalize(c,e),d=a.default.renormalize(d,e),C=A,I=l,_=B,m=p;}for(;u<A;u+=1){const A=255&r[m].R[a.default.get(d,i.TF_SHIFT)];t.putAt(u,A),d=a.default.advanceSymbol(d,e,n[m][A],i.TF_SHIFT),m=A;}};const i=r(6476),a=n(r(488));},488:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:!0});const n=r(9747),i=r(6476);class a{constructor(){this.F=void 0,this.C=void 0;}}function A(e,t,r,n){return r*(e>>n)+(e&(1<<n)-1)-t}t.default={FC:a,AriDecoder:class{constructor(){this.fc=new Array(256);for(let e=0;e<this.fc.length;e+=1)this.fc[e]=new a;this.R=null;}},DecodingSymbol:class{constructor(){this.start=void 0,this.freq=void 0;}},symbolInit:function(e,t,r){if(!(t<=65536))throw new n.CramMalformedError("assertion failed: start <= 1<<16");if(!(r<=65536-t))throw new n.CramMalformedError("assertion failed: freq <= 1<<16");e.start=t,e.freq=r;},advanceStep:A,advanceSymbolStep:function(e,t,r){return A(e,t.start,t.freq,r)},get:function(e,t){return e&(1<<t)-1},advanceSymbol:function(e,t,r,n){return function(e,t,r,n,a){if((e=n*(e>>a)+(e&(1<<a)-1)-r)<i.RANS_BYTE_L)do{e=e<<8|255&t.get();}while(e<i.RANS_BYTE_L);return e}(e,t,r.start,r.freq,n)},renormalize:function(e,t){if(e<i.RANS_BYTE_L)do{e=e<<8|255&t.get();}while(e<i.RANS_BYTE_L);return e}};},1971:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.readStatsO0=function(e,t,r){let n=0,i=0,o=255&e.get();do{null==t.fc[o]&&(t.fc[o]=new A.default.FC),t.fc[o].F=255&e.get(),t.fc[o].F>=128&&(t.fc[o].F&=-129,t.fc[o].F=(127&t.fc[o].F)<<8|255&e.get()),t.fc[o].C=i,A.default.symbolInit(r[o],t.fc[o].C,t.fc[o].F),t.R||(t.R=new Array(a.TOTFREQ)),t.R.fill(o,i,i+t.fc[o].F),i+=t.fc[o].F,0===n&&o+1===(255&e.getByteAt(e.position()))?(o=255&e.get(),n=255&e.get()):0!==n?(n-=1,o+=1):o=255&e.get();}while(0!==o);s(i<a.TOTFREQ);},t.readStatsO1=function(e,t,r){let n=0,i=255&e.get();do{let o=0,c=0,d=255&e.get();null==t[i]&&(t[i]=new A.default.AriDecoder);do{null==t[i].fc[d]&&(t[i].fc[d]=new A.default.FC),t[i].fc[d].F=255&e.get(),t[i].fc[d].F>=128&&(t[i].fc[d].F&=-129,t[i].fc[d].F=(127&t[i].fc[d].F)<<8|255&e.get()),t[i].fc[d].C=c,0===t[i].fc[d].F&&(t[i].fc[d].F=a.TOTFREQ),null==r[i][d]&&(r[i][d]=new A.default.RansDecSymbol),A.default.symbolInit(r[i][d],t[i].fc[d].C,t[i].fc[d].F),null==t[i].R&&(t[i].R=new Array(a.TOTFREQ)),t[i].R.fill(d,c,c+t[i].fc[d].F),c+=t[i].fc[d].F,s(c<=a.TOTFREQ),0===o&&d+1===(255&e.getByteAt(e.position()))?(d=255&e.get(),o=255&e.get()):0!==o?(o-=1,d+=1):d=255&e.get();}while(0!==d);0===n&&i+1===(255&e.getByteAt(e.position()))?(i=255&e.get(),n=255&e.get()):0!==n?(n-=1,i+=1):i=255&e.get();}while(0!==i)};const i=r(9747),a=r(6476),A=n(r(488));function s(e){if(!e)throw new i.CramMalformedError("assertion failed")}},9989:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t,r=0){if(0===e.length)return t.fill(0),t;const n=new d(e,r),l=n.get();if(0!==l&&1!==l)throw new i.CramMalformedError(`Invalid rANS order ${l}`);if(n.getInt()!==n.remaining()-c)throw new i.CramMalformedError("Incorrect input length.");const f=n.getInt(),g=new d(t||new Uint8Array(f));if(g.length<f)throw new i.CramMalformedError(`Output buffer too small to fit ${f} bytes.`);switch(l){case 0:return function(e,t){const r=new s.default.AriDecoder,n=new Array(256);for(let e=0;e<n.length;e+=1)n[e]=new s.default.DecodingSymbol;return (0, o.readStatsO0)(e,r,n),(0, a.default)(e,r,n,t),t}(n,g);case 1:return function(e,t){const r=new Array(256);for(let e=0;e<r.length;e+=1)r[e]=new s.default.AriDecoder;const n=new Array(256);for(let e=0;e<n.length;e+=1){n[e]=new Array(256);for(let t=0;t<n[e].length;t+=1)n[e][t]=new s.default.DecodingSymbol;}return (0, o.readStatsO1)(e,r,n),(0, A.default)(e,t,r,n),t}(n,g);default:throw new i.CramMalformedError(`Invalid rANS order: ${l}`)}};const i=r(9747),a=n(r(997)),A=n(r(7960)),s=n(r(488)),o=r(1971),c=4;class d{constructor(e,t=0){this._buffer=e,this._dataView=new DataView(e.buffer),this._position=t,this.length=e.length;}get(){const e=this._buffer[this._position];return this._position+=1,e}getByte(){return this.get()}getByteAt(e){return this._buffer[e]}position(){return this._position}put(e){return this._buffer[this._position]=e,this._position+=1,e}putAt(e,t){return this._buffer[e]=t,t}setPosition(e){return this._position=e,e}getInt(){const e=this._dataView.getInt32(this._position,!0);return this._position+=4,e}remaining(){return this._buffer.length-this._position}}},5417:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.parseHeaderText=function(e){const t=e.split(/\r?\n/),r=[];for(const e of t){const[t,...n]=e.split(/\t/);t&&r.push({tag:t.slice(1),data:n.map((e=>{const t=e.indexOf(":");return -1!==t?{tag:e.slice(0,t),value:e.slice(t+1)}:{tag:e,value:""}}))});}return r};},2683:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:!0});const n=r(6305),i=[0,1,3,7,15,31,63,127,255];t.default=class{constructor(e){this.stream=e,this.bitOffset=0,this.curByte=0,this.hasByte=!1;}_ensureByte(){this.hasByte||(this.curByte=this.stream.readByte(),this.hasByte=!0);}read(e){let t=0;for(;e>0;){this._ensureByte();const r=8-this.bitOffset;if(e>=r)t<<=r,t|=i[r]&this.curByte,this.hasByte=!1,this.bitOffset=0,e-=r;else {t<<=e;const n=r-e;t|=(this.curByte&i[e]<<n)>>n,this.bitOffset+=e,e=0;}}return t}seek(e){const t=e%8,r=(e-t)/8;this.bitOffset=t,this.stream.seek(r),this.hasByte=!1;}pi(){const e=new Uint8Array(6);for(let t=0;t<e.length;t++)e[t]=this.read(8);return (0, n.toHex)(e)}};},9092:e=>{e.exports=function(){const e=new Uint32Array([0,79764919,159529838,222504665,319059676,398814059,445009330,507990021,638119352,583659535,797628118,726387553,890018660,835552979,1015980042,944750013,1276238704,1221641927,1167319070,1095957929,1595256236,1540665371,1452775106,1381403509,1780037320,1859660671,1671105958,1733955601,2031960084,2111593891,1889500026,1952343757,2552477408,2632100695,2443283854,2506133561,2334638140,2414271883,2191915858,2254759653,3190512472,3135915759,3081330742,3009969537,2905550212,2850959411,2762807018,2691435357,3560074640,3505614887,3719321342,3648080713,3342211916,3287746299,3467911202,3396681109,4063920168,4143685023,4223187782,4286162673,3779000052,3858754371,3904687514,3967668269,881225847,809987520,1023691545,969234094,662832811,591600412,771767749,717299826,311336399,374308984,453813921,533576470,25881363,88864420,134795389,214552010,2023205639,2086057648,1897238633,1976864222,1804852699,1867694188,1645340341,1724971778,1587496639,1516133128,1461550545,1406951526,1302016099,1230646740,1142491917,1087903418,2896545431,2825181984,2770861561,2716262478,3215044683,3143675388,3055782693,3001194130,2326604591,2389456536,2200899649,2280525302,2578013683,2640855108,2418763421,2498394922,3769900519,3832873040,3912640137,3992402750,4088425275,4151408268,4197601365,4277358050,3334271071,3263032808,3476998961,3422541446,3585640067,3514407732,3694837229,3640369242,1762451694,1842216281,1619975040,1682949687,2047383090,2127137669,1938468188,2001449195,1325665622,1271206113,1183200824,1111960463,1543535498,1489069629,1434599652,1363369299,622672798,568075817,748617968,677256519,907627842,853037301,1067152940,995781531,51762726,131386257,177728840,240578815,269590778,349224269,429104020,491947555,4046411278,4126034873,4172115296,4234965207,3794477266,3874110821,3953728444,4016571915,3609705398,3555108353,3735388376,3664026991,3290680682,3236090077,3449943556,3378572211,3174993278,3120533705,3032266256,2961025959,2923101090,2868635157,2813903052,2742672763,2604032198,2683796849,2461293480,2524268063,2284983834,2364738477,2175806836,2238787779,1569362073,1498123566,1409854455,1355396672,1317987909,1246755826,1192025387,1137557660,2072149281,2135122070,1912620623,1992383480,1753615357,1816598090,1627664531,1707420964,295390185,358241886,404320391,483945776,43990325,106832002,186451547,266083308,932423249,861060070,1041341759,986742920,613929101,542559546,756411363,701822548,3316196985,3244833742,3425377559,3370778784,3601682597,3530312978,3744426955,3689838204,3819031489,3881883254,3928223919,4007849240,4037393693,4100235434,4180117107,4259748804,2310601993,2373574846,2151335527,2231098320,2596047829,2659030626,2470359227,2550115596,2947551409,2876312838,2788305887,2733848168,3165939309,3094707162,3040238851,2985771188]);return function(){let t=4294967295;this.getCRC=function(){return ~t>>>0},this.updateCRC=function(r){t=t<<8^e[255&(t>>>24^r)];},this.updateCRCRun=function(r,n){for(;n-- >0;)t=t<<8^e[255&(t>>>24^r)];};}}();},9995:function(e,t,r){var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.decode=function(e,t,r){const n=f(e),i=g(t),a=new l(n,i);for(;!("eof"in n)||!n.eof();)if(a._init_block())a._read_bunzip();else {const e=a.reader.read(32)>>>0;if(e!==a.streamCRC&&d(o.DATA_ERROR,"Bad stream CRC (got "+a.streamCRC.toString(16)+" expected "+e.toString(16)+")"),!r||!("eof"in n)||n.eof())break;a._start_bunzip(n,i);}if("getBuffer"in i)return i.getBuffer()};const i=n(r(2683)),a=n(r(9092)),A=n(r(4271)),s=function(e,t){const r=e[t];for(let r=t;r>0;r--)e[r]=e[r-1];return e[0]=r,r},o={OK:0,LAST_BLOCK:-1,NOT_BZIP_DATA:-2,UNEXPECTED_INPUT_EOF:-3,UNEXPECTED_OUTPUT_EOF:-4,DATA_ERROR:-5,OUT_OF_MEMORY:-6,OBSOLETE_INPUT:-7,END_OF_BLOCK:-8},c={};c[o.LAST_BLOCK]="Bad file checksum",c[o.NOT_BZIP_DATA]="Not bzip data",c[o.UNEXPECTED_INPUT_EOF]="Unexpected input EOF",c[o.UNEXPECTED_OUTPUT_EOF]="Unexpected output EOF",c[o.DATA_ERROR]="Data error",c[o.OUT_OF_MEMORY]="Out of memory",c[o.OBSOLETE_INPUT]="Obsolete (pre 0.9.5) bzip format not supported.";const d=function(e,t){let r=c[e]||"unknown error";t&&(r+=": "+t);const n=new TypeError(r);throw n.errorCode=e,n};class l{constructor(e,t){this.writePos=this.writeCurrent=this.writeCount=0,this._start_bunzip(e,t);}_init_block(){return this._get_next_block()?(this.blockCRC=new a.default,!0):(this.writeCount=-1,!1)}_start_bunzip(e,t){const r=new Uint8Array(4);4===e.read(r,0,4)&&"BZh"===String.fromCharCode(r[0],r[1],r[2])||d(o.NOT_BZIP_DATA,"bad magic");const n=r[3]-48;(n<1||n>9)&&d(o.NOT_BZIP_DATA,"level out of range"),this.reader=new i.default(e),this.dbufSize=1e5*n,this.nextoutput=0,this.outputStream=t,this.streamCRC=0;}_get_next_block(){let e,t,r;const n=this.reader,i=n.pi();if("177245385090"===i)return !1;"314159265359"!==i&&d(o.NOT_BZIP_DATA),this.targetBlockCRC=n.read(32)>>>0,this.streamCRC=(this.targetBlockCRC^(this.streamCRC<<1|this.streamCRC>>>31))>>>0,n.read(1)&&d(o.OBSOLETE_INPUT);const a=n.read(24);a>this.dbufSize&&d(o.DATA_ERROR,"initial position out of bounds");let A=n.read(16),c=new Uint8Array(256),l=0;for(e=0;e<16;e++)if(A&1<<15-e){const i=16*e;for(r=n.read(16),t=0;t<16;t++)r&1<<15-t&&(c[l++]=i+t);}const f=n.read(3);(f<2||f>6)&&d(o.DATA_ERROR);const g=n.read(15);0===g&&d(o.DATA_ERROR);const h=new Uint8Array(256);for(e=0;e<f;e++)h[e]=e;const u=new Uint8Array(g);for(e=0;e<g;e++){for(t=0;n.read(1);t++)t>=f&&d(o.DATA_ERROR);u[e]=s(h,t);}let C,I=l+2,_=[];for(t=0;t<f;t++){const t=new Uint8Array(I),r=new Uint16Array(21);for(A=n.read(5),e=0;e<I;e++){for(;(A<1||A>20)&&d(o.DATA_ERROR),n.read(1);)n.read(1)?A--:A++;t[e]=A;}var m,B;for(m=B=t[0],e=1;e<I;e++)t[e]>B?B=t[e]:t[e]<m&&(m=t[e]);C={},_.push(C),C.permute=new Uint16Array(258),C.limit=new Uint32Array(22),C.base=new Uint32Array(21),C.minLen=m,C.maxLen=B;let i=0;for(e=m;e<=B;e++)for(r[e]=C.limit[e]=0,A=0;A<I;A++)t[A]===e&&(C.permute[i++]=A);for(e=0;e<I;e++)r[t[e]]++;for(i=A=0,e=m;e<B;e++)i+=r[e],C.limit[e]=i-1,i<<=1,A+=r[e],C.base[e+1]=i-A;C.limit[B+1]=Number.MAX_VALUE,C.limit[B]=i+r[B]-1,C.base[m]=0;}const p=new Uint32Array(256);for(e=0;e<256;e++)h[e]=e;let w,E=0,Q=0,y=0;const b=this.dbuf=new Uint32Array(this.dbufSize);for(I=0;;){for(I--||(I=49,y>=g&&d(o.DATA_ERROR),C=_[u[y++]]),e=C.minLen,t=n.read(e);e>C.maxLen&&d(o.DATA_ERROR),!(t<=C.limit[e]);e++)t=t<<1|n.read(1);t-=C.base[e],(t<0||t>=258)&&d(o.DATA_ERROR);const r=C.permute[t];if(0!==r&&1!==r){if(E)for(E=0,Q+A>this.dbufSize&&d(o.DATA_ERROR),w=c[h[0]],p[w]+=A;A--;)b[Q++]=w;if(r>l)break;Q>=this.dbufSize&&d(o.DATA_ERROR),e=r-1,w=s(h,e),w=c[w],p[w]++,b[Q++]=w;}else E||(E=1,A=0),A+=0===r?E:2*E,E<<=1;}for((a<0||a>=Q)&&d(o.DATA_ERROR),t=0,e=0;e<256;e++)r=t+p[e],p[e]=t,t=r;for(e=0;e<Q;e++)w=255&b[e],b[p[w]]|=e<<8,p[w]++;let S=0,D=0,k=0;return Q&&(S=b[a],D=255&S,S>>=8,k=-1),this.writePos=S,this.writeCurrent=D,this.writeCount=Q,this.writeRun=k,!0}_read_bunzip(e,t){let r,n,i;if(this.writeCount<0)return 0;let a=this.dbuf,A=this.writePos,s=this.writeCurrent,c=this.writeCount,l=(this.outputsize,this.writeRun);for(;c;){for(c--,n=s,A=a[A],s=255&A,A>>=8,3==l++?(r=s,i=n,s=-1):(r=1,i=s),this.blockCRC.updateCRCRun(i,r);r--;)this.outputStream.writeByte(i),this.nextoutput++;s!=n&&(l=0);}return this.writeCount=c,this.blockCRC.getCRC()!==this.targetBlockCRC&&d(o.DATA_ERROR,"Bad block CRC (got "+this.blockCRC.getCRC().toString(16)+" expected "+this.targetBlockCRC.toString(16)+")"),this.nextoutput}}const f=function(e){if("readByte"in e)return e;const t=new A.default;return t.pos=0,t.readByte=function(){return e[this.pos++]},t.seek=function(e){this.pos=e;},t.eof=function(){return this.pos>=e.length},t},g=function(e){const t=new A.default;let r=!0;if(e)if("number"==typeof e)t.buffer=new Uint8Array(e),r=!1;else {if("writeByte"in e)return e;t.buffer=e,r=!1;}else t.buffer=new Uint8Array(16384);return t.pos=0,t.writeByte=function(e){if(r&&this.pos>=this.buffer.length){const e=new Uint8Array(2*this.buffer.length);e.set(this.buffer),this.buffer=e;}this.buffer[this.pos++]=e;},t.getBuffer=function(){if(this.pos!==this.buffer.length){if(!r)throw new TypeError("outputsize does not match decoded input");const e=new Uint8Array(this.pos);e.set(this.buffer.slice(0,this.pos)),this.buffer=e;}return this.buffer},t._coerced=!0,t};t.default=l;},4271:e=>{const t=function(){};t.prototype.readByte=function(){throw new Error("abstract method readByte() not implemented")},t.prototype.read=function(e,t,r){let n=0;for(;n<r;){const r=this.readByte();if(r<0)return 0===n?-1:n;e[t++]=r,n++;}return n},t.prototype.seek=function(e){throw new Error("abstract method seek() not implemented")},t.prototype.writeByte=function(e){throw new Error("abstract method readByte() not implemented")},t.prototype.write=function(e,t,r){let n;for(n=0;n<r;n++)this.writeByte(e[t++]);return r},t.prototype.flush=function(){},e.exports=t;},6305:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.toHex=function(e){let t="";for(let r=0,i=e.length;r<i;r++)t+=n[e[r]];return t};const r=["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"],n=new Array(256);for(let e=0;e<256;e++)n[e]=`${r[e>>>4&15]}${r[15&e]}`;},8876:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.unzip=function(e){return (0, n.inflate)(e)};const n=r(1668);},3472:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.concatUint8Array=function(e){const t=new Uint8Array(function(e){let t=0;for(const r of e)t+=r.length;return t}(e));let r=0;for(const n of e)t.set(n,r),r+=n.length;return t};},2151:e=>{var t={utf8:{stringToBytes:function(e){return t.bin.stringToBytes(unescape(encodeURIComponent(e)))},bytesToString:function(e){return decodeURIComponent(escape(t.bin.bytesToString(e)))}},bin:{stringToBytes:function(e){for(var t=[],r=0;r<e.length;r++)t.push(255&e.charCodeAt(r));return t},bytesToString:function(e){for(var t=[],r=0;r<e.length;r++)t.push(String.fromCharCode(e[r]));return t.join("")}}};e.exports=t;},3939:e=>{var t,r;t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r={rotl:function(e,t){return e<<t|e>>>32-t},rotr:function(e,t){return e<<32-t|e>>>t},endian:function(e){if(e.constructor==Number)return 16711935&r.rotl(e,8)|4278255360&r.rotl(e,24);for(var t=0;t<e.length;t++)e[t]=r.endian(e[t]);return e},randomBytes:function(e){for(var t=[];e>0;e--)t.push(Math.floor(256*Math.random()));return t},bytesToWords:function(e){for(var t=[],r=0,n=0;r<e.length;r++,n+=8)t[n>>>5]|=e[r]<<24-n%32;return t},wordsToBytes:function(e){for(var t=[],r=0;r<32*e.length;r+=8)t.push(e[r>>>5]>>>24-r%32&255);return t},bytesToHex:function(e){for(var t=[],r=0;r<e.length;r++)t.push((e[r]>>>4).toString(16)),t.push((15&e[r]).toString(16));return t.join("")},hexToBytes:function(e){for(var t=[],r=0;r<e.length;r+=2)t.push(parseInt(e.substr(r,2),16));return t},bytesToBase64:function(e){for(var r=[],n=0;n<e.length;n+=3)for(var i=e[n]<<16|e[n+1]<<8|e[n+2],a=0;a<4;a++)8*n+6*a<=8*e.length?r.push(t.charAt(i>>>6*(3-a)&63)):r.push("=");return r.join("")},base64ToBytes:function(e){e=e.replace(/[^A-Z0-9+\/]/gi,"");for(var r=[],n=0,i=0;n<e.length;i=++n%4)0!=i&&r.push((t.indexOf(e.charAt(n-1))&Math.pow(2,-2*i+8)-1)<<2*i|t.indexOf(e.charAt(n))>>>6-2*i);return r}},e.exports=r;},689:(e,t,r)=>{r.r(t),r.d(t,{BlobFile:()=>s,LocalFile:()=>i(),RemoteFile:()=>A,fromUrl:()=>o,open:()=>c});var n=r(7937),i=r.n(n);function a(e){return ("object"==typeof e&&null!==e&&"message"in e?e.message:`${e}`).replace(/\.$/,"")}class A{constructor(e,t={}){this.baseOverrides={},this.url=e;const r=t.fetch||globalThis.fetch.bind(globalThis);t.overrides&&(this.baseOverrides=t.overrides),this.fetchImplementation=r;}async fetch(e,t){let r;try{r=await this.fetchImplementation(e,t);}catch(n){if(!`${n}`.includes("Failed to fetch"))throw new Error(`${a(n)} fetching ${e}`,{cause:n});console.warn(`generic-filehandle: refetching ${e} to attempt to work around chrome CORS header caching bug`);try{r=await this.fetchImplementation(e,{...t,cache:"reload"});}catch(t){throw new Error(`${a(t)} fetching ${e}`,{cause:t})}}return r}async read(e,t,r={}){const{headers:n={},signal:i,overrides:a={}}=r;e<1/0?n.range=`bytes=${t}-${t+e}`:e===1/0&&0!==t&&(n.range=`bytes=${t}-`);const A=await this.fetch(this.url,{...this.baseOverrides,...a,headers:{...n,...a.headers,...this.baseOverrides.headers},method:"GET",redirect:"follow",mode:"cors",signal:i});if(!A.ok)throw new Error(`HTTP ${A.status} fetching ${this.url}`);if(200===A.status&&0===t||206===A.status){const t=await A.arrayBuffer(),r=A.headers.get("content-range"),n=/\/(\d+)$/.exec(r||"");return n?.[1]&&(this._stat={size:parseInt(n[1],10)}),new Uint8Array(t.slice(0,e))}throw 200===A.status?new Error(`${this.url} fetch returned status 200, expected 206`):new Error(`HTTP ${A.status} fetching ${this.url}`)}async readFile(e={}){let t,r;"string"==typeof e?(t=e,r={}):(t=e.encoding,r=e,delete r.encoding);const{headers:n={},signal:i,overrides:a={}}=r,A=await this.fetch(this.url,{headers:n,method:"GET",redirect:"follow",mode:"cors",signal:i,...this.baseOverrides,...a});if(200!==A.status)throw new Error(`HTTP ${A.status} fetching ${this.url}`);if("utf8"===t)return A.text();if(t)throw new Error(`unsupported encoding: ${t}`);return new Uint8Array(await A.arrayBuffer())}async stat(){if(!this._stat&&(await this.read(10,0),!this._stat))throw new Error(`unable to determine size of file at ${this.url}`);return this._stat}async close(){}}class s{constructor(e){this.blob=e,this.size=e.size;}async read(e,t=0){if(!e)return new Uint8Array(0);const r=t,n=r+e;return new Uint8Array(await this.blob.slice(r,n).arrayBuffer())}async readFile(e){const t="string"==typeof e?e:e?.encoding;if("utf8"===t)return this.blob.text();if(t)throw new Error(`unsupported encoding: ${t}`);return new Uint8Array(await this.blob.arrayBuffer())}async stat(){return {size:this.size}}async close(){}}function o(e,t={}){return new A(e,t)}function c(e,t,r,n={}){if(void 0!==r)return r;if(void 0!==e)return o(e,n);if(void 0!==t)return new(i())(t,n);throw new Error("no url, path, or filehandle provided, cannot open")}},3503:(e,t,r)=>{var n,i,a,A,s;n=r(3939),i=r(2151).utf8,a=r(652),A=r(2151).bin,(s=function(e,t){e.constructor==String?e=t&&"binary"===t.encoding?A.stringToBytes(e):i.stringToBytes(e):a(e)?e=Array.prototype.slice.call(e,0):Array.isArray(e)||e.constructor===Uint8Array||(e=e.toString());for(var r=n.bytesToWords(e),o=8*e.length,c=1732584193,d=-271733879,l=-1732584194,f=271733878,g=0;g<r.length;g++)r[g]=16711935&(r[g]<<8|r[g]>>>24)|4278255360&(r[g]<<24|r[g]>>>8);r[o>>>5]|=128<<o%32,r[14+(o+64>>>9<<4)]=o;var h=s._ff,u=s._gg,C=s._hh,I=s._ii;for(g=0;g<r.length;g+=16){var _=c,m=d,B=l,p=f;c=h(c,d,l,f,r[g+0],7,-680876936),f=h(f,c,d,l,r[g+1],12,-389564586),l=h(l,f,c,d,r[g+2],17,606105819),d=h(d,l,f,c,r[g+3],22,-1044525330),c=h(c,d,l,f,r[g+4],7,-176418897),f=h(f,c,d,l,r[g+5],12,1200080426),l=h(l,f,c,d,r[g+6],17,-1473231341),d=h(d,l,f,c,r[g+7],22,-45705983),c=h(c,d,l,f,r[g+8],7,1770035416),f=h(f,c,d,l,r[g+9],12,-1958414417),l=h(l,f,c,d,r[g+10],17,-42063),d=h(d,l,f,c,r[g+11],22,-1990404162),c=h(c,d,l,f,r[g+12],7,1804603682),f=h(f,c,d,l,r[g+13],12,-40341101),l=h(l,f,c,d,r[g+14],17,-1502002290),c=u(c,d=h(d,l,f,c,r[g+15],22,1236535329),l,f,r[g+1],5,-165796510),f=u(f,c,d,l,r[g+6],9,-1069501632),l=u(l,f,c,d,r[g+11],14,643717713),d=u(d,l,f,c,r[g+0],20,-373897302),c=u(c,d,l,f,r[g+5],5,-701558691),f=u(f,c,d,l,r[g+10],9,38016083),l=u(l,f,c,d,r[g+15],14,-660478335),d=u(d,l,f,c,r[g+4],20,-405537848),c=u(c,d,l,f,r[g+9],5,568446438),f=u(f,c,d,l,r[g+14],9,-1019803690),l=u(l,f,c,d,r[g+3],14,-187363961),d=u(d,l,f,c,r[g+8],20,1163531501),c=u(c,d,l,f,r[g+13],5,-1444681467),f=u(f,c,d,l,r[g+2],9,-51403784),l=u(l,f,c,d,r[g+7],14,1735328473),c=C(c,d=u(d,l,f,c,r[g+12],20,-1926607734),l,f,r[g+5],4,-378558),f=C(f,c,d,l,r[g+8],11,-2022574463),l=C(l,f,c,d,r[g+11],16,1839030562),d=C(d,l,f,c,r[g+14],23,-35309556),c=C(c,d,l,f,r[g+1],4,-1530992060),f=C(f,c,d,l,r[g+4],11,1272893353),l=C(l,f,c,d,r[g+7],16,-155497632),d=C(d,l,f,c,r[g+10],23,-1094730640),c=C(c,d,l,f,r[g+13],4,681279174),f=C(f,c,d,l,r[g+0],11,-358537222),l=C(l,f,c,d,r[g+3],16,-722521979),d=C(d,l,f,c,r[g+6],23,76029189),c=C(c,d,l,f,r[g+9],4,-640364487),f=C(f,c,d,l,r[g+12],11,-421815835),l=C(l,f,c,d,r[g+15],16,530742520),c=I(c,d=C(d,l,f,c,r[g+2],23,-995338651),l,f,r[g+0],6,-198630844),f=I(f,c,d,l,r[g+7],10,1126891415),l=I(l,f,c,d,r[g+14],15,-1416354905),d=I(d,l,f,c,r[g+5],21,-57434055),c=I(c,d,l,f,r[g+12],6,1700485571),f=I(f,c,d,l,r[g+3],10,-1894986606),l=I(l,f,c,d,r[g+10],15,-1051523),d=I(d,l,f,c,r[g+1],21,-2054922799),c=I(c,d,l,f,r[g+8],6,1873313359),f=I(f,c,d,l,r[g+15],10,-30611744),l=I(l,f,c,d,r[g+6],15,-1560198380),d=I(d,l,f,c,r[g+13],21,1309151649),c=I(c,d,l,f,r[g+4],6,-145523070),f=I(f,c,d,l,r[g+11],10,-1120210379),l=I(l,f,c,d,r[g+2],15,718787259),d=I(d,l,f,c,r[g+9],21,-343485551),c=c+_>>>0,d=d+m>>>0,l=l+B>>>0,f=f+p>>>0;}return n.endian([c,d,l,f])})._ff=function(e,t,r,n,i,a,A){var s=e+(t&r|~t&n)+(i>>>0)+A;return (s<<a|s>>>32-a)+t},s._gg=function(e,t,r,n,i,a,A){var s=e+(t&n|r&~n)+(i>>>0)+A;return (s<<a|s>>>32-a)+t},s._hh=function(e,t,r,n,i,a,A){var s=e+(t^r^n)+(i>>>0)+A;return (s<<a|s>>>32-a)+t},s._ii=function(e,t,r,n,i,a,A){var s=e+(r^(t|~n))+(i>>>0)+A;return (s<<a|s>>>32-a)+t},s._blocksize=16,s._digestsize=16,e.exports=function(e,t){if(null==e)throw new Error("Illegal argument "+e);var r=n.wordsToBytes(s(e,t));return t&&t.asBytes?r:t&&t.asString?A.bytesToString(r):n.bytesToHex(r)};},652:e=>{function t(e){return !!e.constructor&&"function"==typeof e.constructor.isBuffer&&e.constructor.isBuffer(e)}e.exports=function(e){return null!=e&&(t(e)||function(e){return "function"==typeof e.readFloatLE&&"function"==typeof e.slice&&t(e.slice(0,0))}(e)||!!e._isBuffer)};},1668:(e,t,r)=>{var n={};(0, r(9805).assign)(n,r(3303),r(7083),r(9681)),e.exports=n;},3303:(e,t,r)=>{var n=r(8411),i=r(9805),a=r(1996),A=r(4674),s=r(4442),o=Object.prototype.toString;function c(e){if(!(this instanceof c))return new c(e);this.options=i.assign({level:-1,method:8,chunkSize:16384,windowBits:15,memLevel:8,strategy:0,to:""},e||{});var t=this.options;t.raw&&t.windowBits>0?t.windowBits=-t.windowBits:t.gzip&&t.windowBits>0&&t.windowBits<16&&(t.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new s,this.strm.avail_out=0;var r=n.deflateInit2(this.strm,t.level,t.method,t.windowBits,t.memLevel,t.strategy);if(0!==r)throw new Error(A[r]);if(t.header&&n.deflateSetHeader(this.strm,t.header),t.dictionary){var d;if(d="string"==typeof t.dictionary?a.string2buf(t.dictionary):"[object ArrayBuffer]"===o.call(t.dictionary)?new Uint8Array(t.dictionary):t.dictionary,0!==(r=n.deflateSetDictionary(this.strm,d)))throw new Error(A[r]);this._dict_set=!0;}}function d(e,t){var r=new c(t);if(r.push(e,!0),r.err)throw r.msg||A[r.err];return r.result}c.prototype.push=function(e,t){var r,A,s=this.strm,c=this.options.chunkSize;if(this.ended)return !1;A=t===~~t?t:!0===t?4:0,"string"==typeof e?s.input=a.string2buf(e):"[object ArrayBuffer]"===o.call(e)?s.input=new Uint8Array(e):s.input=e,s.next_in=0,s.avail_in=s.input.length;do{if(0===s.avail_out&&(s.output=new i.Buf8(c),s.next_out=0,s.avail_out=c),1!==(r=n.deflate(s,A))&&0!==r)return this.onEnd(r),this.ended=!0,!1;0!==s.avail_out&&(0!==s.avail_in||4!==A&&2!==A)||("string"===this.options.to?this.onData(a.buf2binstring(i.shrinkBuf(s.output,s.next_out))):this.onData(i.shrinkBuf(s.output,s.next_out)));}while((s.avail_in>0||0===s.avail_out)&&1!==r);return 4===A?(r=n.deflateEnd(this.strm),this.onEnd(r),this.ended=!0,0===r):2!==A||(this.onEnd(0),s.avail_out=0,!0)},c.prototype.onData=function(e){this.chunks.push(e);},c.prototype.onEnd=function(e){0===e&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=i.flattenChunks(this.chunks)),this.chunks=[],this.err=e,this.msg=this.strm.msg;},t.Deflate=c,t.deflate=d,t.deflateRaw=function(e,t){return (t=t||{}).raw=!0,d(e,t)},t.gzip=function(e,t){return (t=t||{}).gzip=!0,d(e,t)};},7083:(e,t,r)=>{var n=r(1447),i=r(9805),a=r(1996),A=r(9681),s=r(4674),o=r(4442),c=r(7414),d=Object.prototype.toString;function l(e){if(!(this instanceof l))return new l(e);this.options=i.assign({chunkSize:16384,windowBits:0,to:""},e||{});var t=this.options;t.raw&&t.windowBits>=0&&t.windowBits<16&&(t.windowBits=-t.windowBits,0===t.windowBits&&(t.windowBits=-15)),!(t.windowBits>=0&&t.windowBits<16)||e&&e.windowBits||(t.windowBits+=32),t.windowBits>15&&t.windowBits<48&&(15&t.windowBits||(t.windowBits|=15)),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new o,this.strm.avail_out=0;var r=n.inflateInit2(this.strm,t.windowBits);if(r!==A.Z_OK)throw new Error(s[r]);if(this.header=new c,n.inflateGetHeader(this.strm,this.header),t.dictionary&&("string"==typeof t.dictionary?t.dictionary=a.string2buf(t.dictionary):"[object ArrayBuffer]"===d.call(t.dictionary)&&(t.dictionary=new Uint8Array(t.dictionary)),t.raw&&(r=n.inflateSetDictionary(this.strm,t.dictionary))!==A.Z_OK))throw new Error(s[r])}function f(e,t){var r=new l(t);if(r.push(e,!0),r.err)throw r.msg||s[r.err];return r.result}l.prototype.push=function(e,t){var r,s,o,c,l,f=this.strm,g=this.options.chunkSize,h=this.options.dictionary,u=!1;if(this.ended)return !1;s=t===~~t?t:!0===t?A.Z_FINISH:A.Z_NO_FLUSH,"string"==typeof e?f.input=a.binstring2buf(e):"[object ArrayBuffer]"===d.call(e)?f.input=new Uint8Array(e):f.input=e,f.next_in=0,f.avail_in=f.input.length;do{if(0===f.avail_out&&(f.output=new i.Buf8(g),f.next_out=0,f.avail_out=g),(r=n.inflate(f,A.Z_NO_FLUSH))===A.Z_NEED_DICT&&h&&(r=n.inflateSetDictionary(this.strm,h)),r===A.Z_BUF_ERROR&&!0===u&&(r=A.Z_OK,u=!1),r!==A.Z_STREAM_END&&r!==A.Z_OK)return this.onEnd(r),this.ended=!0,!1;f.next_out&&(0!==f.avail_out&&r!==A.Z_STREAM_END&&(0!==f.avail_in||s!==A.Z_FINISH&&s!==A.Z_SYNC_FLUSH)||("string"===this.options.to?(o=a.utf8border(f.output,f.next_out),c=f.next_out-o,l=a.buf2string(f.output,o),f.next_out=c,f.avail_out=g-c,c&&i.arraySet(f.output,f.output,o,c,0),this.onData(l)):this.onData(i.shrinkBuf(f.output,f.next_out)))),0===f.avail_in&&0===f.avail_out&&(u=!0);}while((f.avail_in>0||0===f.avail_out)&&r!==A.Z_STREAM_END);return r===A.Z_STREAM_END&&(s=A.Z_FINISH),s===A.Z_FINISH?(r=n.inflateEnd(this.strm),this.onEnd(r),this.ended=!0,r===A.Z_OK):s!==A.Z_SYNC_FLUSH||(this.onEnd(A.Z_OK),f.avail_out=0,!0)},l.prototype.onData=function(e){this.chunks.push(e);},l.prototype.onEnd=function(e){e===A.Z_OK&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=i.flattenChunks(this.chunks)),this.chunks=[],this.err=e,this.msg=this.strm.msg;},t.Inflate=l,t.inflate=f,t.inflateRaw=function(e,t){return (t=t||{}).raw=!0,f(e,t)},t.ungzip=f;},9805:(e,t)=>{var r="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Int32Array;function n(e,t){return Object.prototype.hasOwnProperty.call(e,t)}t.assign=function(e){for(var t=Array.prototype.slice.call(arguments,1);t.length;){var r=t.shift();if(r){if("object"!=typeof r)throw new TypeError(r+"must be non-object");for(var i in r)n(r,i)&&(e[i]=r[i]);}}return e},t.shrinkBuf=function(e,t){return e.length===t?e:e.subarray?e.subarray(0,t):(e.length=t,e)};var i={arraySet:function(e,t,r,n,i){if(t.subarray&&e.subarray)e.set(t.subarray(r,r+n),i);else for(var a=0;a<n;a++)e[i+a]=t[r+a];},flattenChunks:function(e){var t,r,n,i,a,A;for(n=0,t=0,r=e.length;t<r;t++)n+=e[t].length;for(A=new Uint8Array(n),i=0,t=0,r=e.length;t<r;t++)a=e[t],A.set(a,i),i+=a.length;return A}},a={arraySet:function(e,t,r,n,i){for(var a=0;a<n;a++)e[i+a]=t[r+a];},flattenChunks:function(e){return [].concat.apply([],e)}};t.setTyped=function(e){e?(t.Buf8=Uint8Array,t.Buf16=Uint16Array,t.Buf32=Int32Array,t.assign(t,i)):(t.Buf8=Array,t.Buf16=Array,t.Buf32=Array,t.assign(t,a));},t.setTyped(r);},1996:(e,t,r)=>{var n=r(9805),i=!0,a=!0;try{String.fromCharCode.apply(null,[0]);}catch(e){i=!1;}try{String.fromCharCode.apply(null,new Uint8Array(1));}catch(e){a=!1;}for(var A=new n.Buf8(256),s=0;s<256;s++)A[s]=s>=252?6:s>=248?5:s>=240?4:s>=224?3:s>=192?2:1;function o(e,t){if(t<65534&&(e.subarray&&a||!e.subarray&&i))return String.fromCharCode.apply(null,n.shrinkBuf(e,t));for(var r="",A=0;A<t;A++)r+=String.fromCharCode(e[A]);return r}A[254]=A[254]=1,t.string2buf=function(e){var t,r,i,a,A,s=e.length,o=0;for(a=0;a<s;a++)55296==(64512&(r=e.charCodeAt(a)))&&a+1<s&&56320==(64512&(i=e.charCodeAt(a+1)))&&(r=65536+(r-55296<<10)+(i-56320),a++),o+=r<128?1:r<2048?2:r<65536?3:4;for(t=new n.Buf8(o),A=0,a=0;A<o;a++)55296==(64512&(r=e.charCodeAt(a)))&&a+1<s&&56320==(64512&(i=e.charCodeAt(a+1)))&&(r=65536+(r-55296<<10)+(i-56320),a++),r<128?t[A++]=r:r<2048?(t[A++]=192|r>>>6,t[A++]=128|63&r):r<65536?(t[A++]=224|r>>>12,t[A++]=128|r>>>6&63,t[A++]=128|63&r):(t[A++]=240|r>>>18,t[A++]=128|r>>>12&63,t[A++]=128|r>>>6&63,t[A++]=128|63&r);return t},t.buf2binstring=function(e){return o(e,e.length)},t.binstring2buf=function(e){for(var t=new n.Buf8(e.length),r=0,i=t.length;r<i;r++)t[r]=e.charCodeAt(r);return t},t.buf2string=function(e,t){var r,n,i,a,s=t||e.length,c=new Array(2*s);for(n=0,r=0;r<s;)if((i=e[r++])<128)c[n++]=i;else if((a=A[i])>4)c[n++]=65533,r+=a-1;else {for(i&=2===a?31:3===a?15:7;a>1&&r<s;)i=i<<6|63&e[r++],a--;a>1?c[n++]=65533:i<65536?c[n++]=i:(i-=65536,c[n++]=55296|i>>10&1023,c[n++]=56320|1023&i);}return o(c,n)},t.utf8border=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;r>=0&&128==(192&e[r]);)r--;return r<0||0===r?t:r+A[e[r]]>t?r:t};},3269:e=>{e.exports=function(e,t,r,n){for(var i=65535&e,a=e>>>16&65535,A=0;0!==r;){r-=A=r>2e3?2e3:r;do{a=a+(i=i+t[n++]|0)|0;}while(--A);i%=65521,a%=65521;}return i|a<<16};},9681:e=>{e.exports={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8};},4823:e=>{var t=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e;}return t}();e.exports=function(e,r,n,i){var a=t,A=i+n;e^=-1;for(var s=i;s<A;s++)e=e>>>8^a[255&(e^r[s])];return ~e};},8411:(e,t,r)=>{var n,i=r(9805),a=r(3665),A=r(3269),s=r(4823),o=r(4674),c=-2,d=258,l=262,f=103,g=113,h=666;function u(e,t){return e.msg=o[t],t}function C(e){return (e<<1)-(e>4?9:0)}function I(e){for(var t=e.length;--t>=0;)e[t]=0;}function _(e){var t=e.state,r=t.pending;r>e.avail_out&&(r=e.avail_out),0!==r&&(i.arraySet(e.output,t.pending_buf,t.pending_out,r,e.next_out),e.next_out+=r,t.pending_out+=r,e.total_out+=r,e.avail_out-=r,t.pending-=r,0===t.pending&&(t.pending_out=0));}function m(e,t){a._tr_flush_block(e,e.block_start>=0?e.block_start:-1,e.strstart-e.block_start,t),e.block_start=e.strstart,_(e.strm);}function B(e,t){e.pending_buf[e.pending++]=t;}function p(e,t){e.pending_buf[e.pending++]=t>>>8&255,e.pending_buf[e.pending++]=255&t;}function w(e,t,r,n){var a=e.avail_in;return a>n&&(a=n),0===a?0:(e.avail_in-=a,i.arraySet(t,e.input,e.next_in,a,r),1===e.state.wrap?e.adler=A(e.adler,t,a,r):2===e.state.wrap&&(e.adler=s(e.adler,t,a,r)),e.next_in+=a,e.total_in+=a,a)}function E(e,t){var r,n,i=e.max_chain_length,a=e.strstart,A=e.prev_length,s=e.nice_match,o=e.strstart>e.w_size-l?e.strstart-(e.w_size-l):0,c=e.window,f=e.w_mask,g=e.prev,h=e.strstart+d,u=c[a+A-1],C=c[a+A];e.prev_length>=e.good_match&&(i>>=2),s>e.lookahead&&(s=e.lookahead);do{if(c[(r=t)+A]===C&&c[r+A-1]===u&&c[r]===c[a]&&c[++r]===c[a+1]){a+=2,r++;do{}while(c[++a]===c[++r]&&c[++a]===c[++r]&&c[++a]===c[++r]&&c[++a]===c[++r]&&c[++a]===c[++r]&&c[++a]===c[++r]&&c[++a]===c[++r]&&c[++a]===c[++r]&&a<h);if(n=d-(h-a),a=h-d,n>A){if(e.match_start=t,A=n,n>=s)break;u=c[a+A-1],C=c[a+A];}}}while((t=g[t&f])>o&&0!=--i);return A<=e.lookahead?A:e.lookahead}function Q(e){var t,r,n,a,A,s=e.w_size;do{if(a=e.window_size-e.lookahead-e.strstart,e.strstart>=s+(s-l)){i.arraySet(e.window,e.window,s,s,0),e.match_start-=s,e.strstart-=s,e.block_start-=s,t=r=e.hash_size;do{n=e.head[--t],e.head[t]=n>=s?n-s:0;}while(--r);t=r=s;do{n=e.prev[--t],e.prev[t]=n>=s?n-s:0;}while(--r);a+=s;}if(0===e.strm.avail_in)break;if(r=w(e.strm,e.window,e.strstart+e.lookahead,a),e.lookahead+=r,e.lookahead+e.insert>=3)for(A=e.strstart-e.insert,e.ins_h=e.window[A],e.ins_h=(e.ins_h<<e.hash_shift^e.window[A+1])&e.hash_mask;e.insert&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[A+3-1])&e.hash_mask,e.prev[A&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=A,A++,e.insert--,!(e.lookahead+e.insert<3)););}while(e.lookahead<l&&0!==e.strm.avail_in)}function y(e,t){for(var r,n;;){if(e.lookahead<l){if(Q(e),e.lookahead<l&&0===t)return 1;if(0===e.lookahead)break}if(r=0,e.lookahead>=3&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+3-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart),0!==r&&e.strstart-r<=e.w_size-l&&(e.match_length=E(e,r)),e.match_length>=3)if(n=a._tr_tally(e,e.strstart-e.match_start,e.match_length-3),e.lookahead-=e.match_length,e.match_length<=e.max_lazy_match&&e.lookahead>=3){e.match_length--;do{e.strstart++,e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+3-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart;}while(0!=--e.match_length);e.strstart++;}else e.strstart+=e.match_length,e.match_length=0,e.ins_h=e.window[e.strstart],e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+1])&e.hash_mask;else n=a._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++;if(n&&(m(e,!1),0===e.strm.avail_out))return 1}return e.insert=e.strstart<2?e.strstart:2,4===t?(m(e,!0),0===e.strm.avail_out?3:4):e.last_lit&&(m(e,!1),0===e.strm.avail_out)?1:2}function b(e,t){for(var r,n,i;;){if(e.lookahead<l){if(Q(e),e.lookahead<l&&0===t)return 1;if(0===e.lookahead)break}if(r=0,e.lookahead>=3&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+3-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart),e.prev_length=e.match_length,e.prev_match=e.match_start,e.match_length=2,0!==r&&e.prev_length<e.max_lazy_match&&e.strstart-r<=e.w_size-l&&(e.match_length=E(e,r),e.match_length<=5&&(1===e.strategy||3===e.match_length&&e.strstart-e.match_start>4096)&&(e.match_length=2)),e.prev_length>=3&&e.match_length<=e.prev_length){i=e.strstart+e.lookahead-3,n=a._tr_tally(e,e.strstart-1-e.prev_match,e.prev_length-3),e.lookahead-=e.prev_length-1,e.prev_length-=2;do{++e.strstart<=i&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+3-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart);}while(0!=--e.prev_length);if(e.match_available=0,e.match_length=2,e.strstart++,n&&(m(e,!1),0===e.strm.avail_out))return 1}else if(e.match_available){if((n=a._tr_tally(e,0,e.window[e.strstart-1]))&&m(e,!1),e.strstart++,e.lookahead--,0===e.strm.avail_out)return 1}else e.match_available=1,e.strstart++,e.lookahead--;}return e.match_available&&(n=a._tr_tally(e,0,e.window[e.strstart-1]),e.match_available=0),e.insert=e.strstart<2?e.strstart:2,4===t?(m(e,!0),0===e.strm.avail_out?3:4):e.last_lit&&(m(e,!1),0===e.strm.avail_out)?1:2}function S(e,t,r,n,i){this.good_length=e,this.max_lazy=t,this.nice_length=r,this.max_chain=n,this.func=i;}function D(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=8,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new i.Buf16(1146),this.dyn_dtree=new i.Buf16(122),this.bl_tree=new i.Buf16(78),I(this.dyn_ltree),I(this.dyn_dtree),I(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new i.Buf16(16),this.heap=new i.Buf16(573),I(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new i.Buf16(573),I(this.depth),this.l_buf=0,this.lit_bufsize=0,this.last_lit=0,this.d_buf=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0;}function k(e){var t;return e&&e.state?(e.total_in=e.total_out=0,e.data_type=2,(t=e.state).pending=0,t.pending_out=0,t.wrap<0&&(t.wrap=-t.wrap),t.status=t.wrap?42:g,e.adler=2===t.wrap?0:1,t.last_flush=0,a._tr_init(t),0):u(e,c)}function v(e){var t,r=k(e);return 0===r&&((t=e.state).window_size=2*t.w_size,I(t.head),t.max_lazy_match=n[t.level].max_lazy,t.good_match=n[t.level].good_length,t.nice_match=n[t.level].nice_length,t.max_chain_length=n[t.level].max_chain,t.strstart=0,t.block_start=0,t.lookahead=0,t.insert=0,t.match_length=t.prev_length=2,t.match_available=0,t.ins_h=0),r}function R(e,t,r,n,a,A){if(!e)return c;var s=1;if(-1===t&&(t=6),n<0?(s=0,n=-n):n>15&&(s=2,n-=16),a<1||a>9||8!==r||n<8||n>15||t<0||t>9||A<0||A>4)return u(e,c);8===n&&(n=9);var o=new D;return e.state=o,o.strm=e,o.wrap=s,o.gzhead=null,o.w_bits=n,o.w_size=1<<o.w_bits,o.w_mask=o.w_size-1,o.hash_bits=a+7,o.hash_size=1<<o.hash_bits,o.hash_mask=o.hash_size-1,o.hash_shift=~~((o.hash_bits+3-1)/3),o.window=new i.Buf8(2*o.w_size),o.head=new i.Buf16(o.hash_size),o.prev=new i.Buf16(o.w_size),o.lit_bufsize=1<<a+6,o.pending_buf_size=4*o.lit_bufsize,o.pending_buf=new i.Buf8(o.pending_buf_size),o.d_buf=1*o.lit_bufsize,o.l_buf=3*o.lit_bufsize,o.level=t,o.strategy=A,o.method=r,v(e)}n=[new S(0,0,0,0,(function(e,t){var r=65535;for(r>e.pending_buf_size-5&&(r=e.pending_buf_size-5);;){if(e.lookahead<=1){if(Q(e),0===e.lookahead&&0===t)return 1;if(0===e.lookahead)break}e.strstart+=e.lookahead,e.lookahead=0;var n=e.block_start+r;if((0===e.strstart||e.strstart>=n)&&(e.lookahead=e.strstart-n,e.strstart=n,m(e,!1),0===e.strm.avail_out))return 1;if(e.strstart-e.block_start>=e.w_size-l&&(m(e,!1),0===e.strm.avail_out))return 1}return e.insert=0,4===t?(m(e,!0),0===e.strm.avail_out?3:4):(e.strstart>e.block_start&&(m(e,!1),e.strm.avail_out),1)})),new S(4,4,8,4,y),new S(4,5,16,8,y),new S(4,6,32,32,y),new S(4,4,16,16,b),new S(8,16,32,32,b),new S(8,16,128,128,b),new S(8,32,128,256,b),new S(32,128,258,1024,b),new S(32,258,258,4096,b)],t.deflateInit=function(e,t){return R(e,t,8,15,8,0)},t.deflateInit2=R,t.deflateReset=v,t.deflateResetKeep=k,t.deflateSetHeader=function(e,t){return e&&e.state?2!==e.state.wrap?c:(e.state.gzhead=t,0):c},t.deflate=function(e,t){var r,i,A,o;if(!e||!e.state||t>5||t<0)return e?u(e,c):c;if(i=e.state,!e.output||!e.input&&0!==e.avail_in||i.status===h&&4!==t)return u(e,0===e.avail_out?-5:c);if(i.strm=e,r=i.last_flush,i.last_flush=t,42===i.status)if(2===i.wrap)e.adler=0,B(i,31),B(i,139),B(i,8),i.gzhead?(B(i,(i.gzhead.text?1:0)+(i.gzhead.hcrc?2:0)+(i.gzhead.extra?4:0)+(i.gzhead.name?8:0)+(i.gzhead.comment?16:0)),B(i,255&i.gzhead.time),B(i,i.gzhead.time>>8&255),B(i,i.gzhead.time>>16&255),B(i,i.gzhead.time>>24&255),B(i,9===i.level?2:i.strategy>=2||i.level<2?4:0),B(i,255&i.gzhead.os),i.gzhead.extra&&i.gzhead.extra.length&&(B(i,255&i.gzhead.extra.length),B(i,i.gzhead.extra.length>>8&255)),i.gzhead.hcrc&&(e.adler=s(e.adler,i.pending_buf,i.pending,0)),i.gzindex=0,i.status=69):(B(i,0),B(i,0),B(i,0),B(i,0),B(i,0),B(i,9===i.level?2:i.strategy>=2||i.level<2?4:0),B(i,3),i.status=g);else {var l=8+(i.w_bits-8<<4)<<8;l|=(i.strategy>=2||i.level<2?0:i.level<6?1:6===i.level?2:3)<<6,0!==i.strstart&&(l|=32),l+=31-l%31,i.status=g,p(i,l),0!==i.strstart&&(p(i,e.adler>>>16),p(i,65535&e.adler)),e.adler=1;}if(69===i.status)if(i.gzhead.extra){for(A=i.pending;i.gzindex<(65535&i.gzhead.extra.length)&&(i.pending!==i.pending_buf_size||(i.gzhead.hcrc&&i.pending>A&&(e.adler=s(e.adler,i.pending_buf,i.pending-A,A)),_(e),A=i.pending,i.pending!==i.pending_buf_size));)B(i,255&i.gzhead.extra[i.gzindex]),i.gzindex++;i.gzhead.hcrc&&i.pending>A&&(e.adler=s(e.adler,i.pending_buf,i.pending-A,A)),i.gzindex===i.gzhead.extra.length&&(i.gzindex=0,i.status=73);}else i.status=73;if(73===i.status)if(i.gzhead.name){A=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>A&&(e.adler=s(e.adler,i.pending_buf,i.pending-A,A)),_(e),A=i.pending,i.pending===i.pending_buf_size)){o=1;break}o=i.gzindex<i.gzhead.name.length?255&i.gzhead.name.charCodeAt(i.gzindex++):0,B(i,o);}while(0!==o);i.gzhead.hcrc&&i.pending>A&&(e.adler=s(e.adler,i.pending_buf,i.pending-A,A)),0===o&&(i.gzindex=0,i.status=91);}else i.status=91;if(91===i.status)if(i.gzhead.comment){A=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>A&&(e.adler=s(e.adler,i.pending_buf,i.pending-A,A)),_(e),A=i.pending,i.pending===i.pending_buf_size)){o=1;break}o=i.gzindex<i.gzhead.comment.length?255&i.gzhead.comment.charCodeAt(i.gzindex++):0,B(i,o);}while(0!==o);i.gzhead.hcrc&&i.pending>A&&(e.adler=s(e.adler,i.pending_buf,i.pending-A,A)),0===o&&(i.status=f);}else i.status=f;if(i.status===f&&(i.gzhead.hcrc?(i.pending+2>i.pending_buf_size&&_(e),i.pending+2<=i.pending_buf_size&&(B(i,255&e.adler),B(i,e.adler>>8&255),e.adler=0,i.status=g)):i.status=g),0!==i.pending){if(_(e),0===e.avail_out)return i.last_flush=-1,0}else if(0===e.avail_in&&C(t)<=C(r)&&4!==t)return u(e,-5);if(i.status===h&&0!==e.avail_in)return u(e,-5);if(0!==e.avail_in||0!==i.lookahead||0!==t&&i.status!==h){var w=2===i.strategy?function(e,t){for(var r;;){if(0===e.lookahead&&(Q(e),0===e.lookahead)){if(0===t)return 1;break}if(e.match_length=0,r=a._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++,r&&(m(e,!1),0===e.strm.avail_out))return 1}return e.insert=0,4===t?(m(e,!0),0===e.strm.avail_out?3:4):e.last_lit&&(m(e,!1),0===e.strm.avail_out)?1:2}(i,t):3===i.strategy?function(e,t){for(var r,n,i,A,s=e.window;;){if(e.lookahead<=d){if(Q(e),e.lookahead<=d&&0===t)return 1;if(0===e.lookahead)break}if(e.match_length=0,e.lookahead>=3&&e.strstart>0&&(n=s[i=e.strstart-1])===s[++i]&&n===s[++i]&&n===s[++i]){A=e.strstart+d;do{}while(n===s[++i]&&n===s[++i]&&n===s[++i]&&n===s[++i]&&n===s[++i]&&n===s[++i]&&n===s[++i]&&n===s[++i]&&i<A);e.match_length=d-(A-i),e.match_length>e.lookahead&&(e.match_length=e.lookahead);}if(e.match_length>=3?(r=a._tr_tally(e,1,e.match_length-3),e.lookahead-=e.match_length,e.strstart+=e.match_length,e.match_length=0):(r=a._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++),r&&(m(e,!1),0===e.strm.avail_out))return 1}return e.insert=0,4===t?(m(e,!0),0===e.strm.avail_out?3:4):e.last_lit&&(m(e,!1),0===e.strm.avail_out)?1:2}(i,t):n[i.level].func(i,t);if(3!==w&&4!==w||(i.status=h),1===w||3===w)return 0===e.avail_out&&(i.last_flush=-1),0;if(2===w&&(1===t?a._tr_align(i):5!==t&&(a._tr_stored_block(i,0,0,!1),3===t&&(I(i.head),0===i.lookahead&&(i.strstart=0,i.block_start=0,i.insert=0))),_(e),0===e.avail_out))return i.last_flush=-1,0}return 4!==t?0:i.wrap<=0?1:(2===i.wrap?(B(i,255&e.adler),B(i,e.adler>>8&255),B(i,e.adler>>16&255),B(i,e.adler>>24&255),B(i,255&e.total_in),B(i,e.total_in>>8&255),B(i,e.total_in>>16&255),B(i,e.total_in>>24&255)):(p(i,e.adler>>>16),p(i,65535&e.adler)),_(e),i.wrap>0&&(i.wrap=-i.wrap),0!==i.pending?0:1)},t.deflateEnd=function(e){var t;return e&&e.state?42!==(t=e.state.status)&&69!==t&&73!==t&&91!==t&&t!==f&&t!==g&&t!==h?u(e,c):(e.state=null,t===g?u(e,-3):0):c},t.deflateSetDictionary=function(e,t){var r,n,a,s,o,d,l,f,g=t.length;if(!e||!e.state)return c;if(2===(s=(r=e.state).wrap)||1===s&&42!==r.status||r.lookahead)return c;for(1===s&&(e.adler=A(e.adler,t,g,0)),r.wrap=0,g>=r.w_size&&(0===s&&(I(r.head),r.strstart=0,r.block_start=0,r.insert=0),f=new i.Buf8(r.w_size),i.arraySet(f,t,g-r.w_size,r.w_size,0),t=f,g=r.w_size),o=e.avail_in,d=e.next_in,l=e.input,e.avail_in=g,e.next_in=0,e.input=t,Q(r);r.lookahead>=3;){n=r.strstart,a=r.lookahead-2;do{r.ins_h=(r.ins_h<<r.hash_shift^r.window[n+3-1])&r.hash_mask,r.prev[n&r.w_mask]=r.head[r.ins_h],r.head[r.ins_h]=n,n++;}while(--a);r.strstart=n,r.lookahead=2,Q(r);}return r.strstart+=r.lookahead,r.block_start=r.strstart,r.insert=r.lookahead,r.lookahead=0,r.match_length=r.prev_length=2,r.match_available=0,e.next_in=d,e.input=l,e.avail_in=o,r.wrap=s,0},t.deflateInfo="pako deflate (from Nodeca project)";},7414:e=>{e.exports=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1;};},7293:e=>{e.exports=function(e,t){var r,n,i,a,A,s,o,c,d,l,f,g,h,u,C,I,_,m,B,p,w,E,Q,y,b;r=e.state,n=e.next_in,y=e.input,i=n+(e.avail_in-5),a=e.next_out,b=e.output,A=a-(t-e.avail_out),s=a+(e.avail_out-257),o=r.dmax,c=r.wsize,d=r.whave,l=r.wnext,f=r.window,g=r.hold,h=r.bits,u=r.lencode,C=r.distcode,I=(1<<r.lenbits)-1,_=(1<<r.distbits)-1;e:do{h<15&&(g+=y[n++]<<h,h+=8,g+=y[n++]<<h,h+=8),m=u[g&I];t:for(;;){if(g>>>=B=m>>>24,h-=B,0==(B=m>>>16&255))b[a++]=65535&m;else {if(!(16&B)){if(64&B){if(32&B){r.mode=12;break e}e.msg="invalid literal/length code",r.mode=30;break e}m=u[(65535&m)+(g&(1<<B)-1)];continue t}for(p=65535&m,(B&=15)&&(h<B&&(g+=y[n++]<<h,h+=8),p+=g&(1<<B)-1,g>>>=B,h-=B),h<15&&(g+=y[n++]<<h,h+=8,g+=y[n++]<<h,h+=8),m=C[g&_];;){if(g>>>=B=m>>>24,h-=B,16&(B=m>>>16&255)){if(w=65535&m,h<(B&=15)&&(g+=y[n++]<<h,(h+=8)<B&&(g+=y[n++]<<h,h+=8)),(w+=g&(1<<B)-1)>o){e.msg="invalid distance too far back",r.mode=30;break e}if(g>>>=B,h-=B,w>(B=a-A)){if((B=w-B)>d&&r.sane){e.msg="invalid distance too far back",r.mode=30;break e}if(E=0,Q=f,0===l){if(E+=c-B,B<p){p-=B;do{b[a++]=f[E++];}while(--B);E=a-w,Q=b;}}else if(l<B){if(E+=c+l-B,(B-=l)<p){p-=B;do{b[a++]=f[E++];}while(--B);if(E=0,l<p){p-=B=l;do{b[a++]=f[E++];}while(--B);E=a-w,Q=b;}}}else if(E+=l-B,B<p){p-=B;do{b[a++]=f[E++];}while(--B);E=a-w,Q=b;}for(;p>2;)b[a++]=Q[E++],b[a++]=Q[E++],b[a++]=Q[E++],p-=3;p&&(b[a++]=Q[E++],p>1&&(b[a++]=Q[E++]));}else {E=a-w;do{b[a++]=b[E++],b[a++]=b[E++],b[a++]=b[E++],p-=3;}while(p>2);p&&(b[a++]=b[E++],p>1&&(b[a++]=b[E++]));}break}if(64&B){e.msg="invalid distance code",r.mode=30;break e}m=C[(65535&m)+(g&(1<<B)-1)];}}break}}while(n<i&&a<s);n-=p=h>>3,g&=(1<<(h-=p<<3))-1,e.next_in=n,e.next_out=a,e.avail_in=n<i?i-n+5:5-(n-i),e.avail_out=a<s?s-a+257:257-(a-s),r.hold=g,r.bits=h;};},1447:(e,t,r)=>{var n=r(9805),i=r(3269),a=r(4823),A=r(7293),s=r(1998),o=-2,c=12,d=30;function l(e){return (e>>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function f(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new n.Buf16(320),this.work=new n.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0;}function g(e){var t;return e&&e.state?(t=e.state,e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=1,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new n.Buf32(852),t.distcode=t.distdyn=new n.Buf32(592),t.sane=1,t.back=-1,0):o}function h(e){var t;return e&&e.state?((t=e.state).wsize=0,t.whave=0,t.wnext=0,g(e)):o}function u(e,t){var r,n;return e&&e.state?(n=e.state,t<0?(r=0,t=-t):(r=1+(t>>4),t<48&&(t&=15)),t&&(t<8||t>15)?o:(null!==n.window&&n.wbits!==t&&(n.window=null),n.wrap=r,n.wbits=t,h(e))):o}function C(e,t){var r,n;return e?(n=new f,e.state=n,n.window=null,0!==(r=u(e,t))&&(e.state=null),r):o}var I,_,m=!0;function B(e){if(m){var t;for(I=new n.Buf32(512),_=new n.Buf32(32),t=0;t<144;)e.lens[t++]=8;for(;t<256;)e.lens[t++]=9;for(;t<280;)e.lens[t++]=7;for(;t<288;)e.lens[t++]=8;for(s(1,e.lens,0,288,I,0,e.work,{bits:9}),t=0;t<32;)e.lens[t++]=5;s(2,e.lens,0,32,_,0,e.work,{bits:5}),m=!1;}e.lencode=I,e.lenbits=9,e.distcode=_,e.distbits=5;}function p(e,t,r,i){var a,A=e.state;return null===A.window&&(A.wsize=1<<A.wbits,A.wnext=0,A.whave=0,A.window=new n.Buf8(A.wsize)),i>=A.wsize?(n.arraySet(A.window,t,r-A.wsize,A.wsize,0),A.wnext=0,A.whave=A.wsize):((a=A.wsize-A.wnext)>i&&(a=i),n.arraySet(A.window,t,r-i,a,A.wnext),(i-=a)?(n.arraySet(A.window,t,r-i,i,0),A.wnext=i,A.whave=A.wsize):(A.wnext+=a,A.wnext===A.wsize&&(A.wnext=0),A.whave<A.wsize&&(A.whave+=a))),0}t.inflateReset=h,t.inflateReset2=u,t.inflateResetKeep=g,t.inflateInit=function(e){return C(e,15)},t.inflateInit2=C,t.inflate=function(e,t){var r,f,g,h,u,C,I,_,m,w,E,Q,y,b,S,D,k,v,R,F,N,M,x,L,O=0,T=new n.Buf8(4),q=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];if(!e||!e.state||!e.output||!e.input&&0!==e.avail_in)return o;(r=e.state).mode===c&&(r.mode=13),u=e.next_out,g=e.output,I=e.avail_out,h=e.next_in,f=e.input,C=e.avail_in,_=r.hold,m=r.bits,w=C,E=I,M=0;e:for(;;)switch(r.mode){case 1:if(0===r.wrap){r.mode=13;break}for(;m<16;){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}if(2&r.wrap&&35615===_){r.check=0,T[0]=255&_,T[1]=_>>>8&255,r.check=a(r.check,T,2,0),_=0,m=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&_)<<8)+(_>>8))%31){e.msg="incorrect header check",r.mode=d;break}if(8!=(15&_)){e.msg="unknown compression method",r.mode=d;break}if(m-=4,N=8+(15&(_>>>=4)),0===r.wbits)r.wbits=N;else if(N>r.wbits){e.msg="invalid window size",r.mode=d;break}r.dmax=1<<N,e.adler=r.check=1,r.mode=512&_?10:c,_=0,m=0;break;case 2:for(;m<16;){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}if(r.flags=_,8!=(255&r.flags)){e.msg="unknown compression method",r.mode=d;break}if(57344&r.flags){e.msg="unknown header flags set",r.mode=d;break}r.head&&(r.head.text=_>>8&1),512&r.flags&&(T[0]=255&_,T[1]=_>>>8&255,r.check=a(r.check,T,2,0)),_=0,m=0,r.mode=3;case 3:for(;m<32;){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}r.head&&(r.head.time=_),512&r.flags&&(T[0]=255&_,T[1]=_>>>8&255,T[2]=_>>>16&255,T[3]=_>>>24&255,r.check=a(r.check,T,4,0)),_=0,m=0,r.mode=4;case 4:for(;m<16;){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}r.head&&(r.head.xflags=255&_,r.head.os=_>>8),512&r.flags&&(T[0]=255&_,T[1]=_>>>8&255,r.check=a(r.check,T,2,0)),_=0,m=0,r.mode=5;case 5:if(1024&r.flags){for(;m<16;){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}r.length=_,r.head&&(r.head.extra_len=_),512&r.flags&&(T[0]=255&_,T[1]=_>>>8&255,r.check=a(r.check,T,2,0)),_=0,m=0;}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&((Q=r.length)>C&&(Q=C),Q&&(r.head&&(N=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),n.arraySet(r.head.extra,f,h,Q,N)),512&r.flags&&(r.check=a(r.check,f,Q,h)),C-=Q,h+=Q,r.length-=Q),r.length))break e;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===C)break e;Q=0;do{N=f[h+Q++],r.head&&N&&r.length<65536&&(r.head.name+=String.fromCharCode(N));}while(N&&Q<C);if(512&r.flags&&(r.check=a(r.check,f,Q,h)),C-=Q,h+=Q,N)break e}else r.head&&(r.head.name=null);r.length=0,r.mode=8;case 8:if(4096&r.flags){if(0===C)break e;Q=0;do{N=f[h+Q++],r.head&&N&&r.length<65536&&(r.head.comment+=String.fromCharCode(N));}while(N&&Q<C);if(512&r.flags&&(r.check=a(r.check,f,Q,h)),C-=Q,h+=Q,N)break e}else r.head&&(r.head.comment=null);r.mode=9;case 9:if(512&r.flags){for(;m<16;){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}if(_!==(65535&r.check)){e.msg="header crc mismatch",r.mode=d;break}_=0,m=0;}r.head&&(r.head.hcrc=r.flags>>9&1,r.head.done=!0),e.adler=r.check=0,r.mode=c;break;case 10:for(;m<32;){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}e.adler=r.check=l(_),_=0,m=0,r.mode=11;case 11:if(0===r.havedict)return e.next_out=u,e.avail_out=I,e.next_in=h,e.avail_in=C,r.hold=_,r.bits=m,2;e.adler=r.check=1,r.mode=c;case c:if(5===t||6===t)break e;case 13:if(r.last){_>>>=7&m,m-=7&m,r.mode=27;break}for(;m<3;){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}switch(r.last=1&_,m-=1,3&(_>>>=1)){case 0:r.mode=14;break;case 1:if(B(r),r.mode=20,6===t){_>>>=2,m-=2;break e}break;case 2:r.mode=17;break;case 3:e.msg="invalid block type",r.mode=d;}_>>>=2,m-=2;break;case 14:for(_>>>=7&m,m-=7&m;m<32;){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}if((65535&_)!=(_>>>16^65535)){e.msg="invalid stored block lengths",r.mode=d;break}if(r.length=65535&_,_=0,m=0,r.mode=15,6===t)break e;case 15:r.mode=16;case 16:if(Q=r.length){if(Q>C&&(Q=C),Q>I&&(Q=I),0===Q)break e;n.arraySet(g,f,h,Q,u),C-=Q,h+=Q,I-=Q,u+=Q,r.length-=Q;break}r.mode=c;break;case 17:for(;m<14;){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}if(r.nlen=257+(31&_),_>>>=5,m-=5,r.ndist=1+(31&_),_>>>=5,m-=5,r.ncode=4+(15&_),_>>>=4,m-=4,r.nlen>286||r.ndist>30){e.msg="too many length or distance symbols",r.mode=d;break}r.have=0,r.mode=18;case 18:for(;r.have<r.ncode;){for(;m<3;){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}r.lens[q[r.have++]]=7&_,_>>>=3,m-=3;}for(;r.have<19;)r.lens[q[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,x={bits:r.lenbits},M=s(0,r.lens,0,19,r.lencode,0,r.work,x),r.lenbits=x.bits,M){e.msg="invalid code lengths set",r.mode=d;break}r.have=0,r.mode=19;case 19:for(;r.have<r.nlen+r.ndist;){for(;D=(O=r.lencode[_&(1<<r.lenbits)-1])>>>16&255,k=65535&O,!((S=O>>>24)<=m);){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}if(k<16)_>>>=S,m-=S,r.lens[r.have++]=k;else {if(16===k){for(L=S+2;m<L;){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}if(_>>>=S,m-=S,0===r.have){e.msg="invalid bit length repeat",r.mode=d;break}N=r.lens[r.have-1],Q=3+(3&_),_>>>=2,m-=2;}else if(17===k){for(L=S+3;m<L;){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}m-=S,N=0,Q=3+(7&(_>>>=S)),_>>>=3,m-=3;}else {for(L=S+7;m<L;){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}m-=S,N=0,Q=11+(127&(_>>>=S)),_>>>=7,m-=7;}if(r.have+Q>r.nlen+r.ndist){e.msg="invalid bit length repeat",r.mode=d;break}for(;Q--;)r.lens[r.have++]=N;}}if(r.mode===d)break;if(0===r.lens[256]){e.msg="invalid code -- missing end-of-block",r.mode=d;break}if(r.lenbits=9,x={bits:r.lenbits},M=s(1,r.lens,0,r.nlen,r.lencode,0,r.work,x),r.lenbits=x.bits,M){e.msg="invalid literal/lengths set",r.mode=d;break}if(r.distbits=6,r.distcode=r.distdyn,x={bits:r.distbits},M=s(2,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,x),r.distbits=x.bits,M){e.msg="invalid distances set",r.mode=d;break}if(r.mode=20,6===t)break e;case 20:r.mode=21;case 21:if(C>=6&&I>=258){e.next_out=u,e.avail_out=I,e.next_in=h,e.avail_in=C,r.hold=_,r.bits=m,A(e,E),u=e.next_out,g=e.output,I=e.avail_out,h=e.next_in,f=e.input,C=e.avail_in,_=r.hold,m=r.bits,r.mode===c&&(r.back=-1);break}for(r.back=0;D=(O=r.lencode[_&(1<<r.lenbits)-1])>>>16&255,k=65535&O,!((S=O>>>24)<=m);){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}if(D&&!(240&D)){for(v=S,R=D,F=k;D=(O=r.lencode[F+((_&(1<<v+R)-1)>>v)])>>>16&255,k=65535&O,!(v+(S=O>>>24)<=m);){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}_>>>=v,m-=v,r.back+=v;}if(_>>>=S,m-=S,r.back+=S,r.length=k,0===D){r.mode=26;break}if(32&D){r.back=-1,r.mode=c;break}if(64&D){e.msg="invalid literal/length code",r.mode=d;break}r.extra=15&D,r.mode=22;case 22:if(r.extra){for(L=r.extra;m<L;){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}r.length+=_&(1<<r.extra)-1,_>>>=r.extra,m-=r.extra,r.back+=r.extra;}r.was=r.length,r.mode=23;case 23:for(;D=(O=r.distcode[_&(1<<r.distbits)-1])>>>16&255,k=65535&O,!((S=O>>>24)<=m);){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}if(!(240&D)){for(v=S,R=D,F=k;D=(O=r.distcode[F+((_&(1<<v+R)-1)>>v)])>>>16&255,k=65535&O,!(v+(S=O>>>24)<=m);){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}_>>>=v,m-=v,r.back+=v;}if(_>>>=S,m-=S,r.back+=S,64&D){e.msg="invalid distance code",r.mode=d;break}r.offset=k,r.extra=15&D,r.mode=24;case 24:if(r.extra){for(L=r.extra;m<L;){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}r.offset+=_&(1<<r.extra)-1,_>>>=r.extra,m-=r.extra,r.back+=r.extra;}if(r.offset>r.dmax){e.msg="invalid distance too far back",r.mode=d;break}r.mode=25;case 25:if(0===I)break e;if(Q=E-I,r.offset>Q){if((Q=r.offset-Q)>r.whave&&r.sane){e.msg="invalid distance too far back",r.mode=d;break}Q>r.wnext?(Q-=r.wnext,y=r.wsize-Q):y=r.wnext-Q,Q>r.length&&(Q=r.length),b=r.window;}else b=g,y=u-r.offset,Q=r.length;Q>I&&(Q=I),I-=Q,r.length-=Q;do{g[u++]=b[y++];}while(--Q);0===r.length&&(r.mode=21);break;case 26:if(0===I)break e;g[u++]=r.length,I--,r.mode=21;break;case 27:if(r.wrap){for(;m<32;){if(0===C)break e;C--,_|=f[h++]<<m,m+=8;}if(E-=I,e.total_out+=E,r.total+=E,E&&(e.adler=r.check=r.flags?a(r.check,g,E,u-E):i(r.check,g,E,u-E)),E=I,(r.flags?_:l(_))!==r.check){e.msg="incorrect data check",r.mode=d;break}_=0,m=0;}r.mode=28;case 28:if(r.wrap&&r.flags){for(;m<32;){if(0===C)break e;C--,_+=f[h++]<<m,m+=8;}if(_!==(4294967295&r.total)){e.msg="incorrect length check",r.mode=d;break}_=0,m=0;}r.mode=29;case 29:M=1;break e;case d:M=-3;break e;case 31:return -4;default:return o}return e.next_out=u,e.avail_out=I,e.next_in=h,e.avail_in=C,r.hold=_,r.bits=m,(r.wsize||E!==e.avail_out&&r.mode<d&&(r.mode<27||4!==t))&&p(e,e.output,e.next_out,E-e.avail_out)?(r.mode=31,-4):(w-=e.avail_in,E-=e.avail_out,e.total_in+=w,e.total_out+=E,r.total+=E,r.wrap&&E&&(e.adler=r.check=r.flags?a(r.check,g,E,e.next_out-E):i(r.check,g,E,e.next_out-E)),e.data_type=r.bits+(r.last?64:0)+(r.mode===c?128:0)+(20===r.mode||15===r.mode?256:0),(0===w&&0===E||4===t)&&0===M&&(M=-5),M)},t.inflateEnd=function(e){if(!e||!e.state)return o;var t=e.state;return t.window&&(t.window=null),e.state=null,0},t.inflateGetHeader=function(e,t){var r;return e&&e.state&&2&(r=e.state).wrap?(r.head=t,t.done=!1,0):o},t.inflateSetDictionary=function(e,t){var r,n=t.length;return e&&e.state?0!==(r=e.state).wrap&&11!==r.mode?o:11===r.mode&&i(1,t,n,0)!==r.check?-3:p(e,t,n,n)?(r.mode=31,-4):(r.havedict=1,0):o},t.inflateInfo="pako inflate (from Nodeca project)";},1998:(e,t,r)=>{var n=r(9805),i=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],a=[16,16,16,16,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,21,21,21,21,16,72,78],A=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0],s=[16,16,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,64,64];e.exports=function(e,t,r,o,c,d,l,f){var g,h,u,C,I,_,m,B,p,w=f.bits,E=0,Q=0,y=0,b=0,S=0,D=0,k=0,v=0,R=0,F=0,N=null,M=0,x=new n.Buf16(16),L=new n.Buf16(16),O=null,T=0;for(E=0;E<=15;E++)x[E]=0;for(Q=0;Q<o;Q++)x[t[r+Q]]++;for(S=w,b=15;b>=1&&0===x[b];b--);if(S>b&&(S=b),0===b)return c[d++]=20971520,c[d++]=20971520,f.bits=1,0;for(y=1;y<b&&0===x[y];y++);for(S<y&&(S=y),v=1,E=1;E<=15;E++)if(v<<=1,(v-=x[E])<0)return -1;if(v>0&&(0===e||1!==b))return -1;for(L[1]=0,E=1;E<15;E++)L[E+1]=L[E]+x[E];for(Q=0;Q<o;Q++)0!==t[r+Q]&&(l[L[t[r+Q]]++]=Q);if(0===e?(N=O=l,_=19):1===e?(N=i,M-=257,O=a,T-=257,_=256):(N=A,O=s,_=-1),F=0,Q=0,E=y,I=d,D=S,k=0,u=-1,C=(R=1<<S)-1,1===e&&R>852||2===e&&R>592)return 1;for(;;){m=E-k,l[Q]<_?(B=0,p=l[Q]):l[Q]>_?(B=O[T+l[Q]],p=N[M+l[Q]]):(B=96,p=0),g=1<<E-k,y=h=1<<D;do{c[I+(F>>k)+(h-=g)]=m<<24|B<<16|p;}while(0!==h);for(g=1<<E-1;F&g;)g>>=1;if(0!==g?(F&=g-1,F+=g):F=0,Q++,0==--x[E]){if(E===b)break;E=t[r+l[Q]];}if(E>S&&(F&C)!==u){for(0===k&&(k=S),I+=y,v=1<<(D=E-k);D+k<b&&!((v-=x[D+k])<=0);)D++,v<<=1;if(R+=1<<D,1===e&&R>852||2===e&&R>592)return 1;c[u=F&C]=S<<24|D<<16|I-d;}}return 0!==F&&(c[I+F]=E-k<<24|64<<16),f.bits=S,0};},4674:e=>{e.exports={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"};},3665:(e,t,r)=>{var n=r(9805);function i(e){for(var t=e.length;--t>=0;)e[t]=0;}var a=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],A=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],s=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],o=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],c=new Array(576);i(c);var d=new Array(60);i(d);var l=new Array(512);i(l);var f=new Array(256);i(f);var g=new Array(29);i(g);var h,u,C,I=new Array(30);function _(e,t,r,n,i){this.static_tree=e,this.extra_bits=t,this.extra_base=r,this.elems=n,this.max_length=i,this.has_stree=e&&e.length;}function m(e,t){this.dyn_tree=e,this.max_code=0,this.stat_desc=t;}function B(e){return e<256?l[e]:l[256+(e>>>7)]}function p(e,t){e.pending_buf[e.pending++]=255&t,e.pending_buf[e.pending++]=t>>>8&255;}function w(e,t,r){e.bi_valid>16-r?(e.bi_buf|=t<<e.bi_valid&65535,p(e,e.bi_buf),e.bi_buf=t>>16-e.bi_valid,e.bi_valid+=r-16):(e.bi_buf|=t<<e.bi_valid&65535,e.bi_valid+=r);}function E(e,t,r){w(e,r[2*t],r[2*t+1]);}function Q(e,t){var r=0;do{r|=1&e,e>>>=1,r<<=1;}while(--t>0);return r>>>1}function y(e,t,r){var n,i,a=new Array(16),A=0;for(n=1;n<=15;n++)a[n]=A=A+r[n-1]<<1;for(i=0;i<=t;i++){var s=e[2*i+1];0!==s&&(e[2*i]=Q(a[s]++,s));}}function b(e){var t;for(t=0;t<286;t++)e.dyn_ltree[2*t]=0;for(t=0;t<30;t++)e.dyn_dtree[2*t]=0;for(t=0;t<19;t++)e.bl_tree[2*t]=0;e.dyn_ltree[512]=1,e.opt_len=e.static_len=0,e.last_lit=e.matches=0;}function S(e){e.bi_valid>8?p(e,e.bi_buf):e.bi_valid>0&&(e.pending_buf[e.pending++]=e.bi_buf),e.bi_buf=0,e.bi_valid=0;}function D(e,t,r,n){var i=2*t,a=2*r;return e[i]<e[a]||e[i]===e[a]&&n[t]<=n[r]}function k(e,t,r){for(var n=e.heap[r],i=r<<1;i<=e.heap_len&&(i<e.heap_len&&D(t,e.heap[i+1],e.heap[i],e.depth)&&i++,!D(t,n,e.heap[i],e.depth));)e.heap[r]=e.heap[i],r=i,i<<=1;e.heap[r]=n;}function v(e,t,r){var n,i,s,o,c=0;if(0!==e.last_lit)do{n=e.pending_buf[e.d_buf+2*c]<<8|e.pending_buf[e.d_buf+2*c+1],i=e.pending_buf[e.l_buf+c],c++,0===n?E(e,i,t):(E(e,(s=f[i])+256+1,t),0!==(o=a[s])&&w(e,i-=g[s],o),E(e,s=B(--n),r),0!==(o=A[s])&&w(e,n-=I[s],o));}while(c<e.last_lit);E(e,256,t);}function R(e,t){var r,n,i,a=t.dyn_tree,A=t.stat_desc.static_tree,s=t.stat_desc.has_stree,o=t.stat_desc.elems,c=-1;for(e.heap_len=0,e.heap_max=573,r=0;r<o;r++)0!==a[2*r]?(e.heap[++e.heap_len]=c=r,e.depth[r]=0):a[2*r+1]=0;for(;e.heap_len<2;)a[2*(i=e.heap[++e.heap_len]=c<2?++c:0)]=1,e.depth[i]=0,e.opt_len--,s&&(e.static_len-=A[2*i+1]);for(t.max_code=c,r=e.heap_len>>1;r>=1;r--)k(e,a,r);i=o;do{r=e.heap[1],e.heap[1]=e.heap[e.heap_len--],k(e,a,1),n=e.heap[1],e.heap[--e.heap_max]=r,e.heap[--e.heap_max]=n,a[2*i]=a[2*r]+a[2*n],e.depth[i]=(e.depth[r]>=e.depth[n]?e.depth[r]:e.depth[n])+1,a[2*r+1]=a[2*n+1]=i,e.heap[1]=i++,k(e,a,1);}while(e.heap_len>=2);e.heap[--e.heap_max]=e.heap[1],function(e,t){var r,n,i,a,A,s,o=t.dyn_tree,c=t.max_code,d=t.stat_desc.static_tree,l=t.stat_desc.has_stree,f=t.stat_desc.extra_bits,g=t.stat_desc.extra_base,h=t.stat_desc.max_length,u=0;for(a=0;a<=15;a++)e.bl_count[a]=0;for(o[2*e.heap[e.heap_max]+1]=0,r=e.heap_max+1;r<573;r++)(a=o[2*o[2*(n=e.heap[r])+1]+1]+1)>h&&(a=h,u++),o[2*n+1]=a,n>c||(e.bl_count[a]++,A=0,n>=g&&(A=f[n-g]),s=o[2*n],e.opt_len+=s*(a+A),l&&(e.static_len+=s*(d[2*n+1]+A)));if(0!==u){do{for(a=h-1;0===e.bl_count[a];)a--;e.bl_count[a]--,e.bl_count[a+1]+=2,e.bl_count[h]--,u-=2;}while(u>0);for(a=h;0!==a;a--)for(n=e.bl_count[a];0!==n;)(i=e.heap[--r])>c||(o[2*i+1]!==a&&(e.opt_len+=(a-o[2*i+1])*o[2*i],o[2*i+1]=a),n--);}}(e,t),y(a,c,e.bl_count);}function F(e,t,r){var n,i,a=-1,A=t[1],s=0,o=7,c=4;for(0===A&&(o=138,c=3),t[2*(r+1)+1]=65535,n=0;n<=r;n++)i=A,A=t[2*(n+1)+1],++s<o&&i===A||(s<c?e.bl_tree[2*i]+=s:0!==i?(i!==a&&e.bl_tree[2*i]++,e.bl_tree[32]++):s<=10?e.bl_tree[34]++:e.bl_tree[36]++,s=0,a=i,0===A?(o=138,c=3):i===A?(o=6,c=3):(o=7,c=4));}function N(e,t,r){var n,i,a=-1,A=t[1],s=0,o=7,c=4;for(0===A&&(o=138,c=3),n=0;n<=r;n++)if(i=A,A=t[2*(n+1)+1],!(++s<o&&i===A)){if(s<c)do{E(e,i,e.bl_tree);}while(0!=--s);else 0!==i?(i!==a&&(E(e,i,e.bl_tree),s--),E(e,16,e.bl_tree),w(e,s-3,2)):s<=10?(E(e,17,e.bl_tree),w(e,s-3,3)):(E(e,18,e.bl_tree),w(e,s-11,7));s=0,a=i,0===A?(o=138,c=3):i===A?(o=6,c=3):(o=7,c=4);}}i(I);var M=!1;function x(e,t,r,i){w(e,0+(i?1:0),3),function(e,t,r){S(e),p(e,r),p(e,~r),n.arraySet(e.pending_buf,e.window,t,r,e.pending),e.pending+=r;}(e,t,r);}t._tr_init=function(e){M||(function(){var e,t,r,n,i,o=new Array(16);for(r=0,n=0;n<28;n++)for(g[n]=r,e=0;e<1<<a[n];e++)f[r++]=n;for(f[r-1]=n,i=0,n=0;n<16;n++)for(I[n]=i,e=0;e<1<<A[n];e++)l[i++]=n;for(i>>=7;n<30;n++)for(I[n]=i<<7,e=0;e<1<<A[n]-7;e++)l[256+i++]=n;for(t=0;t<=15;t++)o[t]=0;for(e=0;e<=143;)c[2*e+1]=8,e++,o[8]++;for(;e<=255;)c[2*e+1]=9,e++,o[9]++;for(;e<=279;)c[2*e+1]=7,e++,o[7]++;for(;e<=287;)c[2*e+1]=8,e++,o[8]++;for(y(c,287,o),e=0;e<30;e++)d[2*e+1]=5,d[2*e]=Q(e,5);h=new _(c,a,257,286,15),u=new _(d,A,0,30,15),C=new _(new Array(0),s,0,19,7);}(),M=!0),e.l_desc=new m(e.dyn_ltree,h),e.d_desc=new m(e.dyn_dtree,u),e.bl_desc=new m(e.bl_tree,C),e.bi_buf=0,e.bi_valid=0,b(e);},t._tr_stored_block=x,t._tr_flush_block=function(e,t,r,n){var i,a,A=0;e.level>0?(2===e.strm.data_type&&(e.strm.data_type=function(e){var t,r=4093624447;for(t=0;t<=31;t++,r>>>=1)if(1&r&&0!==e.dyn_ltree[2*t])return 0;if(0!==e.dyn_ltree[18]||0!==e.dyn_ltree[20]||0!==e.dyn_ltree[26])return 1;for(t=32;t<256;t++)if(0!==e.dyn_ltree[2*t])return 1;return 0}(e)),R(e,e.l_desc),R(e,e.d_desc),A=function(e){var t;for(F(e,e.dyn_ltree,e.l_desc.max_code),F(e,e.dyn_dtree,e.d_desc.max_code),R(e,e.bl_desc),t=18;t>=3&&0===e.bl_tree[2*o[t]+1];t--);return e.opt_len+=3*(t+1)+5+5+4,t}(e),i=e.opt_len+3+7>>>3,(a=e.static_len+3+7>>>3)<=i&&(i=a)):i=a=r+5,r+4<=i&&-1!==t?x(e,t,r,n):4===e.strategy||a===i?(w(e,2+(n?1:0),3),v(e,c,d)):(w(e,4+(n?1:0),3),function(e,t,r,n){var i;for(w(e,t-257,5),w(e,r-1,5),w(e,n-4,4),i=0;i<n;i++)w(e,e.bl_tree[2*o[i]+1],3);N(e,e.dyn_ltree,t-1),N(e,e.dyn_dtree,r-1);}(e,e.l_desc.max_code+1,e.d_desc.max_code+1,A+1),v(e,e.dyn_ltree,e.dyn_dtree)),b(e),n&&S(e);},t._tr_tally=function(e,t,r){return e.pending_buf[e.d_buf+2*e.last_lit]=t>>>8&255,e.pending_buf[e.d_buf+2*e.last_lit+1]=255&t,e.pending_buf[e.l_buf+e.last_lit]=255&r,e.last_lit++,0===t?e.dyn_ltree[2*r]++:(e.matches++,t--,e.dyn_ltree[2*(f[r]+256+1)]++,e.dyn_dtree[2*B(t)]++),e.last_lit===e.lit_bufsize-1},t._tr_align=function(e){w(e,2,3),E(e,256,c),function(e){16===e.bi_valid?(p(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):e.bi_valid>=8&&(e.pending_buf[e.pending++]=255&e.bi_buf,e.bi_buf>>=8,e.bi_valid-=8);}(e);};},4442:e=>{e.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0;};},7392:e=>{class t{constructor(e={}){if(!(e.maxSize&&e.maxSize>0))throw new TypeError("`maxSize` must be a number greater than 0");this.maxSize=e.maxSize,this.cache=new Map,this.oldCache=new Map,this._size=0;}_set(e,t){this.cache.set(e,t),this._size++,this._size>=this.maxSize&&(this._size=0,this.oldCache=this.cache,this.cache=new Map);}get(e){if(this.cache.has(e))return this.cache.get(e);if(this.oldCache.has(e)){const t=this.oldCache.get(e);return this.oldCache.delete(e),this._set(e,t),t}}set(e,t){return this.cache.has(e)?this.cache.set(e,t):this._set(e,t),this}has(e){return this.cache.has(e)||this.oldCache.has(e)}peek(e){return this.cache.has(e)?this.cache.get(e):this.oldCache.has(e)?this.oldCache.get(e):void 0}delete(e){const t=this.cache.delete(e);return t&&this._size--,this.oldCache.delete(e)||t}clear(){this.cache.clear(),this.oldCache.clear(),this._size=0;}*keys(){for(const[e]of this)yield e;}*values(){for(const[,e]of this)yield e;}*[Symbol.iterator](){for(const e of this.cache)yield e;for(const e of this.oldCache){const[t]=e;this.cache.has(t)||(yield e);}}get size(){let e=0;for(const t of this.oldCache.keys())this.cache.has(t)||e++;return this._size+e}}e.exports=t;},1307:function(e,t,r){var n;n=e=>(()=>{var t=[,e=>{e.exports="data:application/wasm;base64, ";},t=>{t.exports=e;}],r={};function n(e){var i=r[e];if(void 0!==i)return i.exports;var a=r[e]={exports:{}};return t[e](a,a.exports,n),a.exports}n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]});},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0});};var i={};return (()=>{n.r(i),n.d(i,{XzReadableStream:()=>a});var e=n(1);const t=globalThis.ReadableStream||n(2).ReadableStream;class r{constructor(e){this.exports=e.exports,this.memory=this.exports.memory,this.ptr=this.exports.create_context(),this._refresh(),this.bufSize=this.mem32[0],this.inStart=this.mem32[1]-this.ptr,this.inEnd=this.inStart+this.bufSize,this.outStart=this.mem32[4]-this.ptr;}supplyInput(e){this._refresh(),this.mem8.subarray(this.inStart,this.inEnd).set(e,0),this.exports.supply_input(this.ptr,e.byteLength),this._refresh();}getNextOutput(){const e=this.exports.get_next_output(this.ptr);if(this._refresh(),0!==e&&1!==e)throw new Error(`get_next_output failed with error code ${e}`);return {outChunk:this.mem8.slice(this.outStart,this.outStart+this.mem32[5]),finished:1===e}}needsMoreInput(){return this.mem32[2]===this.mem32[3]}outputBufferIsFull(){return this.mem32[5]===this.bufSize}resetOutputBuffer(){this.outPos=this.mem32[5]=0;}dispose(){this.exports.destroy_context(this.ptr),this.exports=null;}_refresh(){this.memory.buffer!==this.mem8?.buffer&&(this.mem8=new Uint8Array(this.memory.buffer,this.ptr),this.mem32=new Uint32Array(this.memory.buffer,this.ptr));}}class a extends t{static _moduleInstancePromise;static _moduleInstance;static async _getModuleInstance(){const t=e.replace("data:application/wasm;base64,",""),r=Uint8Array.from(atob(t),(e=>e.charCodeAt(0))).buffer,n=await WebAssembly.instantiate(r,{});a._moduleInstance=n.instance;}constructor(e){let t,n=null;const i=e.getReader();super({async start(e){a._moduleInstance||await(a._moduleInstancePromise||(a._moduleInstancePromise=a._getModuleInstance())),t=new r(a._moduleInstance);},async pull(e){if(t.needsMoreInput()){if(null===n||0===n.byteLength){const{done:e,value:t}=await i.read();e||(n=t);}const e=Math.min(t.bufSize,n.byteLength);t.supplyInput(n.subarray(0,e)),n=n.subarray(e);}const r=t.getNextOutput();e.enqueue(r.outChunk),t.resetOutputBuffer(),r.finished&&(t.dispose(),e.close());},cancel:()=>(t.dispose(),i.cancel())});}}})(),i})(),e.exports=n(r(772));},7937:()=>{},772:()=>{},86:(e,t,r)=>{const n=r(1308).A;e.exports=n,e.exports.default=n;},1308:(e,t)=>{let r=[0,1996959894,3993919788,2567524794,124634137,1886057615,3915621685,2657392035,249268274,2044508324,3772115230,2547177864,162941995,2125561021,3887607047,2428444049,498536548,1789927666,4089016648,2227061214,450548861,1843258603,4107580753,2211677639,325883990,1684777152,4251122042,2321926636,335633487,1661365465,4195302755,2366115317,997073096,1281953886,3579855332,2724688242,1006888145,1258607687,3524101629,2768942443,901097722,1119000684,3686517206,2898065728,853044451,1172266101,3705015759,2882616665,651767980,1373503546,3369554304,3218104598,565507253,1454621731,3485111705,3099436303,671266974,1594198024,3322730930,2970347812,795835527,1483230225,3244367275,3060149565,1994146192,31158534,2563907772,4023717930,1907459465,112637215,2680153253,3904427059,2013776290,251722036,2517215374,3775830040,2137656763,141376813,2439277719,3865271297,1802195444,476864866,2238001368,4066508878,1812370925,453092731,2181625025,4111451223,1706088902,314042704,2344532202,4240017532,1658658271,366619977,2362670323,4224994405,1303535960,984961486,2747007092,3569037538,1256170817,1037604311,2765210733,3554079995,1131014506,879679996,2909243462,3663771856,1141124467,855842277,2852801631,3708648649,1342533948,654459306,3188396048,3373015174,1466479909,544179635,3110523913,3462522015,1591671054,702138776,2966460450,3352799412,1504918807,783551873,3082640443,3233442989,3988292384,2596254646,62317068,1957810842,3939845945,2647816111,81470997,1943803523,3814918930,2489596804,225274430,2053790376,3826175755,2466906013,167816743,2097651377,4027552580,2265490386,503444072,1762050814,4150417245,2154129355,426522225,1852507879,4275313526,2312317920,282753626,1742555852,4189708143,2394877945,397917763,1622183637,3604390888,2714866558,953729732,1340076626,3518719985,2797360999,1068828381,1219638859,3624741850,2936675148,906185462,1090812512,3747672003,2825379669,829329135,1181335161,3412177804,3160834842,628085408,1382605366,3423369109,3138078467,570562233,1426400815,3317316542,2998733608,733239954,1555261956,3268935591,3050360625,752459403,1541320221,2607071920,3965973030,1969922972,40735498,2617837225,3943577151,1913087877,83908371,2512341634,3803740692,2075208622,213261112,2463272603,3855990285,2094854071,198958881,2262029012,4057260610,1759359992,534414190,2176718541,4139329115,1873836001,414664567,2282248934,4279200368,1711684554,285281116,2405801727,4167216745,1634467795,376229701,2685067896,3608007406,1308918612,956543938,2808555105,3495958263,1231636301,1047427035,2932959818,3654703836,1088359270,936918e3,2847714899,3736837829,1202900863,817233897,3183342108,3401237130,1404277552,615818150,3134207493,3453421203,1423857449,601450431,3009837614,3294710456,1567103746,711928724,3020668471,3272380065,1510334235,755167117];"undefined"!=typeof Int32Array&&(r=new Int32Array(r)),t.A=(e,t)=>{let n=0===t?0:~t;for(let t=0;t<e.length;t++)n=r[255&(n^e[t])]^n>>>8;return ~n};}},t={};function r(n){var i=t[n];if(void 0!==i)return i.exports;var a=t[n]={exports:{}};return e[n].call(a.exports,a,a.exports,r),a.exports}r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]});},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0});};var n=r(7806);return n})();

class FileHandler {

    constructor(source, config) {
        this.position = 0;
        this.url = source;
        this.config = config;
        if (isFile$1(source) || config.cacheFetches === false) {
            this.useCache = false;
        } else {
            this.useCache = true;
            this.cache = new Cache({
                fetch: (start, length) => this._fetch(start, length),
                fetchSize: config.fetchSize || 10000
            });
        }
    }

    async _fetch(position, length) {
        const loadRange = {start: position, size: length};
        const arrayBuffer = await igvxhr$1.loadArrayBuffer(this.url, buildOptions(this.config, {range: loadRange}));
        return arrayBuffer
    }

    async read(length, position = 0) {

        let buf;
        if (this.useCache) {
            buf = await this.cache.get(position, length);
        } else {
            buf = await this._fetch(position, length);
        }
        return new Uint8Array(buf)
    }

    async readFile() {
        const arrayBuffer = await igvxhr$1.loadArrayBuffer(this.url, buildOptions(this.config));
        return new Uint8Array(arrayBuffer)
    }

}

/**
 * A crude cache designed for observed access patterns of the cram-js library for cram files
 */

class Cache {

    maxChunkCount = 5
    chunks = []

    constructor({fetch, fetchSize = 30000}) {
        this.fetch = fetch;
        this.fetchSize = fetchSize;
    }

    async get(start, length) {

        const end = start + length;
        for (let c of this.chunks) {
           // console.log("Cache hit")
            if (c.contains(start, end)) {
                const offset = start - c.start;
                return c.buffer.slice(offset, offset + length)
            }
        }

        //console.log("Cache miss")
        const l = Math.max(length, this.fetchSize);
        const s = Math.max(0, start - 1000);
        const e = start + l + 1000;
        const buffer = await this.fetch(s, e - s);
        const c = new Chunk(s, e, buffer);
        if (this.chunks.length > this.maxChunkCount) this.chunks.shift();
        this.chunks.push(c);

        const bufferStart = start - c.start;
        const bufferEnd = bufferStart + length;
        return buffer.slice(bufferStart, bufferEnd)

    }
}

class Chunk {

    constructor(start, end, buffer) {
        this.start = start;
        this.end = end;
        this.buffer = buffer;
    }

    contains(start, end) {
        return start >= this.start && end <= this.end
    }
}

const READ_STRAND_FLAG = 0x10;
const MATE_STRAND_FLAG = 0x20;

const CRAM_MATE_STRAND_FLAG = 0x1;

/**
 * Class for reading a cram file.  Wraps the gMOD Cram package.
 *
 * @param config
 * @constructor
 */
class CramReader {

    chrAliasTable = new Map()

    constructor(config, genome, browser) {

        this.config = config;
        this.browser = browser;
        this.genome = genome;

        this.cramFile = new gmodCRAM.CramFile({
            filehandle: config.fileHandle ? config.fileHandle : new FileHandler(config.url, config),
            //url: config.url,
            seqFetch: config.seqFetch || seqFetch.bind(this),
            checkSequenceMD5: config.checkSequenceMD5 !== undefined ? config.checkSequenceMD5 : true
        });

        const indexFileHandle = config.indexFileHandle ? config.indexFileHandle : new FileHandler(config.indexURL, config);
        this.indexedCramFile = new gmodCRAM.IndexedCramFile({
            cram: this.cramFile,
            index: new gmodCRAM.CraiIndex({
                //url: config.indexURL
                filehandle: indexFileHandle
            }),
            fetchSizeLimit: config.fetchSizeLimit || 1000000000
        });

        BamUtils.setReaderDefaults(this, config);

        async function seqFetch(seqID, start, end) {
            const genome = this.genome;
            const header = await this.getHeader();
            const chr = genome.getChromosomeName(header.indexToChr[seqID]);
            return this.genome.getSequence(chr, start - 1, end)
        }
    }


    /**
     * Parse the sequence dictionary from the SAM header and build chr name tables.
     */

    async getHeader() {

        if (!this.header) {
            const samHeader = await this.cramFile.getSamHeader();
            const chrToIndex = {};
            const indexToChr = [];
            const readGroups = [];

            for (let line of samHeader) {
                if ('SQ' === line.tag) {
                    for (let d of line.data) {
                        if (d.tag === "SN") {
                            const seq = d.value;
                            chrToIndex[seq] = indexToChr.length;
                            indexToChr.push(seq);
                            break
                        }
                    }
                } else if ('RG' === line.tag) {
                    readGroups.push(line.data);
                }
            }

            this.header = {
                indexToChr: indexToChr,
                chrToIndex: chrToIndex,
                chrNames: Object.keys(chrToIndex),
                readGroups: readGroups

            };
        }

        return this.header
    }

    async #getRefId(chr) {

        await this.getHeader();

        if (this.chrAliasTable.has(chr)) {
            chr = this.chrAliasTable.get(chr);
            if (chr === undefined) {
                return undefined
            }
        }

        let refId = this.header.chrToIndex[chr];

        // Try alias
        if (refId === undefined) {
            const aliasRecord = await this.genome.getAliasRecord(chr);
            let alias;
            if (aliasRecord) {
                const aliases = Object.keys(aliasRecord)
                    .filter(k => k !== "start" && k !== "end")
                    .map(k => aliasRecord[k])
                    .filter(a => undefined !== this.header.chrToIndex[a]);
                if (aliases.length > 0) {
                    alias = aliases[0];
                    refId = this.header.chrToIndex[aliases[0]];
                }
            }
            this.chrAliasTable.set(chr, alias);  // alias may be undefined => no alias exists. Setting prevents repeated attempts
        }
        return refId
    }


    async readAlignments(chr, bpStart, bpEnd) {

        const header = await this.getHeader();

        const chrIdx = await this.#getRefId(chr);

        const alignmentContainer = new AlignmentContainer(chr, bpStart, bpEnd, this.config);

        if (chrIdx === undefined) {
            return alignmentContainer

        } else {

            try {
                const records = await this.indexedCramFile.getRecordsForRange(chrIdx, bpStart, bpEnd);

                for (let record of records) {

                    const refID = record.sequenceId;
                    const pos = record.alignmentStart;
                    const alignmentEnd = pos + record.lengthOnRef;

                    if (refID < 0) {
                        continue   // unmapped read
                    } else if (refID > chrIdx || pos > bpEnd) {
                        return    // off right edge, we're done
                    } else if (refID < chrIdx) {
                        continue   // Sequence to left of start, not sure this is possible
                    }
                    if (alignmentEnd < bpStart) {
                        continue
                    }  // Record out-of-range "to the left", skip to next one

                    const alignment = decodeCramRecord(record, header.chrNames);

                    if (this.filter.pass(alignment)) {
                        alignmentContainer.push(alignment);
                    }
                }

                alignmentContainer.finish();

                return alignmentContainer
            } catch (error) {
                let message = error.message;
                if (message && message.indexOf("MD5") >= 0) {
                    message = "Sequence mismatch. Is this the correct genome for the loaded CRAM?";
                }
                this.browser.alert.present(new Error(message));
                throw error
            }
        }

        function decodeCramRecord(record, chrNames) {

            const alignment = new BamAlignment();

            alignment.chr = chrNames[record.sequenceId];
            alignment.start = record.alignmentStart - 1;
            alignment.lengthOnRef = record.lengthOnRef;
            alignment.flags = record.flags;
            alignment.strand = !(record.flags & READ_STRAND_FLAG);
            alignment.fragmentLength = record.templateLength || record.templateSize;
            alignment.mq = record.mappingQuality;
            alignment.end = record.alignmentStart + record.lengthOnRef;
            alignment.readGroupId = record.readGroupId;

            if (record.mate && record.mate.sequenceId !== undefined) {
                const strand = record.mate.flags !== undefined ?
                    !(record.mate.flags & CRAM_MATE_STRAND_FLAG) :
                    !(record.flags & MATE_STRAND_FLAG);

                alignment.mate = {
                    chr: chrNames[record.mate.sequenceId],
                    position: record.mate.alignmentStart,
                    strand: strand
                };
            }

            alignment.seq = record.getReadBases();
            alignment.qual = record.qualityScores;
            alignment.tagDict = record.tags;
            alignment.readName = record.readName;

            // TODO -- cigar encoded in tag?
            // BamUtils.bam_tag2cigar(ba, blockEnd, p, lseq, alignment, cigarArray);

            makeBlocks(record, alignment);

            if (alignment.mate && alignment.start > alignment.mate.position && alignment.fragmentLength > 0) {
                alignment.fragmentLength = -alignment.fragmentLength;
            }

            BamUtils.setPairOrientation(alignment);

            return alignment

        }

        function makeBlocks(cramRecord, alignment) {

            const blocks = [];
            let insertions;
            let gaps;
            let basesUsed = 0;
            let cigarString = '';

            alignment.scStart = alignment.start;
            alignment.scLengthOnRef = alignment.lengthOnRef;

            if (cramRecord.readFeatures) {

                for (let feature of cramRecord.readFeatures) {

                    const code = feature.code;
                    const data = feature.data;
                    const readPos = feature.pos - 1;
                    const refPos = feature.refPos - 1;

                    switch (code) {
                        case 'S' :
                        case 'I':
                        case 'i':
                        case 'N':
                        case 'D':
                            if (readPos > basesUsed) {
                                const len = readPos - basesUsed;
                                blocks.push(new AlignmentBlock({
                                    start: refPos - len,
                                    seqOffset: basesUsed,
                                    len: len,
                                    type: 'M'
                                }));
                                basesUsed += len;
                                cigarString += len + 'M';
                            }

                            if ('S' === code) {
                                let scPos = refPos;
                                alignment.scLengthOnRef += data.length;
                                if (readPos === 0) {
                                    alignment.scStart -= data.length;
                                    scPos -= data.length;
                                }
                                const len = data.length;
                                blocks.push(new AlignmentBlock({
                                    start: scPos,
                                    seqOffset: basesUsed,
                                    len: len,
                                    type: 'S'
                                }));
                                basesUsed += len;
                                cigarString += len + code;
                            } else if ('I' === code || 'i' === code) {
                                if (insertions === undefined) {
                                    insertions = [];
                                }
                                const len = 'i' === code ? 1 : data.length;
                                insertions.push(new AlignmentBlock({
                                    start: refPos,
                                    len: len,
                                    seqOffset: basesUsed,
                                    type: 'I'
                                }));
                                basesUsed += len;
                                cigarString += len + code;
                            } else if ('D' === code || 'N' === code) {
                                if (!gaps) {
                                    gaps = [];
                                }
                                gaps.push({
                                    start: refPos,
                                    len: data,
                                    type: code
                                });
                                cigarString += data + code;
                            }
                            break

                        case 'H':
                        case 'P':
                            cigarString += data + code;
                            break
                        //  Ignore
                    }
                }
            }

            // Last block
            const len = cramRecord.readLength - basesUsed;
            if (len > 0) {
                blocks.push(new AlignmentBlock({
                    start: cramRecord.alignmentStart + cramRecord.lengthOnRef - len - 1,
                    seqOffset: basesUsed,
                    len: len,
                    type: 'M'
                }));

                cigarString += len + 'M';
            }

            alignment.blocks = blocks;
            alignment.insertions = insertions;
            alignment.gaps = gaps;
            alignment.cigar = cigarString;

        }

    }
}

const BIGWIG_MAGIC_LTH = 0x888FFC26; // BigWig Magic Low to High
const BIGBED_MAGIC_LTH = 0x8789F2EB; // BigBed Magic Low to High

const TDF_MAGIC = [84, 68, 70, 52];
const BAM_MAGIC = new Uint8Array([0x42, 0x41, 0x4d, 0x01]);
const CRAM_MAGIC = [67, 82, 65, 77];
const GZIP_MAGIC = [31, 139];
const FEXTRA$2 = 4;


const knownFileExtensions = new Set([

    "narrowpeak",
    "broadpeak",
    "regionpeak",
    "peaks",
    "bedgraph",
    "wig",
    "gff3",
    "gff",
    "gtf",
    "fusionjuncspan",
    "refflat",
    "seg",
    "aed",
    "bed",
    "bedMethyl",
    "vcf",
    "bb",
    "bigbed",
    "biginteract",
    "biggenepred",
    "bignarrowpeak",
    "bw",
    "bigwig",
    "bam",
    "tdf",
    "refgene",
    "genepred",
    "genepredext",
    "bedpe",
    "bp",
    "snp",
    "rmsk",
    "cram",
    "gwas",
    "maf",
    "mut",
    "hiccups",
    "fasta",
    "fa",
    "fna",
    "pytor",
    "hic",
    "qtl"
]);

function compareArrays(a, b) {
    const len = Math.min(a.length, b.length);
    if(len == 0) return false
    for (let i = 0; i < len; i++) {
        if (a[i] !== b[i]) {
            return false
        }
    }
    return true

}

async function inferFileFormat(config) {

    let format;

    // First try determining format from file extension
    const filename = config.filename || await getFilename$1(config.url);
    if(filename) {
        format = await inferFileFormatFromName(filename);
    }

    // Try determining from first few bytes of file
    if (!format) {
        format = await inferFileFormatFromContents(config);
    }
    return format

}

function inferFileFormatFromName(fn) {

    if (!fn) {
        return
    }
    fn = fn.toLowerCase();

    // Special case -- UCSC refgene files
    if (fn.endsWith("refgene.txt.gz") ||
        fn.endsWith("refgene.txt.bgz") ||
        fn.endsWith("refgene.txt") ||
        fn.endsWith("refgene.sorted.txt.gz") ||
        fn.endsWith("refgene.sorted.txt.bgz")) {
        return "refgene"
    }

    // String gzip extension
    if (fn.endsWith(".gz")) {
        fn = fn.substring(0, fn.length - 3);
    }
    if (fn.endsWith(".bgz")) {
        fn = fn.substring(0, fn.length - 4);
    }

    //Strip aux extensions .tsv, .tab, and .txt
    if (fn.endsWith(".txt") || fn.endsWith(".tab") || fn.endsWith(".tsv")) {
        fn = fn.substring(0, fn.length - 4);
    }

    const idx = fn.lastIndexOf(".");
    const ext = idx < 0 ? fn : fn.substring(idx + 1);

    switch (ext) {
        case "bw":
            return "bigwig"
        case "bb":
            return "bigbed"
        case "fasta":
        case "fa":
        case "fna":
            return "fasta"
        default:
            if (knownFileExtensions.has(ext)) {
                return ext
            } else {
                return undefined
            }

    }

}

function inferIndexPath(url, extension) {

    if (isString$3(url)) {
        if (url.includes("?")) {
            const idx = url.indexOf("?");
            return url.substring(0, idx) + "." + extension + url.substring(idx)
        } else {
            return url + "." + extension
        }
    } else {
        return undefined
    }
}


/**
 * Attempt to infer the file format from the first 1000 bytes.
 *
 * @param {url}
 * @returns {Promise<void>}
 */
async function inferFileFormatFromContents(config) {

    const url = config.url;
    let options = buildOptions(config, {range: {start: 0, size: 1000}});
    let data = await igvxhr$1.loadArrayBuffer(url, options);

    let bytes = new Uint8Array(data);
    if (compareArrays(bytes, GZIP_MAGIC)) {

        const b = bytes[3] & FEXTRA$2;
        if (b !== 0 && bytes[12] === 66 && bytes[13] === 67) {
            // This is BGZIPPED, read the first block
            const bssize = bgzBlockSize$1(data);
            options = buildOptions(config, {range: {start: 0, size: bssize}});
            data = await igvxhr$1.loadArrayBuffer(url, options);
            bytes = unbgzf$1(data);
        } else {
            // Not BGZipped, we need to read the entire file.  But we do anyway
            options = buildOptions(config, {});
            data = await igvxhr$1.loadArrayBuffer(url, options);
            bytes = ungzip_blocks$1(data);
            config._filecontents = bytes;

        }
    }

    // BAM and CRAM

    if (compareArrays(bytes, BAM_MAGIC)) {
        return "bam"
    }

    if (compareArrays(bytes, CRAM_MAGIC)) {
        return "cram"
    }

    // BIGWIG and BIGBED
    const littleEndian = true;
    let binaryParser = new BinaryParser$1(new DataView(data), littleEndian);
    let magic = binaryParser.getUInt();

    if (magic === BIGWIG_MAGIC_LTH) {
        return "bigwig"
    } else if (magic === BIGBED_MAGIC_LTH) {
        return "bigbed"
    }

    // TDF
    if (compareArrays(bytes, TDF_MAGIC)) {
        return "tdf"
    }

    // Text formats
    const decoder = new TextDecoder("utf-8");
    const contents = decoder.decode(bytes);

    const lines = contents.split(/\r?\n/);
    const firstLine = lines[0];

    if (firstLine.startsWith("##fileformat=VCF")) {
        return "vcf"
    }
    if (firstLine.startsWith("##gff-version 3")) {
        return "gff3"
    }
    if (firstLine.startsWith("##gff-version")) {
        return "gff"
    }
    if(firstLine.startsWith("##fileformat=")) {
        return firstLine.substring(13).toLowerCase();   // Non standard extension of VCF convention
    }


    // QTL test must preceed GWAS test as GWAS files will also pass the QTL test
    if (QTLParser.isQTL(firstLine)) {
        return "qtl"
    }
    if (GWASParser.isGWAS(firstLine)) {
        return "gwas"
    }

    const columnNames = firstLine.split('\t');
    if (isHiccups(columnNames)) {
        return "hiccups"
    }

    // Format unknown
    return null
}

class BamSource {

    constructor(config, browser) {

        const genome = browser.genome;

        this.config = config;
        this.genome = genome;

        if (isDataURL(config.url)) {
            this.config.indexed = false;
        }

        if ("ga4gh" === config.sourceType) {
            throw Error("Unsupported source type 'ga4gh'")
        } else if ("pysam" === config.sourceType) {
            this.bamReader = new BamWebserviceReader(config, genome);
        } else if ("htsget" === config.sourceType) {
            this.bamReader = new HtsgetBamReader(config, genome);
        } else if ("shardedBam" === config.sourceType) {
            this.bamReader = new ShardedBamReader(config, genome);
        } else if ("cram" === config.format) {
            this.bamReader = new CramReader(config, genome, browser);
        } else {
            if (!this.config.indexURL && config.indexed !== false) {
                if (isString$3(this.config.url)) {
                    const indexPath = inferIndexPath(this.config.url, "bai");
                    if (indexPath) {
                        console.warn(`Warning: no indexURL specified for ${this.config.url}.  Guessing ${indexPath}`);
                        this.config.indexURL = indexPath;
                    } else {
                        console.warning(`Warning: no indexURL specified for ${this.config.url}.`);
                        this.config.indexed = false;
                    }
                } else {
                    console.warning(`Warning: no indexURL specified for ${this.config.name}.`);
                    this.config.indexed = false;
                }
            }

            if (this.config.indexed !== false) { // && this.config.indexURL) {
                this.bamReader = new BamReader(config, genome);
            } else {
                this.bamReader = new BamReaderNonIndexed(config, genome);
            }
        }
    }


    async getAlignments(chr, bpStart, bpEnd) {

        const genome = this.genome;

        const alignmentContainer = await this.bamReader.readAlignments(chr, bpStart, bpEnd);

        if (alignmentContainer.hasAlignments) {
            const sequence = await genome.getSequence(chr, alignmentContainer.start, alignmentContainer.end);
            if (sequence) {
                alignmentContainer.coverageMap.refSeq = sequence;    // TODO -- fix this
                alignmentContainer.sequence = sequence;           // TODO -- fix this
                return alignmentContainer
            } else {
                console.error("No sequence for: " + chr + ":" + alignmentContainer.start + "-" + alignmentContainer.end);
            }
        }
        return alignmentContainer

    }
}

class Locus {

    constructor({chr, start, end}) {
        this.chr = chr;
        this.start = start;
        this.end = end;
    }


    contains(locus) {
        return locus.chr === this.chr && locus.start >= this.start && locus.end <= this.end
    }

    overlaps(locus) {
        return locus.chr === this.chr && !(locus.end < this.start || locus.start > this.end)
    }

    extend(l) {
        if (l.chr !== this.chr) return
        this.start = Math.min(l.start, this.start);
        this.end = Math.max(l.end, this.end);
    }

    getLocusString() {
        if ('all' === this.chr) {
            return 'all'
        } else {
            const ss = numberFormatter$1(Math.floor(this.start) + 1);
            const ee = numberFormatter$1(Math.round(this.end));
            return `${this.chr}:${ss}-${ee}`
        }
    }

    static fromLocusString(str) {
        if ('all' === str) {
            return new Locus({chr: 'all'})
        }
        const parts = str.split(':');
        const chr = parts[0];
        const se = parts[1].split("-");
        const start = Number.parseInt(se[0].replace(/,/g, "")) - 1;
        const end = Number.parseInt(se[1].replace(/,/g, ""));
        return new Locus({chr, start, end})
    }
}

/*!
 * vanilla-picker v2.12.1
 * https://vanilla-picker.js.org
 *
 * Copyright 2017-2021 Andreas Borgen (https://github.com/Sphinxxxx), Adam Brooks (https://github.com/dissimulate)
 * Released under the ISC license.
 */
var classCallCheck = function (instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
};

var createClass = function () {
  function defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i];
      descriptor.enumerable = descriptor.enumerable || false;
      descriptor.configurable = true;
      if ("value" in descriptor) descriptor.writable = true;
      Object.defineProperty(target, descriptor.key, descriptor);
    }
  }

  return function (Constructor, protoProps, staticProps) {
    if (protoProps) defineProperties(Constructor.prototype, protoProps);
    if (staticProps) defineProperties(Constructor, staticProps);
    return Constructor;
  };
}();

var slicedToArray = function () {
  function sliceIterator(arr, i) {
    var _arr = [];
    var _n = true;
    var _d = false;
    var _e = undefined;

    try {
      for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
        _arr.push(_s.value);

        if (i && _arr.length === i) break;
      }
    } catch (err) {
      _d = true;
      _e = err;
    } finally {
      try {
        if (!_n && _i["return"]) _i["return"]();
      } finally {
        if (_d) throw _e;
      }
    }

    return _arr;
  }

  return function (arr, i) {
    if (Array.isArray(arr)) {
      return arr;
    } else if (Symbol.iterator in Object(arr)) {
      return sliceIterator(arr, i);
    } else {
      throw new TypeError("Invalid attempt to destructure non-iterable instance");
    }
  };
}();

String.prototype.startsWith = String.prototype.startsWith || function (needle) {
    return this.indexOf(needle) === 0;
};
String.prototype.padStart = String.prototype.padStart || function (len, pad) {
    var str = this;while (str.length < len) {
        str = pad + str;
    }return str;
};

var colorNames = { cb: '0f8ff', tqw: 'aebd7', q: '-ffff', qmrn: '7fffd4', zr: '0ffff', bg: '5f5dc', bsq: 'e4c4', bck: '---', nch: 'ebcd', b: '--ff', bvt: '8a2be2', brwn: 'a52a2a', brw: 'deb887', ctb: '5f9ea0', hrt: '7fff-', chcT: 'd2691e', cr: '7f50', rnw: '6495ed', crns: '8dc', crms: 'dc143c', cn: '-ffff', Db: '--8b', Dcn: '-8b8b', Dgnr: 'b8860b', Dgr: 'a9a9a9', Dgrn: '-64-', Dkhk: 'bdb76b', Dmgn: '8b-8b', Dvgr: '556b2f', Drng: '8c-', Drch: '9932cc', Dr: '8b--', Dsmn: 'e9967a', Dsgr: '8fbc8f', DsTb: '483d8b', DsTg: '2f4f4f', Dtrq: '-ced1', Dvt: '94-d3', ppnk: '1493', pskb: '-bfff', mgr: '696969', grb: '1e90ff', rbrc: 'b22222', rwht: 'af0', stg: '228b22', chs: '-ff', gnsb: 'dcdcdc', st: '8f8ff', g: 'd7-', gnr: 'daa520', gr: '808080', grn: '-8-0', grnw: 'adff2f', hnw: '0fff0', htpn: '69b4', nnr: 'cd5c5c', ng: '4b-82', vr: '0', khk: '0e68c', vnr: 'e6e6fa', nrb: '0f5', wngr: '7cfc-', mnch: 'acd', Lb: 'add8e6', Lcr: '08080', Lcn: 'e0ffff', Lgnr: 'afad2', Lgr: 'd3d3d3', Lgrn: '90ee90', Lpnk: 'b6c1', Lsmn: 'a07a', Lsgr: '20b2aa', Lskb: '87cefa', LsTg: '778899', Lstb: 'b0c4de', Lw: 'e0', m: '-ff-', mgrn: '32cd32', nn: 'af0e6', mgnt: '-ff', mrn: '8--0', mqm: '66cdaa', mmb: '--cd', mmrc: 'ba55d3', mmpr: '9370db', msg: '3cb371', mmsT: '7b68ee', '': '-fa9a', mtr: '48d1cc', mmvt: 'c71585', mnLb: '191970', ntc: '5fffa', mstr: 'e4e1', mccs: 'e4b5', vjw: 'dead', nv: '--80', c: 'df5e6', v: '808-0', vrb: '6b8e23', rng: 'a5-', rngr: '45-', rch: 'da70d6', pgnr: 'eee8aa', pgrn: '98fb98', ptrq: 'afeeee', pvtr: 'db7093', ppwh: 'efd5', pchp: 'dab9', pr: 'cd853f', pnk: 'c0cb', pm: 'dda0dd', pwrb: 'b0e0e6', prp: '8-080', cc: '663399', r: '--', sbr: 'bc8f8f', rb: '4169e1', sbrw: '8b4513', smn: 'a8072', nbr: '4a460', sgrn: '2e8b57', ssh: '5ee', snn: 'a0522d', svr: 'c0c0c0', skb: '87ceeb', sTb: '6a5acd', sTgr: '708090', snw: 'afa', n: '-ff7f', stb: '4682b4', tn: 'd2b48c', t: '-8080', thst: 'd8bfd8', tmT: '6347', trqs: '40e0d0', vt: 'ee82ee', whT: '5deb3', wht: '', hts: '5f5f5', w: '-', wgrn: '9acd32' };

function printNum(num) {
    var decs = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;

    var str = decs > 0 ? num.toFixed(decs).replace(/0+$/, '').replace(/\.$/, '') : num.toString();
    return str || '0';
}

var Color = function () {
    function Color(r, g, b, a) {
        classCallCheck(this, Color);


        var that = this;
        function parseString(input) {

            if (input.startsWith('hsl')) {
                var _input$match$map = input.match(/([\-\d\.e]+)/g).map(Number),
                    _input$match$map2 = slicedToArray(_input$match$map, 4),
                    h = _input$match$map2[0],
                    s = _input$match$map2[1],
                    l = _input$match$map2[2],
                    _a = _input$match$map2[3];

                if (_a === undefined) {
                    _a = 1;
                }

                h /= 360;
                s /= 100;
                l /= 100;
                that.hsla = [h, s, l, _a];
            } else if (input.startsWith('rgb')) {
                var _input$match$map3 = input.match(/([\-\d\.e]+)/g).map(Number),
                    _input$match$map4 = slicedToArray(_input$match$map3, 4),
                    _r = _input$match$map4[0],
                    _g = _input$match$map4[1],
                    _b = _input$match$map4[2],
                    _a2 = _input$match$map4[3];

                if (_a2 === undefined) {
                    _a2 = 1;
                }

                that.rgba = [_r, _g, _b, _a2];
            } else {
                if (input.startsWith('#')) {
                    that.rgba = Color.hexToRgb(input);
                } else {
                    that.rgba = Color.nameToRgb(input) || Color.hexToRgb(input);
                }
            }
        }

        if (r === undefined) ; else if (Array.isArray(r)) {
            this.rgba = r;
        } else if (b === undefined) {
            var color = r && '' + r;
            if (color) {
                parseString(color.toLowerCase());
            }
        } else {
            this.rgba = [r, g, b, a === undefined ? 1 : a];
        }
    }

    createClass(Color, [{
        key: 'printRGB',
        value: function printRGB(alpha) {
            var rgb = alpha ? this.rgba : this.rgba.slice(0, 3),
                vals = rgb.map(function (x, i) {
                return printNum(x, i === 3 ? 3 : 0);
            });

            return alpha ? 'rgba(' + vals + ')' : 'rgb(' + vals + ')';
        }
    }, {
        key: 'printHSL',
        value: function printHSL(alpha) {
            var mults = [360, 100, 100, 1],
                suff = ['', '%', '%', ''];

            var hsl = alpha ? this.hsla : this.hsla.slice(0, 3),
                vals = hsl.map(function (x, i) {
                return printNum(x * mults[i], i === 3 ? 3 : 1) + suff[i];
            });

            return alpha ? 'hsla(' + vals + ')' : 'hsl(' + vals + ')';
        }
    }, {
        key: 'printHex',
        value: function printHex(alpha) {
            var hex = this.hex;
            return alpha ? hex : hex.substring(0, 7);
        }
    }, {
        key: 'rgba',
        get: function get() {
            if (this._rgba) {
                return this._rgba;
            }
            if (!this._hsla) {
                throw new Error('No color is set');
            }

            return this._rgba = Color.hslToRgb(this._hsla);
        },
        set: function set(rgb) {
            if (rgb.length === 3) {
                rgb[3] = 1;
            }

            this._rgba = rgb;
            this._hsla = null;
        }
    }, {
        key: 'rgbString',
        get: function get() {
            return this.printRGB();
        }
    }, {
        key: 'rgbaString',
        get: function get() {
            return this.printRGB(true);
        }
    }, {
        key: 'hsla',
        get: function get() {
            if (this._hsla) {
                return this._hsla;
            }
            if (!this._rgba) {
                throw new Error('No color is set');
            }

            return this._hsla = Color.rgbToHsl(this._rgba);
        },
        set: function set(hsl) {
            if (hsl.length === 3) {
                hsl[3] = 1;
            }

            this._hsla = hsl;
            this._rgba = null;
        }
    }, {
        key: 'hslString',
        get: function get() {
            return this.printHSL();
        }
    }, {
        key: 'hslaString',
        get: function get() {
            return this.printHSL(true);
        }
    }, {
        key: 'hex',
        get: function get() {
            var rgb = this.rgba,
                hex = rgb.map(function (x, i) {
                return i < 3 ? x.toString(16) : Math.round(x * 255).toString(16);
            });

            return '#' + hex.map(function (x) {
                return x.padStart(2, '0');
            }).join('');
        },
        set: function set(hex) {
            this.rgba = Color.hexToRgb(hex);
        }
    }], [{
        key: 'hexToRgb',
        value: function hexToRgb(input) {

            var hex = (input.startsWith('#') ? input.slice(1) : input).replace(/^(\w{3})$/, '$1F').replace(/^(\w)(\w)(\w)(\w)$/, '$1$1$2$2$3$3$4$4').replace(/^(\w{6})$/, '$1FF');

            if (!hex.match(/^([0-9a-fA-F]{8})$/)) {
                throw new Error('Unknown hex color; ' + input);
            }

            var rgba = hex.match(/^(\w\w)(\w\w)(\w\w)(\w\w)$/).slice(1).map(function (x) {
                return parseInt(x, 16);
            });

            rgba[3] = rgba[3] / 255;
            return rgba;
        }
    }, {
        key: 'nameToRgb',
        value: function nameToRgb(input) {

            var hash = input.toLowerCase().replace('at', 'T').replace(/[aeiouyldf]/g, '').replace('ght', 'L').replace('rk', 'D').slice(-5, 4),
                hex = colorNames[hash];
            return hex === undefined ? hex : Color.hexToRgb(hex.replace(/\-/g, '00').padStart(6, 'f'));
        }
    }, {
        key: 'rgbToHsl',
        value: function rgbToHsl(_ref) {
            var _ref2 = slicedToArray(_ref, 4),
                r = _ref2[0],
                g = _ref2[1],
                b = _ref2[2],
                a = _ref2[3];

            r /= 255;
            g /= 255;
            b /= 255;

            var max = Math.max(r, g, b),
                min = Math.min(r, g, b);
            var h = void 0,
                s = void 0,
                l = (max + min) / 2;

            if (max === min) {
                h = s = 0;
            } else {
                var d = max - min;
                s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
                switch (max) {
                    case r:
                        h = (g - b) / d + (g < b ? 6 : 0);break;
                    case g:
                        h = (b - r) / d + 2;break;
                    case b:
                        h = (r - g) / d + 4;break;
                }

                h /= 6;
            }

            return [h, s, l, a];
        }
    }, {
        key: 'hslToRgb',
        value: function hslToRgb(_ref3) {
            var _ref4 = slicedToArray(_ref3, 4),
                h = _ref4[0],
                s = _ref4[1],
                l = _ref4[2],
                a = _ref4[3];

            var r = void 0,
                g = void 0,
                b = void 0;

            if (s === 0) {
                r = g = b = l;
            } else {
                var hue2rgb = function hue2rgb(p, q, t) {
                    if (t < 0) t += 1;
                    if (t > 1) t -= 1;
                    if (t < 1 / 6) return p + (q - p) * 6 * t;
                    if (t < 1 / 2) return q;
                    if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
                    return p;
                };

                var q = l < 0.5 ? l * (1 + s) : l + s - l * s,
                    p = 2 * l - q;

                r = hue2rgb(p, q, h + 1 / 3);
                g = hue2rgb(p, q, h);
                b = hue2rgb(p, q, h - 1 / 3);
            }

            var rgba = [r * 255, g * 255, b * 255].map(Math.round);
            rgba[3] = a;

            return rgba;
        }
    }]);
    return Color;
}();

var EventBucket = function () {
    function EventBucket() {
        classCallCheck(this, EventBucket);

        this._events = [];
    }

    createClass(EventBucket, [{
        key: 'add',
        value: function add(target, type, handler) {
            target.addEventListener(type, handler, false);
            this._events.push({
                target: target,
                type: type,
                handler: handler
            });
        }
    }, {
        key: 'remove',
        value: function remove(target, type, handler) {
            this._events = this._events.filter(function (e) {
                var isMatch = true;
                if (target && target !== e.target) {
                    isMatch = false;
                }
                if (type && type !== e.type) {
                    isMatch = false;
                }
                if (handler && handler !== e.handler) {
                    isMatch = false;
                }

                if (isMatch) {
                    EventBucket._doRemove(e.target, e.type, e.handler);
                }
                return !isMatch;
            });
        }
    }, {
        key: 'destroy',
        value: function destroy() {
            this._events.forEach(function (e) {
                return EventBucket._doRemove(e.target, e.type, e.handler);
            });
            this._events = [];
        }
    }], [{
        key: '_doRemove',
        value: function _doRemove(target, type, handler) {
            target.removeEventListener(type, handler, false);
        }
    }]);
    return EventBucket;
}();

function parseHTML(htmlString) {

    var div = document.createElement('div');
    div.innerHTML = htmlString;
    return div.firstElementChild;
}

function dragTrack(eventBucket, area, callback) {
    var dragging = false;

    function clamp(val, min, max) {
        return Math.max(min, Math.min(val, max));
    }

    function onMove(e, info, starting) {
        if (starting) {
            dragging = true;
        }
        if (!dragging) {
            return;
        }

        e.preventDefault();

        var bounds = area.getBoundingClientRect(),
            w = bounds.width,
            h = bounds.height,
            x = info.clientX,
            y = info.clientY;

        var relX = clamp(x - bounds.left, 0, w),
            relY = clamp(y - bounds.top, 0, h);

        callback(relX / w, relY / h);
    }

    function onMouse(e, starting) {
        var button = e.buttons === undefined ? e.which : e.buttons;
        if (button === 1) {
            onMove(e, e, starting);
        } else {
            dragging = false;
        }
    }

    function onTouch(e, starting) {
        if (e.touches.length === 1) {
            onMove(e, e.touches[0], starting);
        } else {
            dragging = false;
        }
    }

    eventBucket.add(area, 'mousedown', function (e) {
        onMouse(e, true);
    });
    eventBucket.add(area, 'touchstart', function (e) {
        onTouch(e, true);
    });
    eventBucket.add(window, 'mousemove', onMouse);
    eventBucket.add(area, 'touchmove', onTouch);
    eventBucket.add(window, 'mouseup', function (e) {
        dragging = false;
    });
    eventBucket.add(area, 'touchend', function (e) {
        dragging = false;
    });
    eventBucket.add(area, 'touchcancel', function (e) {
        dragging = false;
    });
}

var BG_TRANSP = 'linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%) 0 0 / 2em 2em,\n                   linear-gradient(45deg, lightgrey 25%,       white 25%,       white 75%, lightgrey 75%) 1em 1em / 2em 2em';
var HUES = 360;

var EVENT_KEY = 'keydown',
    EVENT_CLICK_OUTSIDE = 'mousedown',
    EVENT_TAB_MOVE = 'focusin';

function $$2(selector, context) {
    return (context || document).querySelector(selector);
}

function stopEvent(e) {

    e.preventDefault();
    e.stopPropagation();
}
function onKey(bucket, target, keys, handler, stop) {
    bucket.add(target, EVENT_KEY, function (e) {
        if (keys.indexOf(e.key) >= 0) {
            if (stop) {
                stopEvent(e);
            }
            handler(e);
        }
    });
}

var Picker = function () {
    function Picker(options) {
        classCallCheck(this, Picker);


        this.settings = {

            popup: 'right',
            layout: 'default',
            alpha: true,
            editor: true,
            editorFormat: 'hex',
            cancelButton: false,
            defaultColor: '#0cf'
        };

        this._events = new EventBucket();

        this.onChange = null;

        this.onDone = null;

        this.onOpen = null;

        this.onClose = null;

        this.setOptions(options);
    }

    createClass(Picker, [{
        key: 'setOptions',
        value: function setOptions(options) {
            var _this = this;

            if (!options) {
                return;
            }
            var settings = this.settings;

            function transfer(source, target, skipKeys) {
                for (var key in source) {
                    if (skipKeys && skipKeys.indexOf(key) >= 0) {
                        continue;
                    }

                    target[key] = source[key];
                }
            }

            if (options instanceof HTMLElement) {
                settings.parent = options;
            } else {

                if (settings.parent && options.parent && settings.parent !== options.parent) {
                    this._events.remove(settings.parent);
                    this._popupInited = false;
                }

                transfer(options, settings);

                if (options.onChange) {
                    this.onChange = options.onChange;
                }
                if (options.onDone) {
                    this.onDone = options.onDone;
                }
                if (options.onOpen) {
                    this.onOpen = options.onOpen;
                }
                if (options.onClose) {
                    this.onClose = options.onClose;
                }

                var col = options.color || options.colour;
                if (col) {
                    this._setColor(col);
                }
            }

            var parent = settings.parent;
            if (parent && settings.popup && !this._popupInited) {

                var openProxy = function openProxy(e) {
                    return _this.openHandler(e);
                };

                this._events.add(parent, 'click', openProxy);

                onKey(this._events, parent, [' ', 'Spacebar', 'Enter'], openProxy);

                this._popupInited = true;
            } else if (options.parent && !settings.popup) {
                this.show();
            }
        }
    }, {
        key: 'openHandler',
        value: function openHandler(e) {
            if (this.show()) {

                e && e.preventDefault();

                this.settings.parent.style.pointerEvents = 'none';

                var toFocus = e && e.type === EVENT_KEY ? this._domEdit : this.domElement;
                setTimeout(function () {
                    return toFocus.focus();
                }, 100);

                if (this.onOpen) {
                    this.onOpen(this.colour);
                }
            }
        }
    }, {
        key: 'closeHandler',
        value: function closeHandler(e) {
            var event = e && e.type;
            var doHide = false;

            if (!e) {
                doHide = true;
            } else if (event === EVENT_CLICK_OUTSIDE || event === EVENT_TAB_MOVE) {

                var knownTime = (this.__containedEvent || 0) + 100;
                if (e.timeStamp > knownTime) {
                    doHide = true;
                }
            } else {

                stopEvent(e);

                doHide = true;
            }

            if (doHide && this.hide()) {
                this.settings.parent.style.pointerEvents = '';

                if (event !== EVENT_CLICK_OUTSIDE) {
                    this.settings.parent.focus();
                }

                if (this.onClose) {
                    this.onClose(this.colour);
                }
            }
        }
    }, {
        key: 'movePopup',
        value: function movePopup(options, open) {

            this.closeHandler();

            this.setOptions(options);
            if (open) {
                this.openHandler();
            }
        }
    }, {
        key: 'setColor',
        value: function setColor(color, silent) {
            this._setColor(color, { silent: silent });
        }
    }, {
        key: '_setColor',
        value: function _setColor(color, flags) {
            if (typeof color === 'string') {
                color = color.trim();
            }
            if (!color) {
                return;
            }

            flags = flags || {};
            var c = void 0;
            try {

                c = new Color(color);
            } catch (ex) {
                if (flags.failSilently) {
                    return;
                }
                throw ex;
            }

            if (!this.settings.alpha) {
                var hsla = c.hsla;
                hsla[3] = 1;
                c.hsla = hsla;
            }
            this.colour = this.color = c;
            this._setHSLA(null, null, null, null, flags);
        }
    }, {
        key: 'setColour',
        value: function setColour(colour, silent) {
            this.setColor(colour, silent);
        }
    }, {
        key: 'show',
        value: function show() {
            var parent = this.settings.parent;
            if (!parent) {
                return false;
            }

            if (this.domElement) {
                var toggled = this._toggleDOM(true);

                this._setPosition();

                return toggled;
            }

            var html = this.settings.template || '<div class="picker_wrapper" tabindex="-1"><div class="picker_arrow"></div><div class="picker_hue picker_slider"><div class="picker_selector"></div></div><div class="picker_sl"><div class="picker_selector"></div></div><div class="picker_alpha picker_slider"><div class="picker_selector"></div></div><div class="picker_editor"><input aria-label="Type a color name or hex value"/></div><div class="picker_sample"></div><div class="picker_done"><button>Ok</button></div><div class="picker_cancel"><button>Cancel</button></div></div>';
            var wrapper = parseHTML(html);

            this.domElement = wrapper;
            this._domH = $$2('.picker_hue', wrapper);
            this._domSL = $$2('.picker_sl', wrapper);
            this._domA = $$2('.picker_alpha', wrapper);
            this._domEdit = $$2('.picker_editor input', wrapper);
            this._domSample = $$2('.picker_sample', wrapper);
            this._domOkay = $$2('.picker_done button', wrapper);
            this._domCancel = $$2('.picker_cancel button', wrapper);

            wrapper.classList.add('layout_' + this.settings.layout);
            if (!this.settings.alpha) {
                wrapper.classList.add('no_alpha');
            }
            if (!this.settings.editor) {
                wrapper.classList.add('no_editor');
            }
            if (!this.settings.cancelButton) {
                wrapper.classList.add('no_cancel');
            }
            this._ifPopup(function () {
                return wrapper.classList.add('popup');
            });

            this._setPosition();

            if (this.colour) {
                this._updateUI();
            } else {
                this._setColor(this.settings.defaultColor);
            }
            this._bindEvents();

            return true;
        }
    }, {
        key: 'hide',
        value: function hide() {
            return this._toggleDOM(false);
        }
    }, {
        key: 'destroy',
        value: function destroy() {
            this._events.destroy();
            if (this.domElement) {
                this.settings.parent.removeChild(this.domElement);
            }
        }
    }, {
        key: '_bindEvents',
        value: function _bindEvents() {
            var _this2 = this;

            var that = this,
                dom = this.domElement,
                events = this._events;

            function addEvent(target, type, handler) {
                events.add(target, type, handler);
            }

            addEvent(dom, 'click', function (e) {
                return e.preventDefault();
            });

            dragTrack(events, this._domH, function (x, y) {
                return that._setHSLA(x);
            });

            dragTrack(events, this._domSL, function (x, y) {
                return that._setHSLA(null, x, 1 - y);
            });

            if (this.settings.alpha) {
                dragTrack(events, this._domA, function (x, y) {
                    return that._setHSLA(null, null, null, 1 - y);
                });
            }

            var editInput = this._domEdit;
            {
                addEvent(editInput, 'input', function (e) {
                    that._setColor(this.value, { fromEditor: true, failSilently: true });
                });

                addEvent(editInput, 'focus', function (e) {
                    var input = this;

                    if (input.selectionStart === input.selectionEnd) {
                        input.select();
                    }
                });
            }

            this._ifPopup(function () {

                var popupCloseProxy = function popupCloseProxy(e) {
                    return _this2.closeHandler(e);
                };

                addEvent(window, EVENT_CLICK_OUTSIDE, popupCloseProxy);
                addEvent(window, EVENT_TAB_MOVE, popupCloseProxy);
                onKey(events, dom, ['Esc', 'Escape'], popupCloseProxy);

                var timeKeeper = function timeKeeper(e) {
                    _this2.__containedEvent = e.timeStamp;
                };
                addEvent(dom, EVENT_CLICK_OUTSIDE, timeKeeper);

                addEvent(dom, EVENT_TAB_MOVE, timeKeeper);

                addEvent(_this2._domCancel, 'click', popupCloseProxy);
            });

            var onDoneProxy = function onDoneProxy(e) {
                _this2._ifPopup(function () {
                    return _this2.closeHandler(e);
                });
                if (_this2.onDone) {
                    _this2.onDone(_this2.colour);
                }
            };
            addEvent(this._domOkay, 'click', onDoneProxy);
            onKey(events, dom, ['Enter'], onDoneProxy);
        }
    }, {
        key: '_setPosition',
        value: function _setPosition() {
            var parent = this.settings.parent,
                elm = this.domElement;

            if (parent !== elm.parentNode) {
                parent.appendChild(elm);
            }

            this._ifPopup(function (popup) {

                if (getComputedStyle(parent).position === 'static') {
                    parent.style.position = 'relative';
                }

                var cssClass = popup === true ? 'popup_right' : 'popup_' + popup;

                ['popup_top', 'popup_bottom', 'popup_left', 'popup_right'].forEach(function (c) {

                    if (c === cssClass) {
                        elm.classList.add(c);
                    } else {
                        elm.classList.remove(c);
                    }
                });

                elm.classList.add(cssClass);
            });
        }
    }, {
        key: '_setHSLA',
        value: function _setHSLA(h, s, l, a, flags) {
            flags = flags || {};

            var col = this.colour,
                hsla = col.hsla;

            [h, s, l, a].forEach(function (x, i) {
                if (x || x === 0) {
                    hsla[i] = x;
                }
            });
            col.hsla = hsla;

            this._updateUI(flags);

            if (this.onChange && !flags.silent) {
                this.onChange(col);
            }
        }
    }, {
        key: '_updateUI',
        value: function _updateUI(flags) {
            if (!this.domElement) {
                return;
            }
            flags = flags || {};

            var col = this.colour,
                hsl = col.hsla,
                cssHue = 'hsl(' + hsl[0] * HUES + ', 100%, 50%)',
                cssHSL = col.hslString,
                cssHSLA = col.hslaString;

            var uiH = this._domH,
                uiSL = this._domSL,
                uiA = this._domA,
                thumbH = $$2('.picker_selector', uiH),
                thumbSL = $$2('.picker_selector', uiSL),
                thumbA = $$2('.picker_selector', uiA);

            function posX(parent, child, relX) {
                child.style.left = relX * 100 + '%';
            }
            function posY(parent, child, relY) {
                child.style.top = relY * 100 + '%';
            }

            posX(uiH, thumbH, hsl[0]);

            this._domSL.style.backgroundColor = this._domH.style.color = cssHue;

            posX(uiSL, thumbSL, hsl[1]);
            posY(uiSL, thumbSL, 1 - hsl[2]);

            uiSL.style.color = cssHSL;

            posY(uiA, thumbA, 1 - hsl[3]);

            var opaque = cssHSL,
                transp = opaque.replace('hsl', 'hsla').replace(')', ', 0)'),
                bg = 'linear-gradient(' + [opaque, transp] + ')';

            this._domA.style.background = bg + ', ' + BG_TRANSP;

            if (!flags.fromEditor) {
                var format = this.settings.editorFormat,
                    alpha = this.settings.alpha;

                var value = void 0;
                switch (format) {
                    case 'rgb':
                        value = col.printRGB(alpha);break;
                    case 'hsl':
                        value = col.printHSL(alpha);break;
                    default:
                        value = col.printHex(alpha);
                }
                this._domEdit.value = value;
            }

            this._domSample.style.color = cssHSLA;
        }
    }, {
        key: '_ifPopup',
        value: function _ifPopup(actionIf, actionElse) {
            if (this.settings.parent && this.settings.popup) {
                actionIf && actionIf(this.settings.popup);
            } else {
                actionElse && actionElse();
            }
        }
    }, {
        key: '_toggleDOM',
        value: function _toggleDOM(toVisible) {
            var dom = this.domElement;
            if (!dom) {
                return false;
            }

            var displayStyle = toVisible ? '' : 'none',
                toggle = dom.style.display !== displayStyle;

            if (toggle) {
                dom.style.display = displayStyle;
            }
            return toggle;
        }
    }]);
    return Picker;
}();

{
    var style = document.createElement('style');
    style.textContent = '.picker_wrapper.no_alpha .picker_alpha{display:none}.picker_wrapper.no_editor .picker_editor{position:absolute;z-index:-1;opacity:0}.picker_wrapper.no_cancel .picker_cancel{display:none}.layout_default.picker_wrapper{display:flex;flex-flow:row wrap;justify-content:space-between;align-items:stretch;font-size:10px;width:25em;padding:.5em}.layout_default.picker_wrapper input,.layout_default.picker_wrapper button{font-size:1rem}.layout_default.picker_wrapper>*{margin:.5em}.layout_default.picker_wrapper::before{content:"";display:block;width:100%;height:0;order:1}.layout_default .picker_slider,.layout_default .picker_selector{padding:1em}.layout_default .picker_hue{width:100%}.layout_default .picker_sl{flex:1 1 auto}.layout_default .picker_sl::before{content:"";display:block;padding-bottom:100%}.layout_default .picker_editor{order:1;width:6.5rem}.layout_default .picker_editor input{width:100%;height:100%}.layout_default .picker_sample{order:1;flex:1 1 auto}.layout_default .picker_done,.layout_default .picker_cancel{order:1}.picker_wrapper{box-sizing:border-box;background:#f2f2f2;box-shadow:0 0 0 1px silver;cursor:default;font-family:sans-serif;color:#444;pointer-events:auto}.picker_wrapper:focus{outline:none}.picker_wrapper button,.picker_wrapper input{box-sizing:border-box;border:none;box-shadow:0 0 0 1px silver;outline:none}.picker_wrapper button:focus,.picker_wrapper button:active,.picker_wrapper input:focus,.picker_wrapper input:active{box-shadow:0 0 2px 1px #1e90ff}.picker_wrapper button{padding:.4em .6em;cursor:pointer;background-color:#f5f5f5;background-image:linear-gradient(0deg, gainsboro, transparent)}.picker_wrapper button:active{background-image:linear-gradient(0deg, transparent, gainsboro)}.picker_wrapper button:hover{background-color:#fff}.picker_selector{position:absolute;z-index:1;display:block;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);border:2px solid #fff;border-radius:100%;box-shadow:0 0 3px 1px #67b9ff;background:currentColor;cursor:pointer}.picker_slider .picker_selector{border-radius:2px}.picker_hue{position:relative;background-image:linear-gradient(90deg, red, yellow, lime, cyan, blue, magenta, red);box-shadow:0 0 0 1px silver}.picker_sl{position:relative;box-shadow:0 0 0 1px silver;background-image:linear-gradient(180deg, white, rgba(255, 255, 255, 0) 50%),linear-gradient(0deg, black, rgba(0, 0, 0, 0) 50%),linear-gradient(90deg, #808080, rgba(128, 128, 128, 0))}.picker_alpha,.picker_sample{position:relative;background:linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%) 0 0/2em 2em,linear-gradient(45deg, lightgrey 25%, white 25%, white 75%, lightgrey 75%) 1em 1em/2em 2em;box-shadow:0 0 0 1px silver}.picker_alpha .picker_selector,.picker_sample .picker_selector{background:none}.picker_editor input{font-family:monospace;padding:.2em .4em}.picker_sample::before{content:"";position:absolute;display:block;width:100%;height:100%;background:currentColor}.picker_arrow{position:absolute;z-index:-1}.picker_wrapper.popup{position:absolute;z-index:2;margin:1.5em}.picker_wrapper.popup,.picker_wrapper.popup .picker_arrow::before,.picker_wrapper.popup .picker_arrow::after{background:#f2f2f2;box-shadow:0 0 10px 1px rgba(0,0,0,.4)}.picker_wrapper.popup .picker_arrow{width:3em;height:3em;margin:0}.picker_wrapper.popup .picker_arrow::before,.picker_wrapper.popup .picker_arrow::after{content:"";display:block;position:absolute;top:0;left:0;z-index:-99}.picker_wrapper.popup .picker_arrow::before{width:100%;height:100%;-webkit-transform:skew(45deg);transform:skew(45deg);-webkit-transform-origin:0 100%;transform-origin:0 100%}.picker_wrapper.popup .picker_arrow::after{width:150%;height:150%;box-shadow:none}.popup.popup_top{bottom:100%;left:0}.popup.popup_top .picker_arrow{bottom:0;left:0;-webkit-transform:rotate(-90deg);transform:rotate(-90deg)}.popup.popup_bottom{top:100%;left:0}.popup.popup_bottom .picker_arrow{top:0;left:0;-webkit-transform:rotate(90deg) scale(1, -1);transform:rotate(90deg) scale(1, -1)}.popup.popup_left{top:0;right:100%}.popup.popup_left .picker_arrow{top:0;right:0;-webkit-transform:scale(-1, 1);transform:scale(-1, 1)}.popup.popup_right{top:0;left:100%}.popup.popup_right .picker_arrow{top:0;left:0}';
    document.documentElement.firstElementChild.appendChild(style);

    Picker.StyleElement = style;
}

function getChrColor(chr) {
    if (chrColorMap[chr]) {
        return chrColorMap[chr];
    } else if (chrColorMap["chr" + chr]) {
        const color = chrColorMap["chr" + chr];
        chrColorMap[chr] = color;
        return color;
    } else {
        const color = randomRGB();
        chrColorMap[chr] = color;
        return color;
    }
}

function randomRGB (min, max) {

    var r = Math.round(Math.random() * 255).toString(10);
    var g = Math.round(Math.random() * 255).toString(10);
    var b = Math.round(Math.random() * 255).toString(10);
    return "rgb(" + r + "," + g + "," + b + ")";
}

const chrColorMap = {
    "chrX": "rgb(204, 153, 0)",
    "chrY": "rgb(153, 204, 0",
    "chrUn": "rgb(50, 50, 50)",
    "chr1": "rgb(80, 80, 255)",
    "chrI": "rgb(139, 155, 187)",
    "chr2": "rgb(206, 61, 50)",
    "chrII": "rgb(206, 61, 50)",
    "chr2a": "rgb(216, 71, 60)",
    "chr2b": "rgb(226, 81, 70)",
    "chr3": "rgb(116, 155, 88)",
    "chrIII": "rgb(116, 155, 88)",
    "chr4": "rgb(240, 230, 133)",
    "chrIV": "rgb(240, 230, 133)",
    "chr5": "rgb(70, 105, 131)",
    "chr6": "rgb(186, 99, 56)",
    "chr7": "rgb(93, 177, 221)",
    "chr8": "rgb(128, 34, 104)",
    "chr9": "rgb(107, 215, 107)",
    "chr10": "rgb(213, 149, 167)",
    "chr11": "rgb(146, 72, 34)",
    "chr12": "rgb(131, 123, 141)",
    "chr13": "rgb(199, 81, 39)",
    "chr14": "rgb(213, 143, 92)",
    "chr15": "rgb(122, 101, 165)",
    "chr16": "rgb(228, 175, 105)",
    "chr17": "rgb(59, 27, 83)",
    "chr18": "rgb(205, 222, 183)",
    "chr19": "rgb(97, 42, 121)",
    "chr20": "rgb(174, 31, 99)",
    "chr21": "rgb(231, 199, 111)",
    "chr22": "rgb(90, 101, 94)",
    "chr23": "rgb(204, 153, 0)",
    "chr24": "rgb(153, 204, 0)",
    "chr25": "rgb(51, 204, 0)",
    "chr26": "rgb(0, 204, 51)",
    "chr27": "rgb(0, 204, 153)",
    "chr28": "rgb(0, 153, 204)",
    "chr29": "rgb(10, 71, 255)",
    "chr30": "rgb(71, 117, 255)",
    "chr31": "rgb(255, 194, 10)",
    "chr32": "rgb(255, 209, 71)",
    "chr33": "rgb(153, 0, 51)",
    "chr34": "rgb(153, 26, 0)",
    "chr35": "rgb(153, 102, 0)",
    "chr36": "rgb(128, 153, 0)",
    "chr37": "rgb(51, 153, 0)",
    "chr38": "rgb(0, 153, 26)",
    "chr39": "rgb(0, 153, 102)",
    "chr40": "rgb(0, 128, 153)",
    "chr41": "rgb(0, 51, 153)",
    "chr42": "rgb(26, 0, 153)",
    "chr43": "rgb(102, 0, 153)",
    "chr44": "rgb(153, 0, 128)",
    "chr45": "rgb(214, 0, 71)",
    "chr46": "rgb(255, 20, 99)",
    "chr47": "rgb(0, 214, 143)",
    "chr48": "rgb(20, 255, 177)",
};

class ChordSetManager {

    constructor(config) {
        this.tracks = [];
        this.chordSets = [];
    }

    addChordSet(chordSet) {

        // If a chord set with this name exists replace it (same track, same region)
        this.chordSets = this.chordSets.filter(g => g.name !== chordSet.name);
        this.chordSets.push(chordSet);

        let track = this.tracks.find(t => chordSet.trackName === t.name);
        if (track) {
            track.chordSets = track.chordSets.filter(cs => cs.name !== chordSet.name);
            track.chordSets.push(chordSet);
        }
        if (!track) {
            track = new IGVTrack(chordSet);
            this.tracks.push(track);
        }
    }

    clearChords() {
        this.tracks = [];
        this.chordSets = [];
    }

    getTrack(name) {
        return this.tracks.find(t => name === t.name)
    }

    getChordSet(name) {
        return this.chordSets.find(cs => name === cs.name)
    }

}

class IGVTrack {
    constructor(chordSet) {
        this.name = chordSet.trackName;
        this.color = chordSet.trackColor;
        this.visible = true;
        this.chordSets = [chordSet];
        this.id = guid$1();
    }

    get chords() {
        if (this.chordSets.length === 1) {
            return this.chordSets[0].chords
        }
        const chords = [];
        for (let cs of this.chordSets) {
            for (let c of cs.chords) {
                chords.push(c);
            }
        }
        return chords
    }
}


function guid$1() {
    return ("0000" + (Math.random() * Math.pow(36, 4) << 0).toString(36)).slice(-4)
}

const EXP5 = Math.exp(5);

class CircularView {

    static isInstalled() {
        return window["JBrowseReactCircularGenomeView"] !== undefined && window["React"] !== undefined && window["ReactDOM"] !== undefined
    }

    /**
     * Create a new CircularView
     *
     * @param parent
     * @param config - configuration options
     *   {
     *       assembly: {name: string, id: string, chromosomes: [{name: string, bpLength: integer, color: string}]
     *       onChordClick: function called upon chord click with chord feature as argument
     *   }
     */
    constructor(parent, config) {

        config = config || {};
        this.config = config;

        if (CircularView.isInstalled()) {

            this.parent = parent;
            this.groupByTrack = config.groupByTrack === true;
            this.chordManager = new ChordSetManager(config);

            // wrapper for toolbar and circular-view container
            const wrapper = document.createElement('div');
            wrapper.className = 'igv-circview-container';
            parent.appendChild(wrapper);

            // toolbar
            this.createControls(wrapper);
            this.resetControlPanel();

            // circular view container
            const element = document.createElement('div');
            element.className = 'igv-circview-circular-genome-view';
            wrapper.appendChild(element);
            this.container = element;

            if (config.assembly) {
                this.setAssembly(config.assembly);
            }

            this.width = config.width || 500;
            this.height = config.height || 500;
            this.setSize(this.width, this.height);

        } else {
            console.error("JBrowse circular view is not installed");
        }
    }

    createControls(parent) {

        // toolbar
        const toolbarDiv = document.createElement('div');
        toolbarDiv.className = 'igv-circview-toolbar';
        parent.appendChild(toolbarDiv);
        this.toolbar = toolbarDiv;

        // control panel
        const controlPanelDiv = document.createElement('div');
        controlPanelDiv.className = 'igv-circview-track-panel';
        parent.appendChild(controlPanelDiv);
        this.controlPanel = controlPanelDiv;
        this.controlPanel.style.display = 'none';


        // toolbar button container - Track Options - Clear All
        const buttonContainer = document.createElement('div');
        buttonContainer.className = 'igv-circview-toolbar-button-container';
        this.toolbar.appendChild(buttonContainer);

        // Show Controls
        this.showControlsButton = document.createElement('div');
        this.showControlsButton.className = 'igv-circview-button';
        buttonContainer.appendChild(this.showControlsButton);
        this.showControlsButton.innerText = 'none' === this.controlPanel.style.display ? 'Show Controls' : 'Hide Controls';
        this.showControlsButton.addEventListener('click', (event) => {
            const panelRows = this.controlPanel.querySelectorAll('div');
            if (panelRows.length > 0) {
                if ('none' === this.controlPanel.style.display) {
                    this.controlPanel.style.display = 'flex';
                    event.target.innerText = 'Hide Controls';
                } else {
                    this.controlPanel.style.display = 'none';
                    event.target.innerText = 'Show Controls';
                }
            }
        });

        // Clear All
        let button = document.createElement('div');
        button.className = 'igv-circview-button';
        buttonContainer.appendChild(button);
        button.innerText = 'Clear All';
        button.addEventListener('click', () => {
            this.clearChords();
        });

        // Close
        if (false !== this.config.showCloseButton) {
            button = document.createElement('div');
            button.className = 'igv-circview-button';
            buttonContainer.appendChild(button);
            button.innerText = 'Close';
            button.addEventListener('click', () => {
                this.visible = false;
            });
        }
    }

    resetControlPanel() {
        this.controlPanel.innerHTML = '';
        this.controlPanel.appendChild(this.createGroupByCB());
        const chordSets = this.groupByTrack ? this.chordManager.tracks : this.chordManager.chordSets;
        for(let cs of chordSets) {
            this.addToControlPanel(cs);
        }
    }

    createGroupByCB() {
        const groupByCB = document.createElement('input');
        groupByCB.type = 'checkbox';
        groupByCB.id = 'groupByCB';
        groupByCB.style.width = '1.4em';
        groupByCB.style.height = '1.4em';
        groupByCB.checked = this.groupByTrack;

        groupByCB.onclick = (evt) => {
            this.groupByTrack = evt.target.checked;
            this.resetControlPanel();
            this.render();
        };

        const groupByLabel = document.createElement('label');
        groupByLabel.for = 'groupByCB';
        groupByLabel.innerText = 'Group by track';
        groupByLabel.style.color = 'black';
        groupByLabel.style.paddingLeft = '10px';
        const trackPanelRow = document.createElement('div');
        trackPanelRow.style.width = '100%';
        trackPanelRow.style.paddingTop = '5px';
        trackPanelRow.style.paddingBottom = '5px';
        trackPanelRow.style.background = 'rgb(216, 230, 234)';
        trackPanelRow.appendChild(groupByCB);
        trackPanelRow.appendChild(groupByLabel);
        return trackPanelRow
    }

    addToControlPanel(chordSet) {

        // single track row - container for hide-button | color-picker-swatch | track-name
        const row = document.createElement('div');
        this.controlPanel.appendChild(row);


        // track hide|show
        const hideShowButton = document.createElement('div');
        hideShowButton.className = 'igv-circview-button';
        row.appendChild(hideShowButton);
        hideShowButton.innerText = true === chordSet.visible ? 'Hide' : 'Show';
        hideShowButton.addEventListener('click', event => {
            if (true === chordSet.visible) {
                this.hideChordSet(chordSet.name);
                event.target.innerText = "Show";
            } else {
                this.showChordSet(chordSet.name);
                event.target.innerText = "Hide";
            }
        });

        // The alpha range slider.  Create this here so we can reference it from the color picker
        const alphaSlider = document.createElement('input');
        const valueToAlpha = (value) => Math.exp(value / 200) / EXP5;
        const alphaToValue = (alpha) => 200 * Math.log(alpha * EXP5);

        // color
        const colorPickerButton = document.createElement('div');
        colorPickerButton.className = 'igv-circview-button';
        colorPickerButton.innerHTML = '&nbsp;&nbsp;&nbsp;&nbsp;';   // <- important for button to size properly
        row.appendChild(colorPickerButton);
        colorPickerButton.style.backgroundColor = setAlpha(chordSet.color, 1);
        const pickerConfig =
            {
                parent: colorPickerButton,
                popup: 'right',
                editorFormat: 'rgb',
                color: chordSet.color,
                onChange: ({rgbaString}) => {
                    colorPickerButton.style.backgroundColor = setAlpha(rgbaString, 1);
                    this.setColor(chordSet.name, rgbaString);
                    alphaSlider.value = alphaToValue(getAlpha(chordSet.color));
                }
            };
        const picker = new Picker(pickerConfig);

        // alpha transparency
        alphaSlider.setAttribute('title', 'Adjust transparency of arcs');
        alphaSlider.type = 'range';
        //alphaSlider.className = 'igv-circview-alpha-slider'
        alphaSlider.style.width = '100px';
        alphaSlider.style.marginRight = '10px';
        alphaSlider.setAttribute('class', 'range');
        alphaSlider.setAttribute('min', '0');
        alphaSlider.setAttribute('max', '1000');
        alphaSlider.value = alphaToValue(getAlpha(chordSet.color));
        alphaSlider.oninput = () => {
            const v = valueToAlpha(alphaSlider.value);
            this.setColor(chordSet.name, setAlpha(chordSet.color, v));
            picker.setColor(chordSet.color);
        };
        row.appendChild(alphaSlider);

        // track name
        const trackNameDive = document.createElement('div');
        trackNameDive.style.color = 'black';
        row.appendChild(trackNameDive);
        trackNameDive.innerText = trackNameDive.title = chordSet.name;

    }

    /**
     * Reset view with a new set of chromosomes.
     *
     * @param igvGenome {name: string, id: string, chromosomes: [{name: string, bpLength: integer, color: string}
     */
    setAssembly(igvGenome) {

        const id = this.genomeId || guid$3();

        if (this.genomeId === id) {
            return
        }
        this.chordManager.clearChords();
        this.genomeId = id;
        this.chrNames = new Set(igvGenome.chromosomes.map(chr => shortChrName$1(chr.name)));

        const regions = [];
        const colors = [];
        for (let chr of igvGenome.chromosomes) {
            const shortName = shortChrName$1(chr.name);
            colors.push(chr.color || getChrColor(shortName));
            regions.push(
                {
                    refName: shortName,
                    uniqueId: shortName,
                    start: 0,
                    end: chr.bpLength
                }
            );
        }

        this.assembly = {
            name: igvGenome.name,
            sequence: {
                trackId: id,
                type: 'ReferenceSequenceTrack',
                adapter: {
                    type: 'FromConfigSequenceAdapter',
                    features: regions,
                },
            },
            refNameColors: colors
        };

        this.render();

    }

    /**
     * Append or replace current set of chords to the global set or a specific track.
     *
     * @param newChords array of chord feature objects.  Example:
     * [
     *   {
     *     "uniqueId": "chr1:129763372-129763376_chr1:129806775-129806790",
     *     "color": "rgba(0, 0, 255, 0.1)",
     *     "refName": "1",
     *     "start": 129763372,
     *     "end": 129763376,
     *     "mate": {
     *       "refName": "2",
     *       "start": 129806775,
     *       "end": 129806790
     *     }
     *   }
     * ]
     * @param options {
     *     name: string,    // Track name
     *     color: string,   // Track color
     *     append: boolean  // Replace or append chords to current set.  Default is append (true)
     * }
     */

    addChords(newChords, options = {}) {

        const tmp = options.name || options.track || "*";
        const trackName = tmp.split(' ')[0].replaceAll("%20", " ");
        const chordSetName = tmp.replaceAll("%20", " ");

        const chordSet =  {
            name: chordSetName,
            trackName: trackName,
            chords: newChords,
            color: options.color || "black",
            trackColor: options.trackColor || options.color || "black",
            visible: true,
            id: options.id || guid$3()
        };

        this.chordManager.addChordSet(chordSet);

        this.resetControlPanel();

        this.render();
    }

    /**
     * Set the nominal size of the view in pixels.  Size is reduced some aribtrary amount to account for borders and margins
     */
    setSize(width, height) {

        height = height || width;

        this.width = width;
        this.height = height;
        if (this.viewState) {
            const view = this.viewState.session.view;
            view.setWidth(width);
            view.setHeight(height /* this is the height of the area inside the border in pixels */);
            view.setBpPerPx(view.minBpPerPx);
        }
    }

    getSize() {
        return Math.min(this.width, this.height)
    }

    clearChords() {
        //this.tracks = []
        this.chordManager.clearChords();
        this.resetControlPanel();
        this.render();
    }

    clearSelection() {
        this.viewState.pluginManager.rootModel.session.clearSelection();
    }

    /**
     * Deprecated, use "visible" property
     */
    show() {
        this.parent.style.display = 'block';
    }

    /**
     * Deprecated, use "visible" property
     */
    hide() {
        this.parent.style.display = 'none';
    }

    get visible() {
        return this.parent.style.display !== 'none'
    }

    set visible(isVisible) {
        this.parent.style.display = isVisible ? 'block' : 'none';
    }

    hideChordSet(trackName) {
        let cs = this.getChordSet(trackName);
        if (cs) {
            cs.visible = false;
            this.render();
        } else {
            console.warn(`No track with name: ${name}`);
        }
    }

    showChordSet(name) {
        let cs = this.getChordSet(name);
        if (cs) {
            cs.visible = true;
            this.render();
        } else {
            console.warn(`No track with name: ${name}`);
        }
    }

    // showTrack(trackID) {
    //     let idx = this.tracks.findIndex(t => trackID === t.id)
    //     if (idx >= 0) {
    //         const track = this.tracks[idx]
    //         track.visible = true
    //         this.tracks.splice(idx, 1)   // Change z-order
    //         this.tracks.push(track)
    //         this.render()
    //     } else {
    //         console.warn(`No track with name: ${name}`)
    //     }
    // }

    // TODO -- remove corresponding row from track panel
    deleteTrack(trackID) {
        let idx = this.tracks.findIndex(t => trackID === t.name);
        if (idx >= 0) {
            this.tracks.splice(idx, 1);
        }
        this.render();
    }

    getChordSet(name) {
        return this.groupByTrack ? this.chordManager.getTrack(name) : this.chordManager.getChordSet(name)
    }

    setColor(name, color) {
        const t = this.getChordSet(name);
        if (t) {
            t.color = color;
            const trackID = t.id;
            for (let jbrowseTrack of this.viewState.config.tracks) {
                if (trackID === jbrowseTrack.trackId) {
                    jbrowseTrack.displays[0].renderer.strokeColor.set(color);
                    break
                }
            }
        }
    }

    /**
     * The main render function.  Render here means build the React DOM.  Trying to change react state dynamically
     * has been buggy, so we completely rebuild the DOM ("render") on any state change.
     */
    render() {

        const {
            createViewState,
            JBrowseCircularGenomeView,
        } = JBrowseReactCircularGenomeView;

        // Remove all children from possible previous renders.  React might do this for us when we render, but just in case.
        ReactDOM.unmountComponentAtNode(this.container);

        const visibleChordSets =
            (this.groupByTrack ? this.chordManager.tracks : this.chordManager.chordSets).filter(t => t.visible);

        const jbrowseTracks = [];
        const colors = [];

        for (let chordSet of visibleChordSets) {

            jbrowseTracks.push({
                trackId: chordSet.id,
                name: chordSet.name,
                assemblyNames: ['forIGV'],
                type: 'VariantTrack',
                adapter: {
                    type: 'FromConfigAdapter',
                    features: chordSet.chords,
                }
            });
            colors.push(chordSet.color);
        }

        this.viewState = createViewState({
            assembly: this.assembly,
            tracks: jbrowseTracks,
        });

        // Set view colors
        for (let i = 0; i < visibleChordSets.length; i++) {
            this.viewState.config.tracks[i].displays[0].renderer.strokeColor.set(colors[i]);
            //this.viewState.config.tracks[i].displays[0].renderer.strokeColor.set("jexl:get(feature, 'color') || 'black'");
            //this.viewState.config.tracks[i].displays[0].renderer.strokeColorSelected.set("jexl:get(feature, 'highlightColor') || 'red'");
        }

        this.element = React.createElement(JBrowseCircularGenomeView, {viewState: this.viewState});
        this.setSize(this.width, this.height);

        ReactDOM.render(this.element, this.container);

        const onChordClick = this.config.onChordClick || defaultOnChordClick;
        for (let i = 0; i < visibleChordSets.length; i++) {
            this.viewState.session.view.showTrack(this.viewState.config.tracks[i].trackId);
            if (onChordClick) {
                this.viewState.pluginManager.jexl.addFunction('onChordClick', onChordClick);
                this.viewState.config.tracks[i].displays[0].onChordClick.set(
                    'jexl:onChordClick(feature, track, pluginManager)'
                );
            }
        }
    }
}

function setAlpha(rgba, alpha) {
    const [a, b, c, ignore] = rgba.split(','); // rgba(r g b alpha)
    return `${a},${b},${c},${alpha})`
}

function getAlpha(rgba) {
    if (rgba.startsWith("rgba(")) {
        return Number(rgba.split(',')[3].replace(')', ''))
    } else {
        return 1
    }
}

function shortChrName$1(chrName) {
    return chrName.startsWith("chr") ? chrName.substring(3) : chrName
}

function defaultOnChordClick(feature, chordTrack, pluginManager) {
}

function guid$3() {
    return ("0000" + (Math.random() * Math.pow(36, 4) << 0).toString(36)).slice(-4)
}

function embedCSS() {

    const css =  '.igv-circview-container {\n  width: fit-content;\n  height: fit-content;\n  box-sizing: content-box;\n  color: dimgray;\n  font-family: \"Open Sans\", sans-serif;\n  font-size: 12px;\n  background-color: white;\n  border-color: dimgray;\n  border-style: solid;\n  border-width: thin;\n  display: flex;\n  flex-direction: column;\n  flex-wrap: nowrap;\n  justify-content: flex-start;\n  align-items: flex-start;\n}\n\n.igv-circview-toolbar {\n  position: relative;\n  width: 100%;\n  height: 32px;\n  background-color: lightgrey;\n  border-bottom-style: solid;\n  border-bottom-color: dimgray;\n  border-bottom-width: thin;\n  display: flex;\n  flex-flow: row;\n  flex-wrap: nowrap;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.igv-circview-toolbar-button-container {\n  height: 100%;\n  width: fit-content;\n  display: flex;\n  flex-flow: row;\n  flex-wrap: nowrap;\n  justify-content: flex-start;\n  align-items: center;\n}\n.igv-circview-toolbar-button-container > div {\n  margin: 4px;\n}\n\n.igv-circview-track-panel {\n  z-index: 1024;\n  position: absolute;\n  top: 33px;\n  left: 0;\n  width: 100%;\n  height: fit-content;\n  border-bottom-style: solid;\n  border-bottom-color: dimgray;\n  border-bottom-width: thin;\n  background-color: white;\n  display: flex;\n  flex-flow: column;\n  flex-wrap: nowrap;\n  justify-content: flex-start;\n  align-items: flex-start;\n}\n.igv-circview-track-panel > div {\n  display: flex;\n  flex-flow: row;\n  flex-wrap: nowrap;\n  justify-content: flex-start;\n  align-items: center;\n}\n.igv-circview-track-panel > div > div {\n  margin: 4px;\n}\n\n.igv-circview-swatch-button {\n  cursor: pointer;\n  padding: 5px;\n  width: 8px;\n  height: 8px;\n  border: 1px solid #8d8b8b;\n  border-radius: 16px;\n}\n\n.igv-circview-button {\n  cursor: pointer;\n  padding: 5px;\n  color: #444;\n  vertical-align: middle;\n  text-align: center;\n  font-family: \"Open Sans\", sans-serif;\n  font-size: 12px;\n  border: 1px solid #8d8b8b;\n  border-radius: 4px;\n  background: #efefef;\n  box-shadow: 0 0 5px -1px rgba(0, 0, 0, 0.2);\n}\n\n.igv-circview-button:hover {\n  background: #efefef;\n  box-shadow: 0 0 5px -1px rgba(0, 0, 0, 0.6);\n}\n\n.igv-circview-button:active {\n  color: #007bff;\n  box-shadow: 0 0 5px -1px rgba(0, 0, 0, 0.6);\n}\n\n/*# sourceMappingURL=circular-view.css.map */\n';

    const style = document.createElement('style');
    style.setAttribute('type', 'text/css');
    style.innerHTML = css;
    document.head.insertBefore(style, document.head.childNodes[ document.head.childNodes.length - 1 ]);
}

if(typeof document !== 'undefined') {

    if (!stylesheetExists("circular-view.css")) {
        embedCSS();
    }

    function stylesheetExists(stylesheetName) {
        for (let ss of document.styleSheets) {
            ss = ss.href ? ss.href.replace(/^.*[\\\/]/, '') : '';
            if (ss === stylesheetName) {
                return true;
            }
        }
        return false;
    }
}

/**
 * The minimum length for a VCF structural variant.  VCF records < this length are ignored in the circular view
 * @type {number}
 */
const MINIMUM_SV_LENGTH = 1000000;

const shortChrName = (chrName) => {
    return chrName.startsWith("chr") ? chrName.substring(3) : chrName
};

const makePairedAlignmentChords = (alignments) => {

    const chords = [];
    for (let a of alignments) {

        if(a.paired) {
            if(a.firstAlignment && a.secondAlignment) {
                chords.push({
                    uniqueId: a.readName,
                    refName: shortChrName(a.firstAlignment.chr),
                    start: a.firstAlignment.start,
                    end: a.firstAlignment.end,
                    mate: {
                        refName: shortChrName(a.secondAlignment.chr),
                        start: a.secondAlignment.start,
                        end: a.secondAlignment.end,
                    }
                });
            }
        }
        else {
            const mate = a.mate;
            if (mate && mate.chr && mate.position) {
                chords.push({
                    uniqueId: a.readName,
                    refName: shortChrName(a.chr),
                    start: a.start,
                    end: a.end,
                    mate: {
                        refName: shortChrName(mate.chr),
                        start: mate.position - 1,
                        end: mate.position,
                    }
                });
            }
        }
    }
    return chords
};

const makeSupplementalAlignmentChords = (alignments) => {

    const makeChords = (a) => {
        const sa = a.tags()['SA'];
        const supAl = createSupplementaryAlignments(sa);
        let n = 0;
        for (let s of supAl) {
            if (s.start !== a.start) {
                chords.push({
                    uniqueId: `${a.readName}_${n++}`,
                    refName: shortChrName(a.chr),
                    start: a.start,
                    end: a.end,
                    mate: {
                        refName: shortChrName(s.chr),
                        start: s.start,
                        end: s.start + s.lenOnRef
                    }
                });
            }
        }
    };

    const chords = [];
    for (let a of alignments) {
        if(a.paired) {
            makeChords(a.firstAlignment);
            if(a.secondAlignment) {
                makeChords(a.secondAlignment);
            }
        } else {
            makeChords(a);
        }
    }
    return chords
};

const makeBedPEChords = (features) => {

    return features.map(v => {

        // If v is a whole-genome feature, get the true underlying variant.
        const f = v._f || v;

        return {
            uniqueId: `${f.chr1}:${f.start1}-${f.end1}_${f.chr2}:${f.start2}-${f.end2}`,
            refName: shortChrName(f.chr1),
            start: f.start1,
            end: f.end1,
            mate: {
                refName: shortChrName(f.chr2),
                start: f.start2,
                end: f.end2,
            }
        }
    })
};


const makeVCFChords = (features) => {

    const svFeatures = features.filter(v => {
        const f = v._f || v;
        const isLargeEnough = f.info && f.info.CHR2 && f.info.END &&
            (f.info.CHR2 !== f.chr || Math.abs(Number.parseInt(f.info.END) - f.pos) > MINIMUM_SV_LENGTH);
        return isLargeEnough
    });
    return svFeatures.map(v => {

        // If v is a whole-genome feature, get the true underlying variant.
        const f = v._f || v;

        const pos2 = Number.parseInt(f.info.END);
        const start2 = pos2 - 100;
        const end2 = pos2 + 100;

        return {
            uniqueId: `${f.chr}:${f.start}-${f.end}_${f.info.CHR2}:${f.info.END}`,
            refName: shortChrName(f.chr),
            start: f.start,
            end: f.end,
            mate: {
                refName: shortChrName(f.info.CHR2),
                start: start2,
                end: end2
            }
        }
    })
};

function makeCircViewChromosomes(genome) {
    const regions = [];
    const colors = [];
    if(genome.wgChromosomeNames) {
        for (let chrName of genome.wgChromosomeNames) {
            const chr = genome.getChromosome(chrName);
            colors.push(getChrColor$1(chr.name));
            regions.push(
                {
                    name: chr.name,
                    bpLength: chr.bpLength
                }
            );
        }
    }
    return regions
}

function sendChords(chords, track, refFrame, alpha) {

    const baseColor =  track.color || 'rgb(0,0,255)';

    const chordSetColor = IGVColor.addAlpha("all" === refFrame.chr ? baseColor : getChrColor$1(refFrame.chr), alpha);
    const trackColor = IGVColor.addAlpha(baseColor, alpha);

    // name the chord set to include locus and filtering information
    const encodedName = track.name.replaceAll(' ', '%20');
    const chordSetName = "all" === refFrame.chr ? encodedName :
        `${encodedName}  ${refFrame.chr}:${refFrame.start}-${refFrame.end}`;
    track.browser.circularView.addChords(chords, {track: chordSetName, color: chordSetColor, trackColor: trackColor});

    // show circular view if hidden
    if(!track.browser.circularViewVisible) track.browser.circularViewVisible = true;

}


function createCircularView(el, browser) {

    const circularView = new CircularView(el, {

        onChordClick: (feature, chordTrack, pluginManager) => {

            const f1 = feature.data;
            const f2 = f1.mate;
            addFrameForFeature(f1);
            addFrameForFeature(f2);

            function addFrameForFeature(feature) {

                feature.chr = browser.genome.getChromosomeName(feature.refName);
                let frameFound = false;
                for (let referenceFrame of browser.referenceFrameList) {
                    const l = Locus.fromLocusString(referenceFrame.getLocusString());
                    if (l.contains(feature)) {
                        frameFound = true;
                        break
                    } else if (l.overlaps(feature)) {
                        referenceFrame.extend(feature);
                        frameFound = true;
                        break
                    }
                }
                if (!frameFound) {
                    const flanking = 2000;
                    const center = (feature.start + feature.end) / 2;
                    browser.addMultiLocusPanel(feature.chr, center - flanking, center + flanking);

                }
            }
        }
    });

    return circularView
}

class PairedEndStats {

    constructor(alignments, {minTLENPercentile, maxTLENPercentile}) {
        this.totalCount = 0;
        this.frCount = 0;
        this.rfCount = 0;
        this.ffCount = 0;
        this.sumF = 0;
        this.sumF2 = 0;
        this.lp = minTLENPercentile === undefined ? 0.1 : minTLENPercentile;
        this.up = maxTLENPercentile === undefined ? 99.5 : maxTLENPercentile;
        this.isizes = [];
        this.compute(alignments);
    }

    compute(alignments) {

        for (let alignment of alignments) {
            if (alignment.isProperPair()) {
                var tlen = Math.abs(alignment.fragmentLength);
                this.sumF += tlen;
                this.sumF2 += tlen * tlen;
                this.isizes.push(tlen);

                var po = alignment.pairOrientation;

                if (typeof po === "string" && po.length === 4) {
                    var tmp = '' + po.charAt(0) + po.charAt(2);
                    switch (tmp) {
                        case 'FF':
                        case 'RR':
                            this.ffCount++;
                            break
                        case "FR":
                            this.frCount++;
                            break
                        case"RF":
                            this.rfCount++;
                    }
                }
                this.totalCount++;
            }
        }

        if (this.ffCount / this.totalCount > 0.9) this.orienation = "ff";
        else if (this.frCount / this.totalCount > 0.9) this.orienation = "fr";
        else if (this.rfCount / this.totalCount > 0.9) this.orienation = "rf";

        this.minTLEN = this.lp === 0 ? 0 : percentile(this.isizes, this.lp);
        this.maxTLEN = percentile(this.isizes, this.up);

        // var fMean = this.sumF / this.totalCount
        // var stdDev = Math.sqrt((this.totalCount * this.sumF2 - this.sumF * this.sumF) / (this.totalCount * this.totalCount))
        // this.minTLEN = fMean - 3 * stdDev
        // this.maxTLEN = fMean + 3 * stdDev

    }
}

function percentile(array, p) {

    if (array.length === 0) return undefined
    var k = Math.floor(array.length * (p / 100));
    array.sort(function (a, b) {
        return a - b
    });
    return array[k]

}

/**
 * C Modifications
 * C m 5mC 5-Methylcytosine 27551
 * C h 5hmC 5-Hydroxymethylcytosine 76792
 * C f 5fC 5-Formylcytosine 76794
 * C c 5caC 5-Carboxylcytosine 76793
 * C C Ambiguity code; any C mod
 */

const mColor = "rgb(255,0,0)";
const hColor = "rgb(255,0,255)";
const aColor = "rgb(51,0,111)";
const oColor = "rgb(111, 78, 129)";
const fColor = "rgb(246, 200, 95)";
const cColor = "rgb(157, 216, 102)";
const gColor = "rgb(255, 160, 86)";
const eColor = "rgb(141, 221, 208)";
const bColor = "rgb(202, 71, 47)";
const noModColor = "rgb(0,0,255)";
const genericColor = "rgb(132, 178, 158)";

const colors = new Map();
colors.set("m", mColor);
colors.set("h", hColor);
colors.set("o", oColor);
colors.set("f", fColor);
colors.set("c", cColor);
colors.set("g", gColor);
colors.set("e", eColor);
colors.set("b", bColor);
colors.set("h", hColor);
colors.set("a", aColor);
colors.set("NONE_A", noModColor);
colors.set("NONE_C", noModColor);
colors.set("NONE_T", noModColor);
colors.set("NONE_G", noModColor);
colors.set("NONE_N", noModColor);

/**
 * Cache for modified colors
 */
const modColorMap = new Map();

//String modification, byte likelihood, AlignmentTrack.ColorOption colorOption
function getModColor(modification, likelihood, colorOption) {

    let baseColor = getBaseColor(modification);

    let l = byteToUnsignedInt(likelihood);
    if (l > 255) {
        return baseColor
    }

    const key = modification + l + colorOption;

    const threshold = 256 * 0; //PreferencesManager.getPreferences().getAsFloat("SAM.BASEMOD_THRESHOLD");
    if (l < threshold) {
        l = 0;
    }
    if (!modColorMap.has(key)) {
        const alpha = colorOption === "basemod2" ?
            Math.max(20, Math.min(255, 20 + (l * l / 50 - 4 * l + 200))) :
            Math.max(20, Math.min(255, 6.127e-3 * l * l));

        const [r, g, b] = IGVColor.rgbComponents(baseColor);
        modColorMap.set(key, `rgba(${r},${g},${b},${alpha / 255})`);
    }
    return modColorMap.get(key)

}


function getBaseColor(modification, colorOption) {
    if (colors.has(modification)) {
        return colors.get(modification)
    } else {
        return genericColor
    }
}

class BaseModificationRenderer {

    constructor(alignmentTrack) {
        this.alignmentTrack = alignmentTrack;
    }

    /**
     * Update the context in which alignments are drawn.
     *  ctx,
     *  bpPerPixel,
     *  bpStart,
     *  bpEnd,
     *  pixelEnd,
     *  refSequence,
     *  refSequenceStart
     *
     * @param context
     */
    updateContext(context) {
        this.context = context;
    }

    drawModifications(alignment, y, height, context, colorOption, threshold) {

        const {ctx, pixelEnd, bpStart, bpPerPixel} = context;

        const baseModificationSets = alignment.getBaseModificationSets();
        if (baseModificationSets) {

            let selectedModification;
            const parts = colorOption.split(":");
            if(parts.length == 2) {
                colorOption = parts[0];
                selectedModification = parts[1];
            }

            for (let block of alignment.blocks) {

                if (block.type === 'S') continue   // Soft clip

                // Compute bounds
                const pY = y;
                const dY = height;
                let dX = Math.max(1, (1.0 / bpPerPixel));

                // Loop through sequence for this block
                for (let i = block.seqOffset; i < block.seqOffset + block.len; i++) {

                    let pX = ((block.start + (i - block.seqOffset) - bpStart) / bpPerPixel);
                    // Don't draw out of clipping rect
                    if (pX > pixelEnd) {
                        break
                    } else if (pX + dX < 0) {
                        continue
                    }

                    // Search all sets for modifications of this base, select modification with largest likelihood
                    let maxLh = -1;
                    let noModLh = 255;
                    let modification = undefined;
                    let canonicalBase = 0;

                    for (let bmSet of baseModificationSets) {
                        if(selectedModification && bmSet.modification !== selectedModification) {
                            continue
                        }
                        if (bmSet.containsPosition(i)) {
                            const lh = byteToUnsignedInt(bmSet.likelihoods.get(i));
                            noModLh -= lh;
                            if (!modification || lh > maxLh) {         // TODO -- filter
                                modification = bmSet.modification;
                                canonicalBase = bmSet.canonicalBase;
                                maxLh = lh;
                            }
                        }
                    }


                    if (modification) {

                        const scaledThreshold = threshold * 255;

                        let c;
                        if (noModLh > maxLh && colorOption === "basemod2" && noModLh >= scaledThreshold) {
                            c = getModColor("NONE_" + canonicalBase, noModLh, colorOption);
                        } else if (maxLh >= scaledThreshold) {
                            c = getModColor(modification, maxLh, colorOption);
                        }

                        ctx.fillStyle = c;

                        // Expand narrow width to make more visible
                        if (dX < 3) {
                            dX = 3;
                            pX--;
                        }
                        ctx.fillRect(pX, pY, dX, Math.max(1, dY - 2));

                    }
                }
            }
        }
    }
}

const alignmentStartGap = 5;
const downsampleRowHeight = 10;
const groupGap = 10;
const DEFAULT_ALIGNMENT_COLOR = "rgb(185, 185, 185)";
const DEFAULT_CONNECTOR_COLOR = "rgb(200, 200, 200)";
const DEFAULT_HIGHLIGHT_COLOR = "#00ff00";
const MINIMUM_BLAT_LENGTH = 20;

const pairCompatibleGroupOptions = new Set(["firstOfPairStrand"]);

class AlignmentTrack extends TrackBase {

    static defaults = {
        viewAsPairs: false,
        showSoftClips: false,
        showAllBases: false,
        showInsertions: true,
        showMismatches: true,
        colorBy: undefined,
        groupBy: undefined,
        displayMode: "EXPANDED",
        alignmentRowHeight: 14,
        squishedRowHeight: 3,
        negStrandColor: "rgba(150, 150, 230, 0.75)",
        posStrandColor: "rgba(230, 150, 150, 0.75)",
        baseModPosStrandColor: "rgb(195, 195, 195)",
        baseModNegStrandColor: "rgb(195, 210, 195)",
        insertionColor: "rgb(138, 94, 161)",
        insertionTextColor: "white",
        showInsertionText: false,
        deletionColor: "black",
        deletionTextColor: "black",
        showDeletionText: false,
        skippedColor: "rgb(150, 170, 170)",
        pairConnectorColor: undefined,
        smallTLENColor: "rgb(0, 0, 150)",
        largeTLENColor: "rgb(200, 0, 0)",
        expectedPairOrientation: 'fr',
        rlColor: "rgb(0, 150, 0)",
        rrColor: "rgb(20, 50, 200)",
        llColor: "rgb(0, 150, 150)",
        bamColorTag: "YC",
        hideSmallIndels: false,
        indelSizeThreshold: 1,
        highlightColor: undefined,
        minTLEN: undefined,
        maxTLEN: undefined,
        tagColorPallete: "Set1",
    }

    _colorTables = new Map()
    _baseModifications = new Set()

    constructor(config, browser) {

        super(config, browser);

        // Explicit color table
        if (config.colorTable || config.tagColorTable) {
            this.colorTable = new ColorTable(config.tagColorTable);
        }

        // Only one of showTags / hideTags should be specified.  If both are specified showTags takes precedence.
        if (config.showTags && config.hideTags) {
            console.warn("Both showTags and hideTags specified.  showTags will be used.");
        }
        if (config.showTags) {
            this.showTags = new Set(config.showTags);
            this.hiddenTags = new Set();
        } else {
            this.hiddenTags = new Set(config.hideTags || ["SA", "MD"]);
        }


        // Backward compatibility overrides
        if (config.largeFragmentLengthColor) this.largeTLENColor = config.largeFragmentLengthColor;
        if (config.pairOrienation) this.expectedPairOrientation = config.pairOrientation;
        if (config.smallFragmentLengthColor) this.smallTLENColor = config.smallFragmentLengthColor;
        if (config.largeFragmentLengthColor) this.largeTLENColor = config.largeFragmentLengthColor;
        if (config.minFragmentLength) this.minTLEN = config.minFragmentLength;
        if (config.maxFragmentLength) this.maxTLEN = config.maxFragmentLength;
        if (config.displayMode) this.displayMode = config.displayMode.toUpperCase();
        if (config.colorBy && config.colorByTag) {
            this.colorBy = config.colorBy + ":" + config.colorByTag;
        }

        this.featureSource = this.parent.featureSource;
        this.top = 0 === config.coverageTrackHeight ? 0 : config.coverageTrackHeight + 5;

        this.pairColors = {
            "RL": this.rlColor,
            "RR": this.rrColor,
            "LL": this.llColor
        };

        if (config.highlightedReads) {
            this.setHighlightedReads(config.highlightedReads);
        }

        this.hasPairs = false;   // Until proven otherwise
        this.hasSupplemental = false;


        this._groupByTags = [];
        this._groupByPositions = [];
        if (config.groupBy) {
            this.groupBy = config.groupBy;
            if (config.groupBy.startsWith("base:")) {
                this._groupByPositions.push(config.groupBy.substring(5));
            }
            if (config.groupBy.startsWith("tag:")) {
                this._groupByTags.push(config.groupBy.substring(4));
            }
        }

        // Listen for locus change events,  needed to repack alignments when in "FULL" mode
        this._locusChange = locusChange.bind(this);
        this.browser.on('locuschange', this._locusChange);
    }

    init(config) {
        this.parent = config.parent;  // A hack to get around initialization problem
        delete config.parent;
        super.init(config);
    }

    dispose() {
        this.browser.off('locuschange', this._locusChange);
    }

    /**
     * Lazy initialize a base modification renderer
     *
     * @returns {BaseModificationRenderer}
     */
    get baseModRenderer() {
        if (!this._baseModRenderer) {
            this._baseModRenderer = new BaseModificationRenderer(this);
        }
        return this._baseModRenderer
    }

    get baseModificationThreshold() {
        return this.parent.baseModificationThreshold
    }

    setTop(coverageTrack, showCoverage) {
        this.top = (0 === coverageTrack.height || false === showCoverage) ? 0 : (5 + coverageTrack.height);
    }

    setHighlightedReads(highlightedReads, highlightColor) {
        if (!Array.isArray(highlightedReads) || !highlightedReads.every(i => typeof i === "string")) {
            throw new Error("AlignmentTrack.setHighlightedReads() only accept array of strings")
        }
        if (highlightColor) {
            this.highlightColor = highlightColor;
        }
        this.highlightedReads = new Set(highlightedReads);
    }

    /**
     * Compute the pixel height required to display all content.
     *
     * @param alignmentContainer
     * @returns {number|*}
     */
    computePixelHeight(alignmentContainer) {

        if (alignmentContainer.packedGroups) {
            let h = alignmentContainer.hasDownsampledIntervals() ? downsampleRowHeight + alignmentStartGap : 0;
            const alignmentRowHeight = this.displayMode === "SQUISHED" ?
                this.squishedRowHeight :
                this.alignmentRowHeight;
            for (let group of alignmentContainer.packedGroups.values()) {
                h += (alignmentRowHeight * group.length) + groupGap;
            }
            return h + 5
        } else {
            return 0
        }
    }

    draw(options) {

        const alignmentContainer = options.features;
        const ctx = options.context;
        const bpPerPixel = options.bpPerPixel;
        const bpStart = options.bpStart;
        const pixelWidth = options.pixelWidth;
        const bpEnd = bpStart + pixelWidth * bpPerPixel + 1;
        const showSoftClips = this.showSoftClips;
        const showAllBases = this.showAllBases;
        const nucleotideColors = this.browser.nucleotideColors;

        ctx.save();

        // Update base modification keys.
        for (let k of alignmentContainer.baseModificationKeys) {
            this._baseModifications.add(k.modification);
        }

        let referenceSequence = alignmentContainer.sequence;
        if (referenceSequence) {
            referenceSequence = referenceSequence.toUpperCase();
        }
        let alignmentRowYInset = 0;

        // Set colorBy default if neccessary
        if (!this.colorBy) {
            this.colorBy = this.hasPairs ? "unexpectedPair" : "none";
        }

        let pixelTop = options.pixelTop - BAMTrack.coverageTrackHeight;
        if (this.top) {
            ctx.translate(0, this.top);
        }
        const pixelBottom = pixelTop + options.pixelHeight;

        if (alignmentContainer.hasDownsampledIntervals()) {
            alignmentRowYInset = downsampleRowHeight + alignmentStartGap;

            alignmentContainer.downsampledIntervals.forEach(function (interval) {
                var xBlockStart = (interval.start - bpStart) / bpPerPixel,
                    xBlockEnd = (interval.end - bpStart) / bpPerPixel;

                if (xBlockEnd - xBlockStart > 5) {
                    xBlockStart += 1;
                    xBlockEnd -= 1;
                }
                IGVGraphics.fillRect(ctx, xBlockStart, 2, (xBlockEnd - xBlockStart), downsampleRowHeight - 2, {fillStyle: "black"});
            });

        } else {
            alignmentRowYInset = 0;
        }

        // Transient variable -- rewritten on every draw, used for click object selection
        this.alignmentsYOffset = alignmentRowYInset;
        const alignmentRowHeight = this.displayMode === "SQUISHED" ?
            this.squishedRowHeight :
            this.alignmentRowHeight;

        const packedAlignmentGroups = alignmentContainer.packedGroups;

        if (packedAlignmentGroups) {

            let alignmentY = alignmentRowYInset;
            for (let groupName of packedAlignmentGroups.keys()) {

                const group = packedAlignmentGroups.get(groupName);
                const packedAlignmentRows = group.rows;
                const nRows = packedAlignmentRows.length;
                group.pixelTop = alignmentY;

                for (let rowIndex = 0; rowIndex < nRows; rowIndex++) {

                    const alignmentRow = packedAlignmentRows[rowIndex];
                    const alignmentHeight = alignmentRowHeight <= 4 ? alignmentRowHeight : alignmentRowHeight - 2;

                    if (alignmentY > pixelBottom) {
                        break
                    } else if (alignmentY + alignmentHeight < pixelTop) {
                        alignmentY += alignmentRowHeight;
                        continue
                    }

                    for (let alignment of alignmentRow.alignments) {

                        if (this.browser.circularView) {
                            // This is an expensive check, only do it if needed
                            this.hasSupplemental = this.hasSupplemental || alignment.hasTag('SA');
                        }

                        if ((alignment.start + alignment.lengthOnRef) < bpStart) continue
                        if (alignment.start > bpEnd) break
                        if (true === alignment.hidden) {
                            continue
                        }

                        if (alignment instanceof PairedAlignment) {

                            drawPairConnector.call(this, alignment, alignmentY, alignmentHeight);

                            drawSingleAlignment.call(this, alignment.firstAlignment, alignmentY, alignmentHeight);

                            if (alignment.secondAlignment) {
                                drawSingleAlignment.call(this, alignment.secondAlignment, alignmentY, alignmentHeight);
                            }

                        } else {
                            drawSingleAlignment.call(this, alignment, alignmentY, alignmentHeight);
                        }
                    }
                    alignmentY += alignmentRowHeight;
                }

                group.pixelBottom = alignmentY;

                if (this.groupBy && groupName) {

                    ctx.save();

                    ctx.font = '400 12px sans-serif';
                    const textMetrics = ctx.measureText(groupName);
                    const w = textMetrics.width + 10;
                    const x = -options.pixelShift + options.viewportWidth - w - 10;
                    const h = textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent + 10;
                    const baselineY = Math.min(group.pixelTop + h - 1, group.pixelBottom);

                    ctx.textAlign = "center";
                    ctx.fillStyle = 'white';
                    ctx.strokeStyle = 'lightGray';
                    IGVGraphics.roundRect(ctx, x, baselineY - textMetrics.actualBoundingBoxAscent - 5, w, h, 2, 1, 1);

                    ctx.fillStyle = 'black';
                    ctx.fillText(groupName, x + w / 2, baselineY);
                    IGVGraphics.dashedLine(ctx, 0, alignmentY, pixelWidth, alignmentY);

                    ctx.restore();

                    alignmentY += groupGap;  // Group separator
                }
            }
        }
        ctx.restore();

        // alignment is a PairedAlignment
        function drawPairConnector(alignment, yRect, alignmentHeight) {

            var connectorColor = this.getConnectorColor(alignment.firstAlignment),
                xBlockStart = (alignment.connectingStart - bpStart) / bpPerPixel,
                xBlockEnd = (alignment.connectingEnd - bpStart) / bpPerPixel,
                yStrokedLine = yRect + alignmentHeight / 2;

            if ((alignment.connectingEnd) < bpStart || alignment.connectingStart > bpEnd) {
                return
            }
            if (alignment.mq <= 0) {
                connectorColor = IGVColor.addAlpha(connectorColor, 0.15);
            }
            IGVGraphics.setProperties(ctx, {fillStyle: connectorColor, strokeStyle: connectorColor});
            IGVGraphics.strokeLine(ctx, xBlockStart, yStrokedLine, xBlockEnd, yStrokedLine);

        }

        function drawSingleAlignment(alignment, y, alignmentHeight) {


            if ((alignment.start + alignment.lengthOnRef) < bpStart || alignment.start > bpEnd) {
                return
            }

            const blocks = showSoftClips ? alignment.blocks : alignment.blocks.filter(b => 'S' !== b.type);

            let alignmentColor = this.getAlignmentColor(alignment);
            const outlineColor = alignmentColor;
            if (alignment.mq <= 0) {
                alignmentColor = IGVColor.addAlpha(alignmentColor, 0.15);
            }
            IGVGraphics.setProperties(ctx, {fillStyle: alignmentColor, strokeStyle: outlineColor});

            // Save bases to draw into an array for later drawing, so they can be overlaid on insertion blocks,
            // which is relevant if we have insertions with size label
            const basesToDraw = [];

            for (let b = 0; b < blocks.length; b++) {   // Can't use forEach here -- we need ability to break

                const block = blocks[b];

                // Somewhat complex test, neccessary to insure gaps are drawn.
                // If this is not the last block, and the next block starts before the orign (off screen to left) then skip.
                if ((b !== blocks.length - 1) && blocks[b + 1].start < bpStart) continue

                // drawBlock returns bases to draw, which are drawn on top of insertion blocks (if they're wider than
                // the space between two bases) like in Java IGV
                basesToDraw.push(...drawBlock.call(this, block, b));

                if ((block.start + block.len) > bpEnd) {
                    // Do this after drawBlock to insure gaps are drawn
                    break
                }
            }

            if (alignment.gaps) {
                const yStrokedLine = y + alignmentHeight / 2;
                for (let gap of alignment.gaps) {
                    const sPixel = (gap.start - bpStart) / bpPerPixel;
                    const ePixel = ((gap.start + gap.len) - bpStart) / bpPerPixel;
                    const lineWidth = ePixel - sPixel;
                    const gapLenText = gap.len.toString();
                    const gapTextWidth = gapLenText.length * 6;
                    const gapCenter = sPixel + (lineWidth / 2);

                    const color = ("D" === gap.type) ? this.deletionColor : this.skippedColor;

                    IGVGraphics.strokeLine(ctx, sPixel, yStrokedLine, ePixel, yStrokedLine, {
                        strokeStyle: color,
                        lineWidth: 2,
                    });

                    // Add gap width as text like Java IGV if it fits nicely and is a multi-base gap
                    if (this.showDeletionText && gap.len > 1 && lineWidth >= gapTextWidth + 8) {
                        const textStart = gapCenter - (gapTextWidth / 2);
                        IGVGraphics.fillRect(ctx, textStart - 1, y - 1, gapTextWidth + 2, 12, {fillStyle: "white"});
                        IGVGraphics.fillText(ctx, gapLenText, textStart, y + 10, {
                            'font': 'normal 10px monospace',
                            'fillStyle': this.deletionTextColor,
                        });
                    }
                }
            }

            if (alignment.insertions && this.showInsertions) {
                let lastXBlockStart = -1;
                for (let insertionBlock of alignment.insertions) {
                    if (this.hideSmallIndels && insertionBlock.len <= this.indelSizeThreshold) {
                        continue
                    }
                    if (insertionBlock.start < bpStart) {
                        continue
                    }
                    if (insertionBlock.start > bpEnd) {
                        break
                    }

                    const refOffset = insertionBlock.start - bpStart;
                    const insertLenText = insertionBlock.len.toString();

                    const textPixelWidth = 2 + (insertLenText.length * 6);
                    const basePixelWidth = (!this.showInsertionText || insertionBlock.len === 1)
                        ? 2
                        : Math.round(insertionBlock.len / bpPerPixel);
                    const widthBlock = Math.max(Math.min(textPixelWidth, basePixelWidth), 2);

                    const xBlockStart = (refOffset / bpPerPixel) - (widthBlock / 2);
                    if ((xBlockStart - lastXBlockStart) > 2) {
                        const props = {fillStyle: this.insertionColor};

                        // Draw decorations like Java IGV to make an 'I' shape
                        IGVGraphics.fillRect(ctx, xBlockStart - 2, y, widthBlock + 4, 2, props);
                        IGVGraphics.fillRect(ctx, xBlockStart, y + 2, widthBlock, alignmentHeight - 4, props);
                        IGVGraphics.fillRect(ctx, xBlockStart - 2, y + alignmentHeight - 2, widthBlock + 4, 2, props);

                        // Show # of inserted bases as text if it's a multi-base insertion and the insertion block
                        // is wide enough to hold text (its size is capped at the text label size, but can be smaller
                        // if the browser is zoomed out and the insertion is small)
                        if (this.showInsertionText && insertionBlock.len > 1 && basePixelWidth > textPixelWidth) {
                            IGVGraphics.fillText(ctx, insertLenText, xBlockStart + 1, y + 10, {
                                'font': 'normal 10px monospace',
                                'fillStyle': this.insertionTextColor,
                            });
                        }
                        lastXBlockStart = xBlockStart;
                    }
                }
            }

            for (let {bbox, baseColor, readChar} of basesToDraw) {
                const threshold = 1.0 / 10.0;
                if (bpPerPixel <= threshold && bbox.height >= 8) {
                    // render letter
                    const fontHeight = Math.min(10, bbox.height);
                    ctx.font = '' + fontHeight + 'px sans-serif';
                    const center = bbox.x + (bbox.width / 2.0);
                    IGVGraphics.strokeText(ctx, readChar, center - (ctx.measureText(readChar).width / 2), fontHeight - 1 + bbox.y, {strokeStyle: baseColor});
                } else {

                    // render colored block
                    IGVGraphics.fillRect(ctx, bbox.x, bbox.y, bbox.width, bbox.height, {fillStyle: baseColor});
                }
            }

            if (this.colorBy && this.colorBy.startsWith("basemod")) {
                const context = (
                    {
                        ctx,
                        bpPerPixel,
                        bpStart,
                        bpEnd,
                        pixelEnd: pixelWidth
                    });
                this.baseModRenderer.drawModifications(alignment, y, alignmentHeight, context, this.colorBy, this.baseModificationThreshold);
            }


            function drawBlock(block, b) {
                // Collect bases to draw for later rendering
                const blockBasesToDraw = [];

                const offsetBP = block.start - alignmentContainer.start;
                const blockStartPixel = (block.start - bpStart) / bpPerPixel;
                const blockEndPixel = ((block.start + block.len) - bpStart) / bpPerPixel;
                const blockWidthPixel = Math.max(1, blockEndPixel - blockStartPixel);

                //const arrowHeadWidthPixel = alignmentRowHeight / 2.0;
                const nomPixelWidthOnRef = 100 / bpPerPixel;
                const arrowHeadWidthPixel = Math.min(alignmentRowHeight / 2.0, nomPixelWidthOnRef / 6);

                const isSoftClip = 'S' === block.type;

                const strokeOutline =
                    alignment.mq <= 0 ||
                    this.selectedReadName === alignment.readName ||
                    isSoftClip ||
                    this.highlightedReads && this.highlightedReads.has(alignment.readName);

                let blockOutlineColor = outlineColor;
                if (this.selectedReadName === alignment.readName) {
                    blockOutlineColor = 'red';
                } else if (isSoftClip) {
                    blockOutlineColor = 'rgb(50,50,50)';
                } else if (this.highlightedReads && this.highlightedReads.has(alignment.readName)) {
                    blockOutlineColor = this.highlightColor || DEFAULT_HIGHLIGHT_COLOR;
                }

                const lastBlockPositiveStrand = (true === alignment.strand && b === blocks.length - 1);
                const lastBlockReverseStrand = (false === alignment.strand && b === 0);
                const lastBlock = lastBlockPositiveStrand | lastBlockReverseStrand;

                if (lastBlock) {
                    let xListPixel;
                    let yListPixel;
                    if (lastBlockPositiveStrand) {
                        xListPixel = [
                            blockStartPixel,
                            blockEndPixel,
                            blockEndPixel + arrowHeadWidthPixel,
                            blockEndPixel,
                            blockStartPixel,
                            blockStartPixel];
                        yListPixel = [
                            y,
                            y,
                            y + (alignmentHeight / 2.0),
                            y + alignmentHeight,
                            y + alignmentHeight,
                            y];

                    }

                    // Last block on - strand ?
                    else if (lastBlockReverseStrand) {
                        xListPixel = [
                            blockEndPixel,
                            blockStartPixel,
                            blockStartPixel - arrowHeadWidthPixel,
                            blockStartPixel,
                            blockEndPixel,
                            blockEndPixel];
                        yListPixel = [
                            y,
                            y,
                            y + (alignmentHeight / 2.0),
                            y + alignmentHeight,
                            y + alignmentHeight,
                            y];

                    }
                    IGVGraphics.fillPolygon(ctx, xListPixel, yListPixel, {fillStyle: alignmentColor});

                    if (strokeOutline) {
                        IGVGraphics.strokePolygon(ctx, xListPixel, yListPixel, {strokeStyle: blockOutlineColor});
                    }
                }

                // Internal block
                else {
                    IGVGraphics.fillRect(ctx, blockStartPixel, y, blockWidthPixel, alignmentHeight, {fillStyle: alignmentColor});

                    if (strokeOutline) {
                        ctx.save();
                        ctx.strokeStyle = blockOutlineColor;
                        ctx.strokeRect(blockStartPixel, y, blockWidthPixel, alignmentHeight);
                        ctx.restore();
                    }
                }


                // Read base coloring

                if (isSoftClip ||
                    showAllBases ||
                    this.showMismatches && (referenceSequence && alignment.seq && alignment.seq !== "*")) {

                    const seq = alignment.seq ? alignment.seq.toUpperCase() : undefined;
                    const qual = alignment.qual;
                    const seqOffset = block.seqOffset;
                    const widthPixel = Math.max(1, 1 / bpPerPixel);


                    for (let i = 0, len = block.len; i < len; i++) {

                        const xPixel = ((block.start + i) - bpStart) / bpPerPixel;

                        if (xPixel + widthPixel < 0) continue   // Off left edge
                        if (xPixel > pixelWidth) break  // Off right edge

                        let readChar = seq ? seq.charAt(seqOffset + i) : '';
                        const refChar = offsetBP + i >= 0 ? referenceSequence.charAt(offsetBP + i) : '';

                        if (readChar === "=") {
                            readChar = refChar;
                        }
                        if (readChar === "X" || refChar !== readChar || isSoftClip || showAllBases) {

                            let baseColor = nucleotideColors[readChar] || "rgb(0,0,0)";
                            if (!isSoftClip && qual !== undefined && qual.length > seqOffset + i) {
                                const readQual = qual[seqOffset + i];
                                baseColor = shadedBaseColor(readQual, baseColor);
                            }

                            blockBasesToDraw.push({
                                bbox: {
                                    x: xPixel,
                                    y: y,
                                    width: widthPixel,
                                    height: alignmentHeight
                                },
                                baseColor,
                                readChar,
                            });
                        }

                    }
                }

                return blockBasesToDraw
            }
        }

    };

    popupData(clickState) {
        const clickedObject = this.getClickedObject(clickState);
        return clickedObject?.popupData(clickState.genomicLocation, this.hiddenTags, this.showTags)
    };

    /**
     * Return menu items for the AlignmentTrack
     *
     * NOTE: click handler functions are called in the context of the parent BAMTrack, thus the rather odd looking
     * reference to "this.alignmentTrack" in click handler function scopes.
     *
     * @returns {*[]}
     */
    menuItemList() {

        // Start with overage track items
        let menuItems = [];

        // Color by items //////////////////////////////////////////////////
        menuItems.push('<hr/>');
        let element = createElementWithString('<div class="igv-track-menu-category">');
        element.innerText = 'Color by:';
        menuItems.push({name: undefined, element, click: undefined, init: undefined});

        const colorByMenuItems = [];
        colorByMenuItems.push({key: 'none', label: 'none'});
        colorByMenuItems.push({key: 'strand', label: 'read strand'});
        if (this.hasPairs) {
            colorByMenuItems.push({key: 'firstOfPairStrand', label: 'first-of-pair strand'});
            colorByMenuItems.push({key: 'pairOrientation', label: 'pair orientation'});
            colorByMenuItems.push({key: 'tlen', label: 'insert size (TLEN)'});
            colorByMenuItems.push({key: 'unexpectedPair', label: 'pair orientation & insert size (TLEN)'});
        }
        if (this.colorBy && this.colorBy.startsWith("tag:")) {
            colorByMenuItems.push({key: this.colorBy, label: this.colorBy});
        }
        colorByMenuItems.push({key: 'tag', label: 'tag...'});
        for (let item of colorByMenuItems) {
            const selected = (this.colorBy === undefined && item.key === 'none') || this.colorBy === item.key;
            menuItems.push(this.colorByCB(item, selected));
        }


        const size = this._baseModifications.size;
        if (size > 0) {
            menuItems.push('<hr style="border-top:1px dotted;border-bottom: none;">');
            let label = size === 1 ? 'base modification' : 'base modification (all)';
            menuItems.push(this.basemodColorByCB({
                key: 'basemod', label
            }));
            if (size > 1) {
                for (let m of this._baseModifications) {
                    menuItems.push(this.basemodColorByCB({
                            key: 'basemod:' + m,
                            label: `base modification (${modificationName(m)})`
                        }));
                }
            }

            menuItems.push('<hr style="border-top:1px dotted;border-bottom: none;">');
            label = size === 1 ? 'base modification 2-color' : 'base modification 2-color (all)';
            menuItems.push(this.basemodColorByCB({
                key: 'basemod2', label
            }));
            if (size > 1) {
                for (let m of this._baseModifications) {
                    menuItems.push(this.basemodColorByCB({
                        key: 'basemod2:' + m,
                        label: `base modification 2-color (${modificationName(m)})`
                    }));
                }
            }
        }

        // Group by items //////////////////////////////////////////////////
        menuItems.push('<hr/>');
        element = document.createElement('div');
        element.className = 'igv-track-menu-category';
        element.textContent = 'Group by:';
        menuItems.push({name: undefined, element, click: undefined, init: undefined});

        const groupByMenuItems = [];
        groupByMenuItems.push({key: 'none', label: 'none'});
        groupByMenuItems.push({key: 'strand', label: 'read strand'});
        if (this.hasPairs) {
            groupByMenuItems.push({key: 'firstOfPairStrand', label: 'first-of-pair strand'});
            groupByMenuItems.push({key: 'pairOrientation', label: 'pair orientation'});
            groupByMenuItems.push({key: 'mateChr', label: 'chromosome of mate'});
        }
        groupByMenuItems.push({key: 'chimeric', label: 'chimeric'});
        groupByMenuItems.push({key: 'supplementary', label: 'supplementary flag'});
        groupByMenuItems.push({key: 'readOrder', label: 'read order'});
        //groupByMenuItems.push({key: 'phase', label: 'phase'})

        for (let groupByTag of this._groupByTags) {
            groupByMenuItems.push({key: `tag:${groupByTag}`, label: `tag:${groupByTag}`});
        }
        for (let groupByPos of this._groupByPositions) {
            groupByMenuItems.push({key: `base:${groupByPos}`, label: `base:${groupByPos}`});
        }

        groupByMenuItems.push({key: 'tag', label: 'tag...'});

        for (let item of groupByMenuItems) {
            const selected = (this.groupBy === undefined && item.key === 'none') || this.groupBy === item.key;
            menuItems.push(this.groupByCB(item, selected));
        }

        // Show all bases
        menuItems.push('<hr/>');
        menuItems.push({
            element: createCheckbox("Show all bases", this.showAllBases),
            click: function showAllBasesHandler() {
                this.alignmentTrack.showAllBases = !this.alignmentTrack.showAllBases;
                this.trackView.repaintViews();
            }
        });

        // Show mismatches
        menuItems.push({
            element: createCheckbox("Show mismatches", this.showMismatches),
            click: function showMismatchesHandler() {
                this.alignmentTrack.showMismatches = !this.alignmentTrack.showMismatches;
                this.trackView.repaintViews();
            }
        });

        // Insertions
        menuItems.push({
            element: createCheckbox("Show insertions", this.showInsertions),
            click: function showInsertionsHandler() {
                this.alignmentTrack.showInsertions = !this.alignmentTrack.showInsertions;
                this.trackView.repaintViews();
            }
        });

        // Soft clips
        menuItems.push({
            element: createCheckbox("Show soft clips", this.showSoftClips),
            click: function showSoftClipsHandler() {
                this.alignmentTrack.showSoftClips = !this.alignmentTrack.showSoftClips;
                const alignmentContainers = this.getCachedAlignmentContainers();
                for (let ac of alignmentContainers) {
                    ac.pack(this);
                }
                this.trackView.repaintViews();
            }
        });

        // View as pairs
        if (this.hasPairs) {
            menuItems.push('<hr/>');
            menuItems.push({
                element: createCheckbox("View as pairs", this.viewAsPairs),
                click: function viewAsPairsHandler() {
                    const b = !this.alignmentTrack.viewAsPairs;
                    if (b && this.groupBy && !pairCompatibleGroupOptions.has(this.groupBy)) {
                        this.browser.alert.present(`'View as Pairs' is incompatible with 'Group By ${this.groupBy}'`);
                        return
                    }
                    this.alignmentTrack.viewAsPairs = b;
                    const alignmentContainers = this.getCachedAlignmentContainers();
                    for (let ac of alignmentContainers) {
                        ac.pack(this);
                    }
                    this.trackView.checkContentHeight();
                    this.trackView.repaintViews();
                }
            });
        }

        // Add chords to JBrowse circular view, if present
        if (this.browser.circularView &&
            (this.hasPairs || this.hasSupplemental)) {
            menuItems.push('<hr/>');
            if (this.hasPairs) {
                menuItems.push({
                    label: 'Add discordant pairs to circular view',
                    click: function discordantPairsHandler() {
                        for (let viewport of this.trackView.viewports) {
                            this.addPairedChordsForViewport(viewport);
                        }
                    }
                });
            }
            if (this.hasSupplemental) {
                menuItems.push({
                    label: 'Add split reads to circular view',
                    click: function splitReadsHandler() {
                        for (let viewport of this.trackView.viewports) {
                            this.addSplitChordsForViewport(viewport);
                        }
                    }
                });
            }
        }


        // Display mode
        menuItems.push('<hr/>');

        element = document.createElement('div');
        element.className = 'igv-track-menu-category';
        element.textContent = 'Display mode:';
        menuItems.push({name: undefined, element, click: undefined, init: undefined});

        for (let mode of ["EXPANDED", "SQUISHED", "FULL"])
            menuItems.push({
                element: createCheckbox(mode.toLowerCase(), this.displayMode === mode),
                click: function expandHandler() {
                    this.alignmentTrack.setDisplayMode(mode);
                }
            });

        return menuItems
    }

    setDisplayMode(mode) {
        const repack = "FULL" === this.displayMode || "FULL" === mode;
        this.displayMode = mode;
        if (repack) {
            const alignmentContainers = this.getCachedAlignmentContainers();
            for (let ac of alignmentContainers) {
                ac.pack(this);
            }
        }
        this.trackView.checkContentHeight();
        this.trackView.repaintViews();
    }

    /**
     * Create a "color by" checkbox menu item, optionally initially checked
     *
     * NOTE: click handler functions are called in the context of the parent BAMTrack, thus the rather odd looking
     * reference to "this.alignmentTrack".
     *
     * @param menuItem
     * @param showCheck
     */
    colorByCB(menuItem, showCheck) {

        const element = createCheckbox(menuItem.label, showCheck);

        if (menuItem.key !== 'tag') {

            function clickHandler() {
                this.alignmentTrack.colorBy = menuItem.key;
                this.trackView.repaintViews();
            }

            return {name: undefined, element, click: clickHandler, init: undefined}
        } else {

            function dialogPresentationHandler(ev) {

                const tag = this.alignmentTrack.colorBy.startsWith("tag:") ? this.alignmentTrack.colorBy.substring(4) : '';

                this.browser.inputDialog.present({
                    label: 'Tag Name',
                    value: tag,
                    callback: (tag) => {
                        const alignmentTrack = this.alignmentTrack;
                        if (tag) {
                            alignmentTrack.colorBy = 'tag:' + tag;
                            if (!alignmentTrack.colorTable) {
                                alignmentTrack.colorTable = new PaletteColorTable("Set1");
                            }
                        } else {
                            alignmentTrack.colorBy = undefined;
                        }
                        this.trackView.repaintViews();
                    }
                }, ev);
            }

            return {name: undefined, element, dialog: dialogPresentationHandler, init: undefined}

        }
    }

    basemodColorByCB(menuItem) {

        const showCheck = this.colorBy === menuItem.key;

        function clickHandler() {
            this.alignmentTrack.colorBy = menuItem.key;
            this.trackView.repaintViews();
        }

        return {
            name: undefined,
            element: createCheckbox(menuItem.label, showCheck),
            click: clickHandler,
            init: undefined
        }
    }


    /**
     * Create a "group by" checkbox menu item, optionally initially checked.
     *
     * NOTE: click handler functions are called in the context of the parent BAMTrack, thus the rather odd looking
     * reference to "this.alignmentTrack".
     *
     * @param menuItem
     * @param showCheck
     */
    groupByCB(menuItem, showCheck) {

        function clickHandler(ev) {

            const doGroupBy = () => this.alignmentTrack.repackAlignments();

            if (menuItem.key === 'tag') {
                let currentTag = '';
                if (this.alignmentTrack.groupBy && this.alignmentTrack.groupBy.startsWith('tag:')) {
                    currentTag = this.alignmentTrack.groupBy.substring(4);
                }

                this.browser.inputDialog.present({
                    label: 'Tag Name',
                    value: currentTag,
                    callback: (tag) => {
                        if (tag) {
                            this.alignmentTrack.groupBy = 'tag:' + tag;
                            this.alignmentTrack._groupByTags.push(tag);
                            doGroupBy();
                        }
                    }
                }, ev);
            } else {

                if (menuItem.key === 'none') {
                    this.alignmentTrack.groupBy = undefined;
                } else {
                    this.alignmentTrack.groupBy = menuItem.key;
                }
                doGroupBy();
            }
        }

        return {
            name: undefined,
            element: createCheckbox(menuItem.label, showCheck),
            dialog: clickHandler,
            init: undefined
        }

    }

    repackAlignments() {
        const alignmentContainers = this.getCachedAlignmentContainers();
        for (let ac of alignmentContainers) {
            if (typeof ac.pack === 'function') ac.pack(this);
        }
        this.trackView.checkContentHeight();
        this.trackView.repaintViews();
    }


    contextMenuItemList(clickState) {

        const viewport = clickState.viewport;
        const list = [];

        const sortByOption = (option) => {
            const cs = this.sortObject;
            const direction = (cs && cs.position === Math.floor(clickState.genomicLocation)) ? !cs.direction : true;
            const newSortObject = {
                chr: viewport.referenceFrame.chr,
                position: Math.floor(clickState.genomicLocation),
                option: option,
                direction: direction,
                sortAsPairs: viewport.trackView.track.viewAsPairs
            };
            this.sortObject = newSortObject;
            viewport.cachedFeatures.sortRows(newSortObject);
            viewport.repaint();
        };
        list.push('<b>Sort by...</b>');
        list.push({label: '&nbsp; base', click: () => sortByOption("BASE")});
        list.push({label: '&nbsp; read strand', click: () => sortByOption("strand")});
        list.push({label: '&nbsp; start location', click: () => sortByOption("START")});
        list.push({label: '&nbsp; insert size', click: () => sortByOption("INSERT_SIZE")});
        list.push({label: '&nbsp; gap size', click: () => sortByOption("GAP_SIZE")});
        list.push({label: '&nbsp; chromosome of mate', click: () => sortByOption("MATE_CHR")});
        list.push({label: '&nbsp; mapping quality', click: () => sortByOption("MQ")});
        list.push({label: '&nbsp; read name', click: () => sortByOption("READ_NAME")});
        list.push({label: '&nbsp; aligned read length', click: () => sortByOption("ALIGNED_READ_LENGTH")});
        list.push({
            label: '&nbsp; tag', click: () => {
                const cs = this.sortObject;
                const direction = (cs && cs.position === Math.floor(clickState.genomicLocation)) ? !cs.direction : true;
                const config =
                    {
                        label: 'Tag Name',
                        value: this.sortByTag ? this.sortByTag : '',
                        callback: (tag) => {
                            if (tag) {
                                const newSortObject = {
                                    chr: viewport.referenceFrame.chr,
                                    position: Math.floor(clickState.genomicLocation),
                                    option: "TAG",
                                    tag: tag,
                                    direction: direction
                                };
                                this.sortByTag = tag;
                                this.sortObject = newSortObject;
                                viewport.cachedFeatures.sortRows(newSortObject);
                                viewport.repaint();
                            }
                        }
                    };
                this.browser.inputDialog.present(config, clickState.event);
            }
        });
        list.push('<hr/>');

        // Positional group by options
        const position = `${viewport.referenceFrame.chr}:${numberFormatter$1(Math.floor(clickState.genomicLocation) + 1)}`;
        list.push({
            label: `Group by base @${position}`,
            click: () => {
                this._groupByPositions.push(position);
                this.groupBy = `base:${position}`;
                const alignmentContainers = this.getCachedAlignmentContainers();
                for (let ac of alignmentContainers) {
                    ac.pack(this);
                }
                this.trackView.checkContentHeight();
                this.trackView.repaintViews();
            }
        });
        list.push('<hr/>');


        const clickedObject = this.getClickedObject(clickState);

        if (clickedObject) {

            const showSoftClips = this.showSoftClips;
            const clickedAlignment = (typeof clickedObject.alignmentContaining === 'function') ?
                clickedObject.alignmentContaining(clickState.genomicLocation, showSoftClips) :
                clickedObject;
            if (clickedAlignment) {
                if (clickedAlignment.isPaired() && clickedAlignment.isMateMapped()) {
                    list.push({
                        label: 'View mate in split screen',
                        click: () => {
                            if (clickedAlignment.mate) {
                                const referenceFrame = clickState.viewport.referenceFrame;
                                const chromosomeObject = this.browser.genome.getChromosome(clickedAlignment.mate.chr);
                                if (chromosomeObject) {
                                    this.selectedReadName = clickedAlignment.readName;
                                    //this.browser.presentMultiLocusPanel(clickedAlignment, referenceFrame)
                                    const bpWidth = referenceFrame.bpPerPixel * clickState.viewport.getWidth();
                                    const frameStart = clickedAlignment.mate.position - bpWidth / 2;
                                    const frameEnd = clickedAlignment.mate.position + bpWidth / 2;
                                    this.browser.addMultiLocusPanel(chromosomeObject.name, frameStart, frameEnd, referenceFrame);
                                } else {
                                    this.browser.alert.present(`Reference does not contain chromosome: ${clickedAlignment.mate.chr}`);
                                }
                            }
                        },
                        init: undefined
                    });
                }

                list.push('<hr/>');
                const softClips = clickedAlignment.softClippedBlocks();
                list.push({
                    label: 'View read sequence',
                    click: () => {
                        const seqstring = clickedAlignment.seq;
                        this.browser.alert.present(seqstring && seqstring !== "*" ? seqstring : "Read sequence: *");
                    }
                });

                if (softClips.left && softClips.left.len > 0) {
                    list.push({
                        label: 'View left soft-clipped sequence',
                        click: () => {
                            const clippedSequence = clickedAlignment.seq.substring(softClips.left.seqOffset, softClips.left.seqOffset + softClips.left.len);
                            this.browser.alert.present(clippedSequence);
                        }
                    });
                }

                if (softClips.right && softClips.right.len > 0) {
                    list.push({
                        label: 'View right soft-clipped sequence',
                        click: () => {
                            const clippedSequence = clickedAlignment.seq.substring(softClips.right.seqOffset, softClips.right.seqOffset + softClips.right.len);
                            this.browser.alert.present(clippedSequence);
                        }
                    });
                }

                list.push('<hr/>');

                if (isSecureContext()) {
                    list.push({
                        label: 'Copy read sequence',
                        click: async () => {
                            try {
                                await navigator.clipboard.writeText(clickedAlignment.seq);
                            } catch (e) {
                                console.error(e);
                                this.browser.alert.present(`error copying sequence to clipboard ${e}`);
                            }
                        }
                    });

                    if (softClips.left && softClips.left.len > 0) {
                        list.push({
                            label: 'Copy left soft-clipped sequence',
                            click: async () => {
                                try {
                                    const clippedSequence = clickedAlignment.seq.substring(softClips.left.seqOffset, softClips.left.seqOffset + softClips.left.len);
                                    await navigator.clipboard.writeText(clippedSequence);
                                } catch (e) {
                                    console.error(e);
                                    this.browser.alert.present(`error copying sequence to clipboard ${e}`);
                                }
                            }
                        });
                    }

                    if (softClips.right && softClips.right.len > 0) {
                        list.push({
                            label: 'Copy right soft-clipped sequence',
                            click: async () => {
                                try {
                                    const clippedSequence = clickedAlignment.seq.substring(softClips.right.seqOffset, softClips.right.seqOffset + softClips.right.len);
                                    await navigator.clipboard.writeText(clippedSequence);
                                } catch (e) {
                                    console.error(e);
                                    this.browser.alert.present(`error copying sequence to clipboard ${e}`);
                                }
                            }
                        });
                    }
                }

                // TODO test if genome supports blat
                const seqstring = clickedAlignment.seq;
                if (seqstring && "*" !== seqstring) {

                    list.push('<hr/>');

                    if (seqstring.length < maxSequenceSize$1) {
                        list.push({
                            label: 'BLAT visible sequence',
                            click: () => {
                                const sequence = clickedAlignment.isNegativeStrand() ? reverseComplementSequence(seqstring) : seqstring;
                                const name = `${clickedAlignment.readName} - blat`;
                                const title = `${this.name} - ${name}`;
                                createBlatTrack({sequence, browser: this.browser, name, title});
                            }
                        });
                    }

                    const softClips = clickedAlignment.softClippedBlocks();
                    if (softClips.left && softClips.left.len > MINIMUM_BLAT_LENGTH && softClips.left.len < maxSequenceSize$1) {
                        list.push({
                            label: 'BLAT left soft-clipped sequence',
                            click: () => {
                                const clippedSequence = seqstring.substring(softClips.left.seqOffset, softClips.left.seqOffset + softClips.left.len);
                                const sequence = clickedAlignment.isNegativeStrand() ? reverseComplementSequence(clippedSequence) : clippedSequence;
                                const name = `${clickedAlignment.readName} - blat left clip`;
                                const title = `${this.name} - ${name}`;
                                createBlatTrack({sequence, browser: this.browser, name, title});
                            }
                        });
                    }
                    if (softClips.right && softClips.right.len > MINIMUM_BLAT_LENGTH && softClips.right.len < maxSequenceSize$1) {
                        list.push({
                            label: 'BLAT right soft-clipped sequence',
                            click: () => {
                                const clippedSequence = seqstring.substring(softClips.right.seqOffset, softClips.right.seqOffset + softClips.right.len);
                                const sequence = clickedAlignment.isNegativeStrand() ? reverseComplementSequence(clippedSequence) : clippedSequence;
                                const name = `${clickedAlignment.readName} - blat right clip`;
                                const title = `${this.name} - ${name}`;
                                createBlatTrack({sequence, browser: this.browser, name, title});
                            }
                        });
                    }
                }

                list.push('<hr/>');
            }
        }

        // Experimental JBrowse feature
        if (this.browser.circularView && (this.hasPairs || this.hasSupplemental)) {
            if (this.hasPairs) {
                list.push({
                    label: 'Add discordant pairs to circular view',
                    click: () => {
                        this.addPairedChordsForViewport(viewport);
                    }
                });
            }
            if (this.hasSupplemental) {
                list.push({
                    label: 'Add split reads to circular view',
                    click: () => {
                        this.addSplitChordsForViewport(viewport);
                    }
                });
            }
            list.push('<hr/>');
        }

        return list

    }

    getClickedObject(clickState) {

        const viewport = clickState.viewport;
        let features = viewport.cachedFeatures;
        if (!features) return

        const y = clickState.y;
        const offsetY = y - this.top;
        const genomicLocation = clickState.genomicLocation;

        if (features.packedGroups) {
            let minGroupY = Number.MAX_VALUE;
            for (let group of features.packedGroups.values()) {
                minGroupY = Math.min(minGroupY, group.pixelTop);
                if (offsetY > group.pixelTop && offsetY <= group.pixelBottom) {

                    const alignmentRowHeight = this.displayMode === "SQUISHED" ?
                        this.squishedRowHeight :
                        this.alignmentRowHeight;

                    let packedAlignmentsIndex = Math.floor((offsetY - group.pixelTop) / alignmentRowHeight);

                    if (packedAlignmentsIndex >= 0 && packedAlignmentsIndex < group.length) {
                        const alignmentRow = group.rows[packedAlignmentsIndex];
                        const clicked = alignmentRow.alignments.filter(alignment => alignment.containsLocation(genomicLocation, this.showSoftClips));
                        if (clicked.length > 0) return clicked[0]
                    }
                }
            }
        }

        // If we get here check downsampled intervals
        if (offsetY < minGroupY && features.downsampledIntervals) {
            for (const interval of features.downsampledIntervals) {
                if (interval.start <= genomicLocation && interval.end >= genomicLocation) {
                    return interval
                }
            }
        }


    }

    /**
     * Return the color for connectors in paired alignment view.   If explicitly set return that, otherwise return
     * the alignment color, unless the color option can result in split colors (separte color for each mate).
     *
     * @param alignment
     * @returns {string}
     */
    getConnectorColor(alignment) {

        if (this.pairConnectorColor) {
            return this.pairConnectorColor
        }

        let colorBy = this.colorBy;
        if (colorBy && colorBy.startsWith("tag:")) {
            colorBy.substring(4);
            colorBy = "tag";
        }

        switch (colorBy) {
            case "strand":
            case "firstOfPairStrand":
            case "pairOrientation":
            case "tag":
                if (this.color) {
                    return (typeof this.color === "function") ? this.color(alignment) : this.color
                } else {
                    return DEFAULT_CONNECTOR_COLOR
                }
            default:
                return this.getAlignmentColor(alignment)

        }
    }

    getAlignmentColor(alignment) {

        let color;
        if (this.color) {
            color = (typeof this.color === "function") ? this.color(alignment) : this.color;
        } else {
            color = DEFAULT_ALIGNMENT_COLOR;
        }

        let colorBy = this.colorBy;
        let tag;
        if (colorBy && colorBy.startsWith("tag:")) {
            tag = colorBy.substring(4);
            colorBy = "tag";
        }
        switch (colorBy) {

            case "basemod":
            case "basemod2":
                color = alignment.strand ? this.baseModPosStrandColor : this.baseModNegStrandColor;
                break
            case "strand":
                color = alignment.strand ? this.posStrandColor : this.negStrandColor;
                break

            case "firstOfPairStrand":
                const s = alignment.firstOfPairStrand;
                if (s !== undefined) {
                    color = s ? this.posStrandColor : this.negStrandColor;
                }
                break

            case "unexpectedPair":
            case "pairOrientation":

                if (alignment.pairOrientation) {
                    const oTypes = orientationTypes[this.expectedPairOrientation];
                    if (oTypes) {
                        const pairColor = this.pairColors[oTypes[alignment.pairOrientation]];
                        if (pairColor) {
                            color = pairColor;
                            break
                        }
                    }
                }
                if ("pairOrientation" === colorBy) {
                    break
                }

            case "tlen":
            case "fragmentLength":

                if (alignment.mate && alignment.isMateMapped()) {
                    if (alignment.mate.chr !== alignment.chr) {
                        color = getChrColor$1(alignment.mate.chr);
                    } else if (this.minTemplateLength && Math.abs(alignment.fragmentLength) < this.minTemplateLength) {
                        color = this.smallTLENColor;
                    } else if (this.maxTemplateLength && Math.abs(alignment.fragmentLength) > this.maxTemplateLength) {
                        color = this.largeTLENColor;
                    }
                }
                break

            case "tag":
                const tagValue = alignment.tags()[tag];
                if (tagValue !== undefined) {

                    // If the tag value can be interpreted as a color, use it
                    if (typeof tagValue.startsWith === 'function') {
                        color = IGVColor.createColorStringSafe(tagValue);
                    }

                    // Tag value is not a color, use a color table
                    if (!color) {
                        if (!this.colorTable) {
                            this.colorTable = new PaletteColorTable(this.tagColorPallete);
                        }
                        color = this.colorTable.getColor(tagValue);
                    }
                }
                break
        }

        return color

    }

    get nucleotideColors() {
        return this.browser.nucleotideColors
    }


    get minTemplateLength() {
        return (this.minTLEN !== undefined) ? this.minTLEN :
            this.parent._pairedEndStats ? this.parent._pairedEndStats.minTLEN : 0
    }

    get maxTemplateLength() {
        return (this.maxTLEN !== undefined) ? this.maxTLEN :
            this.parent._pairedEndStats ? this.parent._pairedEndStats.maxTLEN : 1000
    }

    getState() {
        const config = super.getState();
        if (this.highlightedReads) {
            config.highlightedReads = Array.from(this.highlightedReads);
        }
        return config
    }


    // Property delegates

    get name() {
        return this.parent.name
    }

    set name(nm) {
        this.parent.name = nm;
    }

    get color() {
        return this.parent.color
    }

    set color(c) {
        this.parent.color = c;
    }

    get trackView() {
        return this.parent.trackView
    }

    get getCachedAlignmentContainers() {
        return this.parent.getCachedAlignmentContainers
    }

    get sortObject() {
        return this.parent.sortObject
    }

    set sortObject(obj) {
        this.parent.sortObject = obj;
    }

    addPairedChordsForViewport(viewport) {
        return this.parent.addPairedChordsForViewport(viewport)
    }

    addSplitChordsForViewport(viewport) {
        return this.parent.addSplitChordsForViewport(viewport)
    }

}

function shadedBaseColor(qual, baseColor) {

    const minQ = 5;   //prefs.getAsInt(PreferenceManager.SAM_BASE_QUALITY_MIN),
    const maxQ = 20;  //prefs.getAsInt(PreferenceManager.SAM_BASE_QUALITY_MAX);

    let alpha;
    if (qual < minQ) {
        alpha = 0.1;
    } else {
        alpha = Math.max(0.1, Math.min(1.0, 0.1 + 0.9 * (qual - minQ) / (maxQ - minQ)));
    }
    // Round alpha to nearest 0.1
    alpha = Math.round(alpha * 10) / 10.0;

    if (alpha < 1) {
        baseColor = IGVColor.addAlpha(baseColor, alpha);
    }
    return baseColor
}

function locusChange() {
    if ("FULL" === this.displayMode && !this.browser.isTrackPanning()) {
        this.repackAlignments();
    }
}

function drawModifications(ctx,
                           pX,
                           pBottom,
                           dX,
                           barHeight,
                           pos,
                           alignmentContainer,
                           colorOption,
                           threshold) {

    const modificationCounts = alignmentContainer.baseModCounts;
    const coverageMap = alignmentContainer.coverageMap;

    if (modificationCounts) {

        let selectedModification;
        const parts = colorOption.split(":");
        if(parts.length == 2) {
            colorOption = parts[0];
            selectedModification = parts[1];
        }

        //Set<BaseModificationKey> allModificationKeys = modificationCounts.getAllModificationKeys();
        //List<BaseModificationKey> sortedKeys = new ArrayList<>(allModificationKeys);
        const sortedKeys = Array.from(modificationCounts.allModifications);
        sortedKeys.sort(BaseModificationKey.compare);

        const total = coverageMap.getTotalCount(pos);

        // If site has no modification likelihoods skip (don't draw only "NONE_")
        const realModificationKeys = sortedKeys.filter(key => {
            if (selectedModification) {
                return selectedModification === key.modification
            } else {
                return !key.modification.startsWith("NONE_")
            }
        });
        if(!realModificationKeys.find(key => modificationCounts.getCount(pos, key, 0, false) > 0)) {
            return
        }

        for (let key of sortedKeys) {

            //if (filter && !filter.pass(key.modification, key.getCanonicalBase())) continue;

            if (key.modification.startsWith("NONE_") && colorOption !== "basemod2")
                continue

            if(selectedModification && selectedModification !== key.modification && !key.modification.startsWith("NONE_")) {
                continue
            }


            const base = key.base;
            const compl = complementBase(base);

            const modifiable = coverageMap.getCount(pos, base) + coverageMap.getCount(pos, compl);
            const detectable = modificationCounts.simplexModifications.has(key.modification) ?
                coverageMap.getPosCount(pos, base) + coverageMap.getNegCount(pos, compl) :
                modifiable;


            if (detectable == 0) continue  //No informative reads

            const includeNoMod = colorOption === "basemod2";

            const count = modificationCounts.getCount(pos, key, threshold, includeNoMod );
            if (count == 0) continue

            const modFraction = (modifiable / total) * (count / detectable);
            const modHeight = Math.round(modFraction * barHeight);

            const likelihoodSum = modificationCounts.getLikelihoodSum(pos, key, threshold, includeNoMod);
            const averageLikelihood = likelihoodSum / count;

            const baseY = pBottom - modHeight;
            const modColor = getModColor(key.modification, averageLikelihood, colorOption);

            ctx.fillStyle = modColor;
            ctx.fillRect(pX, baseY, dX, modHeight);
            pBottom = baseY;
        }
    }
}

const DEFAULT_COVERAGE_COLOR = "rgb(150, 150, 150)";

class CoverageTrack  {


    constructor(config, parent) {
        this.featureType = 'numeric';
        this.parent = parent;
        this.featureSource = parent.featureSource;

        this.paintAxis = paintAxis;
        this.top = 0;

        this.autoscale = config.autoscale || config.max === undefined;
        if(config.coverageColor) {
            this.color = config.coverageColor;
        }

        if (!this.autoscale) {
            this.dataRange = {
                min: config.min || 0,
                max: config.max
            };
        }

    }

    get height() {
        return this.parent.coverageTrackHeight
    }

    draw(options) {

        const pixelTop = options.pixelTop;
        pixelTop + options.pixelHeight;
        const nucleotideColors = this.parent.browser.nucleotideColors;

        if (pixelTop > this.height) {
            return //scrolled out of view
        }

        const ctx = options.context;
        const alignmentContainer = options.features;
        const coverageMap = alignmentContainer.coverageMap;

        let sequence;
        if (coverageMap.refSeq) {
            sequence = coverageMap.refSeq.toUpperCase();
        }

        const bpPerPixel = options.bpPerPixel;
        const bpStart = options.bpStart;
        const pixelWidth = options.pixelWidth;
        const bpEnd = bpStart + pixelWidth * bpPerPixel + 1;

        // paint for all coverage buckets
        // If alignment track color is != default, use it
        let color;
        if (this.color) {
            color = this.color;
        } else if (this.parent.color && typeof this.parent.color !== "function") {
            color = IGVColor.darkenLighten(this.parent.color, -35);
        } else {
            color = DEFAULT_COVERAGE_COLOR;
        }
        IGVGraphics.setProperties(ctx, {
            fillStyle: color,
            strokeStyle: color
        });
        const w =  Math.max(1, 1.0 / bpPerPixel);
        for (let i = 0, len = coverageMap.coverage.length; i < len; i++) {

            const bp = (coverageMap.bpStart + i);
            if (bp < bpStart) continue
            if (bp > bpEnd) break

            const item = coverageMap.coverage[i];
            if (!item) continue

            const h = (item.total / this.dataRange.max) * this.height;
            const y = this.height - h;
            const x = (bp - bpStart) / bpPerPixel;


            // IGVGraphics.setProperties(ctx, {fillStyle: "rgba(0, 200, 0, 0.25)", strokeStyle: "rgba(0, 200, 0, 0.25)" });
            IGVGraphics.fillRect(ctx, x, y, w, h);
        }

        // coverage mismatch coloring -- don't try to do this in above loop, color bar will be overwritten when w<1
        if (sequence) {
            for (let i = 0, len = coverageMap.coverage.length; i < len; i++) {

                const bp = (coverageMap.bpStart + i);
                if (bp < bpStart) continue
                if (bp > bpEnd) break

                const item = coverageMap.coverage[i];
                if (!item) continue

                const h = (item.total / this.dataRange.max) * this.height;
                let y = this.height - h;
                const x = Math.floor((bp - bpStart) / bpPerPixel);

                const refBase = sequence[i];

                if (this.parent.colorBy && this.parent.colorBy.startsWith("basemod")) {
                    drawModifications(ctx, x, this.height, w, h, bp, alignmentContainer, this.parent.colorBy, this.parent.baseModificationThreshold);

                } else if (item.isMismatch(refBase)) {
                    IGVGraphics.setProperties(ctx, {fillStyle: nucleotideColors[refBase]});
                    IGVGraphics.fillRect(ctx, x, y, w, h);

                    let accumulatedHeight = 0.0;
                    for (let nucleotide of ["A", "C", "T", "G"]) {

                        const count = item["pos" + nucleotide] + item["neg" + nucleotide];

                        // non-logoritmic
                        const hh = (count / this.dataRange.max) * this.height;
                        y = (this.height - hh) - accumulatedHeight;
                        accumulatedHeight += hh;
                        IGVGraphics.setProperties(ctx, {fillStyle: nucleotideColors[nucleotide]});
                        IGVGraphics.fillRect(ctx, x, y, w, hh);
                    }
                }
            }
        }
    }

    getClickedObject(clickState) {

        let features = clickState.viewport.cachedFeatures;
        if (!features || features.length === 0) return

        const genomicLocation = Math.floor(clickState.genomicLocation);
        const coverageMap = features.coverageMap;
        const coverageMapIndex = Math.floor(genomicLocation - coverageMap.bpStart);
        const coverage = coverageMap.coverage[coverageMapIndex];
        if(coverage) {
            return {
                coverage: coverage,
                baseModCounts: features.baseModCounts,
                hoverText: () => coverageMap.coverage[coverageMapIndex].hoverText()
            }
        }
    }

    popupData(clickState) {

        const nameValues = [];

        const {coverage, baseModCounts} = this.getClickedObject(clickState);
        if (coverage) {
            const genomicLocation = Math.floor(clickState.genomicLocation);
            const referenceFrame = clickState.viewport.referenceFrame;

            nameValues.push(referenceFrame.chr + ":" + numberFormatter$1(1 + genomicLocation));

            nameValues.push({name: 'Total Count', value: coverage.total});

            // A
            let tmp = coverage.posA + coverage.negA;
            if (tmp > 0) tmp = tmp.toString() + " (" + Math.round((tmp / coverage.total) * 100.0) + "%, " + coverage.posA + "+, " + coverage.negA + "- )";
            nameValues.push({name: 'A', value: tmp});

            // C
            tmp = coverage.posC + coverage.negC;
            if (tmp > 0) tmp = tmp.toString() + " (" + Math.round((tmp / coverage.total) * 100.0) + "%, " + coverage.posC + "+, " + coverage.negC + "- )";
            nameValues.push({name: 'C', value: tmp});

            // G
            tmp = coverage.posG + coverage.negG;
            if (tmp > 0) tmp = tmp.toString() + " (" + Math.round((tmp / coverage.total) * 100.0) + "%, " + coverage.posG + "+, " + coverage.negG + "- )";
            nameValues.push({name: 'G', value: tmp});

            // T
            tmp = coverage.posT + coverage.negT;
            if (tmp > 0) tmp = tmp.toString() + " (" + Math.round((tmp / coverage.total) * 100.0) + "%, " + coverage.posT + "+, " + coverage.negT + "- )";
            nameValues.push({name: 'T', value: tmp});

            // N
            tmp = coverage.posN + coverage.negN;
            if (tmp > 0) tmp = tmp.toString() + " (" + Math.round((tmp / coverage.total) * 100.0) + "%, " + coverage.posN + "+, " + coverage.negN + "- )";
            nameValues.push({name: 'N', value: tmp});

            nameValues.push('<HR/>');
            nameValues.push({name: 'DEL', value: coverage.del.toString()});
            nameValues.push({name: 'INS', value: coverage.ins.toString()});

            if(baseModCounts) {
                nameValues.push('<hr/>');
                nameValues.push(...baseModCounts.popupData(genomicLocation, this.parent.colorBy));

            }

        }

        return nameValues

    }

}

class BAMTrack extends TrackBase {

    static defaults = {
        alleleFreqThreshold: 0.2,
        visibilityWindow: 30000,
        showCoverage: true,
        showAlignments: true,
        height: 300,
        coverageTrackHeight: 50,
        baseModificationThreshold: 0
    }

    constructor(config, browser) {
        super(config, browser);
    }

    init(config) {

        this.type = "alignment";
        this.featureSource = new BamSource(config, this.browser);

        const coverageTrackConfig = Object.assign({parent: this}, config);
        this.coverageTrack = new CoverageTrack(coverageTrackConfig, this);

        const alignmentTrackConfig = Object.assign({parent: this}, config);
        this.alignmentTrack = new AlignmentTrack(alignmentTrackConfig, this.browser);

        super.init(config);

        if (!this.showAlignments) {
            this._height = this.coverageTrackHeight;
        }

        // The sort object can be an array in the case of multi-locus view, however if multiple sort positions
        // are present for a given reference frame the last one will take precedence
        if (config.sort) {
            if (Array.isArray(config.sort)) {
                // Legacy support
                this.assignSort(config.sort[0]);
            } else {
                this.assignSort(config.sort);
            }
        }
    }

    dispose() {
        this.alignmentTrack.dispose();
    }


    setHighlightedReads(highlightedReads, highlightColor) {
        this.alignmentTrack.setHighlightedReads(highlightedReads, highlightColor);
        this.updateViews();
    }

    get expectedPairOrientation() {
        return this.alignmentTrack.expectedPairOrientation
    }

    get viewAsPairs() {
        return this.alignmentTrack.viewAsPairs
    }

    get colorBy() {
        return this.alignmentTrack.colorBy
    }

    set height(h) {
        this._height = h;
        if (this.showAlignments) {
            this.alignmentTrack.height = this.showCoverage ? h - this.coverageTrackHeight : h;
        }
    }

    get height() {
        return this._height
    }

    sort(options) {
        options = this.assignSort(options);

        for (let vp of this.trackView.viewports) {
            if (vp.containsPosition(options.chr, options.position)) {
                const alignmentContainer = vp.cachedFeatures;
                if (alignmentContainer) {
                    alignmentContainer.sortRows(options);
                    vp.repaint();
                }
            }
        }
    }

    /**
     * Fix syntax problems for sort options.
     * @param options
     */
    assignSort(options) {
        // convert old syntax
        if (options.locus) {
            const range = parseLocusString$1(options.locus);
            options.chr = range.chr;
            options.position = range.start;
        } else {
            options.position--;
        }
        options.direction = options.direction === "ASC" || options.direction === true;

        // chr aliasing
        options.chr = this.browser.genome.getChromosomeName(options.chr);
        this.sortObject = options;

        return this.sortObject
    }

    async getFeatures(chr, bpStart, bpEnd, bpPerPixel, viewport) {

        const alignmentContainer = await this.featureSource.getAlignments(chr, bpStart, bpEnd);
        alignmentContainer.viewport = viewport;

        if (alignmentContainer.hasPairs && !this._pairedEndStats && !this.config.maxFragmentLength) {
            const pairedEndStats = new PairedEndStats(alignmentContainer.allAlignments(), this.config);
            if (pairedEndStats.totalCount > 99) {
                this._pairedEndStats = pairedEndStats;
            }
        }

        // Must pack before sorting
        alignmentContainer.pack(this.alignmentTrack);

        const sort = this.sortObject;
        if (sort) {
            if (sort.chr === chr && sort.position >= bpStart && sort.position <= bpEnd) {
                alignmentContainer.sortRows(sort);
            }
        }

        this.alignmentTrack.hasPairs = this.alignmentTrack.hasPairs || alignmentContainer.hasPairs;

        return alignmentContainer
    }


    /**
     * Compute the pixel height required to display all content.  This is not the same as the viewport height
     * (track.height) which might include a scrollbar.
     *
     * @param alignmentContainer
     * @returns {number}
     */
    computePixelHeight(alignmentContainer) {
        return (this.showCoverage ? this.coverageTrackHeight : 0) +
            (this.showAlignments ? this.alignmentTrack.computePixelHeight(alignmentContainer) : 0)
    }

    draw(options) {

        IGVGraphics.fillRect(options.context, 0, options.pixelTop, options.pixelWidth, options.pixelHeight, {'fillStyle': "rgb(255, 255, 255)"});

        if (true === this.showCoverage && this.coverageTrackHeight > 0) {
            this.trackView.axisCanvas.style.display = 'block';
            this.coverageTrack.draw(options);
        } else {
            this.trackView.axisCanvas.style.display = 'none';
        }

        if (true === this.showAlignments) {
            this.alignmentTrack.setTop(this.coverageTrack, this.showCoverage);
            this.alignmentTrack.draw(options);
        }
    }

    paintAxis(ctx, pixelWidth, pixelHeight) {

        this.coverageTrack.paintAxis(ctx, pixelWidth, this.coverageTrackHeight);

        // if (this.browser.isMultiLocusMode()) {
        //     ctx.clearRect(0, 0, pixelWidth, pixelHeight);
        // } else {
        //     this.coverageTrack.paintAxis(ctx, pixelWidth, this.coverageTrackHeight);
        // }
    }

    contextMenuItemList(config) {
        return this.alignmentTrack.contextMenuItemList(config)
    }

    popupData(clickState) {
        if (true === this.showCoverage && clickState.y >= this.coverageTrack.top && clickState.y < this.coverageTrackHeight) {
            return this.coverageTrack.popupData(clickState)
        } else {
            return this.alignmentTrack.popupData(clickState)
        }
    }

    /**
     * Return the features (alignment, coverage, downsampled interval) clicked on.  Needed for "onclick" event.
     * @param clickState
     * @param features
     */
    clickedFeatures(clickState) {

        let clickedObject;
        if (true === this.showCoverage && clickState.y >= this.coverageTrack.top && clickState.y < this.coverageTrackHeight) {
            clickedObject = this.coverageTrack.getClickedObject(clickState);
        } else {
            clickedObject = this.alignmentTrack.getClickedObject(clickState);
        }
        return clickedObject ? [clickedObject] : undefined
    }

    hoverText(clickState) {
        if (true === this.showCoverage && clickState.y >= this.coverageTrack.top && clickState.y < this.coverageTrackHeight) {
            const clickedObject = this.coverageTrack.getClickedObject(clickState);
            if (clickedObject) {
                return clickedObject.hoverText()
            }
        }

    }

    menuItemList() {

        // Start with overage track items
        let menuItems = [];

        menuItems = menuItems.concat(this.numericDataMenuItems());

        menuItems = menuItems.concat(this.alignmentTrack.menuItemList());

        // Show coverage / alignment options
        const adjustTrackHeight = () => {
            if (!this.autoHeight) {
                const h =
                    (this.showCoverage ? this.coverageTrackHeight : 0) +
                    (this.showAlignments ? this.alignmentTrack.height : 0);
                this.trackView.setTrackHeight(h);
            }
        };

        menuItems.push('<hr/>');

        function showCoverageHandler() {
            this.showCoverage = !this.showCoverage;
            adjustTrackHeight();
            this.trackView.checkContentHeight();
            this.trackView.repaintViews();
        }

        menuItems.push({
            element: createCheckbox("Show Coverage", this.showCoverage),
            click: showCoverageHandler
        });

        function showAlignmentHandler() {
            this.showAlignments = !this.showAlignments;
            adjustTrackHeight();
            this.trackView.checkContentHeight();
            this.trackView.repaintViews();
        }

        menuItems.push({
            element: createCheckbox("Show Alignments", this.showAlignments),
            click: showAlignmentHandler
        });


        return menuItems
    }


    /**
     * Return the current state of the track.  Used to create sessions and bookmarks.
     *
     * @returns {*|{}}
     */
    getState() {

        const config = super.getState();

        // Shared state
        if (this.sortObject) {
            config.sort = {
                chr: this.sortObject.chr,
                position: this.sortObject.position + 1,
                option: this.sortObject.option,
                direction: this.sortObject.direction ? "ASC" : "DESC"
            };
        }

        // Alignment track
        Object.assign(config, this.alignmentTrack.getState());

        return config
    }

    getCachedAlignmentContainers() {
        return this.trackView.viewports.map(vp => vp.cachedFeatures)
    }

    get dataRange() {
        return this.coverageTrack.dataRange
    }

    set dataRange(dataRange) {
        this.coverageTrack.dataRange = dataRange;
    }

    get logScale() {
        return this.coverageTrack.logScale
    }

    set logScale(logScale) {
        this.coverageTrack.logScale = logScale;
    }

    get autoscale() {
        return this.coverageTrack.autoscale
    }

    set autoscale(autoscale) {
        this.coverageTrack.autoscale = autoscale;
    }

    /**
     * Add chords to the circular view for the given viewport, represented by its reference frame
     * @param refFrame
     */
    addPairedChordsForViewport(viewport) {

        const maxTemplateLength = this.maxTemplateLength;
        const inView = [];
        const refFrame = viewport.referenceFrame;
        for (let a of viewport.cachedFeatures.allAlignments()) {
            if (a.end >= refFrame.start
                && a.start <= refFrame.end) {
                if (a.paired) {
                    if (a.end - a.start > maxTemplateLength) {
                        inView.push(a);
                    }
                } else {
                    if (a.mate
                        && a.mate.chr
                        && (a.mate.chr !== a.chr || Math.max(a.fragmentLength) > maxTemplateLength)) {
                        inView.push(a);
                    }
                }
            }
        }
        const chords = makePairedAlignmentChords(inView);
        sendChords(chords, this, refFrame, 0.02);
    }

    addSplitChordsForViewport(viewport) {

        const inView = [];
        const refFrame = viewport.referenceFrame;
        for (let a of viewport.cachedFeatures.allAlignments()) {

            const sa = a.hasTag('SA');
            if (a.end >= refFrame.start && a.start <= refFrame.end && sa) {
                inView.push(a);
            }
        }

        const chords = makeSupplementalAlignmentChords(inView);
        sendChords(chords, this, refFrame, 0.02);
    }

}

function getArcType(config) {
    if (!config.arcType) {
        return "nested"
    }
    switch (config.arcType) {
        case "chiapet":
            return "inView"
        case "chiapetoutbound":
            return "partialInView"
        default:
            return config.arcType
    }
}

const DEFAULT_ARC_COLOR = "rgb(180,25,137)";

class InteractionTrack extends TrackBase {

    static defaults = {
        height: 250,
        theta: Math.PI / 4,
        arcOrientation: "UP",
        showBlocks: true,
        blockHeight: 3,
        thickness: 1,
        alpha: 0.02,
        logScale: true,
        colorBy: undefined
    }

    constructor(config, browser) {
        super(config, browser);
    }

    init(config) {

        super.init(config);

        // Backward compatibility hack, arcOrientation was previously a boolean, now a string
        if (config.arcOrientation === false) {
            this.arcOrientation = "DOWN";
        } else if (config.arcOrientation === true) {
            this.arcOrientation = "UP";
        } else if (config.arcOrientation) {
            this.arcOrientation = config.arcOrientation.toUpperCase();
        } else {
            this.arcOrientation = "UP";
        }

        this.sinTheta = Math.sin(this.theta);
        this.cosTheta = Math.cos(this.theta);
        this.arcType = getArcType(config);   // nested | proportional | inView | partialInView
        this.painter = {flipAxis: "DOWN" === this.arcOrientation, dataRange: this.dataRange, paintAxis: paintAxis};

        if (config.valueColumn) {
            this.valueColumn = config.valueColumn;
            this.hasValue = true;
        } else if (config.useScore) {
            this.hasValue = true;
            this.valueColumn = "score";
        }

        if (config.max) {
            this.dataRange = {
                min: config.min || 0,
                max: config.max
            };
            this.autoscale = false;
        } else {
            this.autoscale = true;
        }

        if (config.colorTable) {
            this.colorTable = new ColorTable(config.colorTable);
        } else if (config.colorBy) {
            this.colorTable = new PaletteColorTable("Set1");
        }

        // Create the FeatureSource and override the default whole genome method
        if (config.featureSource) {
            this.featureSource = config.featureSource;
            delete config._featureSource;
        } else {
            if (config.features) {
                fixFeatures(config.features);
            }
            this.featureSource = FeatureSource(config, this.browser.genome);
            this.featureSource.getWGFeatures = getWGFeatures;
        }
    }

    async postInit() {

        if (typeof this.featureSource.getHeader === "function") {
            this.header = await this.featureSource.getHeader();
            if (this.disposed) return   // This track was removed during async load
        }

        // Set properties from track line
        if (this.header) {
            this.setTrackProperties(this.header);
        }

        if (this.visibilityWindow === undefined && typeof this.featureSource.defaultVisibilityWindow === 'function') {
            this.visibilityWindow = await this.featureSource.defaultVisibilityWindow();
            this.featureSource.visibilityWindow = this.visibilityWindow;  // <- this looks odd
        }

        this._initialColor = this.color || this.constructor.defaultColor;
        this._initialAltColor = this.altColor || this.constructor.defaultColor;

        return this
    }

    get supportsWholeGenome() {
        return typeof this.featureSource.supportsWholeGenome === 'function' ? this.featureSource.supportsWholeGenome() : true
    }

    async getFeatures(chr, start, end) {
        const visibilityWindow = this.visibilityWindow;
        const features = await this.featureSource.getFeatures({chr, start, end, visibilityWindow});

        // Check for score or value
        if (this.hasValue === undefined && features && features.length > 0) {
            this.hasValue = features[0].score !== undefined;
        }

        return features
    }

    draw(options) {

        if (this.arcType === "proportional") {
            this.drawProportional(options);
        } else if (this.arcType === "inView" || this.arcType === "partialInView") {
            this.drawProportional(options);
        } else {
            this.drawNested(options);
        }
    }

    drawNested(options) {

        const ctx = options.context;
        const pixelWidth = options.pixelWidth;
        const pixelHeight = options.pixelHeight;
        const viewportWidth = options.viewportWidth;
        const bpPerPixel = options.bpPerPixel;
        const bpStart = options.bpStart;
        const xScale = bpPerPixel;

        IGVGraphics.fillRect(ctx, 0, options.pixelTop, pixelWidth, pixelHeight, {'fillStyle': "rgb(255, 255, 255)"});

        const featureList = options.features;

        if (featureList) {

            // Autoscale theta
            autoscaleNested.call(this);
            const direction = "UP" === this.arcOrientation;
            const y = direction ? options.pixelHeight : 0;

            ctx.font = "8px sans-serif";
            ctx.textAlign = "center";

            for (let feature of featureList) {

                // Reset transient property drawState.  An undefined value => feature has not been drawn.
                feature.drawState = undefined;
                let color = this.getColor(feature);
                ctx.fillStyle = color;
                ctx.strokeStyle = color;

                ctx.lineWidth = feature.thickness || this.thickness || 1;

                if (feature.chr1 === feature.chr2 || feature.chr === 'all') {

                    const {m1, m2} = getMidpoints(feature, this.browser.genome);

                    let pixelStart = Math.round((m1 - bpStart) / xScale);
                    let pixelEnd = Math.round((m2 - bpStart) / xScale);
                    if (pixelEnd < 0 || pixelStart > pixelWidth) continue

                    let w = (pixelEnd - pixelStart);
                    if (w < 3) {
                        w = 3;
                        pixelStart--;
                    }

                    const a = w / 2;
                    const r = a / this.sinTheta;
                    const b = this.cosTheta * r;
                    const xc = pixelStart + a;

                    let yc, startAngle, endAngle;
                    if (direction) { // UP
                        yc = this.height + b;
                        startAngle = Math.PI + Math.PI / 2 - this.theta;
                        endAngle = Math.PI + Math.PI / 2 + this.theta;
                    } else { // DOWN
                        yc = -b;
                        startAngle = Math.PI / 2 - this.theta;
                        endAngle = Math.PI / 2 + this.theta;
                    }

                    if (this.showBlocks && feature.chr !== 'all') {
                        const s1 = (feature.start1 - bpStart) / xScale;
                        const e1 = (feature.end1 - bpStart) / xScale;
                        const s2 = (feature.start2 - bpStart) / xScale;
                        const e2 = (feature.end2 - bpStart) / xScale;
                        const hb = direction ? -this.blockHeight : this.blockHeight;
                        ctx.fillRect(s1, y, e1 - s1, hb);
                        ctx.fillRect(s2, y, e2 - s2, hb);
                    }

                    // Alpha shade (de-emphasize) arcs that extend beyond viewport, unless alpha shading is used for score.
                    if (color && !this.config.useScore && w > viewportWidth) {
                        color = getAlphaColor(color, this.alpha);
                        ctx.strokeStyle = color;
                        ctx.fillStyle = color;
                    }
                    ctx.beginPath();
                    ctx.arc(xc, yc, r, startAngle, endAngle, false);
                    ctx.stroke();
                    feature.drawState = {xc, yc, r};
                } else {

                    let pixelStart = Math.round((feature.start - bpStart) / xScale);
                    let pixelEnd = Math.round((feature.end - bpStart) / xScale);
                    if (pixelEnd < 0 || pixelStart > pixelWidth) continue

                    let w = (pixelEnd - pixelStart);
                    if (w < 3) {
                        w = 3;
                        pixelStart--;
                    }
                    const otherChr = feature.chr === feature.chr1 ? feature.chr2 : feature.chr1;
                    ctx.strokeStyle = color;
                    // get a sense of trans "spread"
                    ctx.fillStyle = getAlphaColor(getChrColor$1(otherChr), 0.5);
                    // ctx.fillStyle = color

                    if (direction) {
                        // UP
                        ctx.fillRect(pixelStart, this.height / 2, w, this.height / 2);
                        ctx.fillText(otherChr, pixelStart + w / 2, this.height / 2 - 5);
                        feature.drawState = {x: pixelStart, y: this.height / 2, w: w, h: this.height / 2};
                    } else {
                        ctx.fillRect(pixelStart, 0, w, this.height / 2);
                        ctx.fillText(otherChr, pixelStart + w / 2, this.height / 2 + 13);
                        feature.drawState = {x: pixelStart, y: 0, w: w, h: this.height / 2};
                    }
                }
            }
        }

        function autoscaleNested() {
            let max = 0;
            for (let feature of featureList) {
                let pixelStart = (feature.start - bpStart) / xScale;
                let pixelEnd = (feature.end - bpStart) / xScale;
                if (pixelStart >= 0 && pixelEnd <= pixelWidth) {
                    max = Math.max(max, pixelEnd - pixelStart);
                }
            }
            let a = Math.min(viewportWidth, max) / 2;
            if (max > 0) {
                let coa = (pixelHeight - 10) / a;
                this.theta = estimateTheta(coa);
                this.sinTheta = Math.sin(this.theta);
                this.cosTheta = Math.cos(this.theta);
            }
        }
    }

    drawProportional(options) {

        const ctx = options.context;
        const pixelWidth = options.pixelWidth;
        const pixelHeight = options.pixelHeight;
        const bpPerPixel = options.bpPerPixel;
        const bpStart = options.bpStart;
        const xScale = bpPerPixel;
        const refStart = options.referenceFrame.start;
        const refEnd = options.referenceFrame.end;
        const direction = "UP" === this.arcOrientation;

        IGVGraphics.fillRect(ctx, 0, options.pixelTop, pixelWidth, pixelHeight, {'fillStyle': "rgb(255, 255, 255)"});

        const featureList = options.features;

        if (featureList && featureList.length > 0) {

            // we use the min as a filter but not moving the axis
            const effectiveMin = 0;
            const yScale = this.getScaleFactor(effectiveMin, this.dataRange.max, options.pixelHeight - 1, this.logScale);
            const y = direction ? options.pixelHeight : 0;

            for (let feature of featureList) {

                // Reset transient property drawState.  An undefined value => feature has not been drawn.
                feature.drawState = undefined;

                const value = this.valueColumn ? feature[this.valueColumn] : feature.score;
                if (value === undefined || Number.isNaN(value)) continue

                const radiusY = Math.round((this.logScale ? Math.log10(value + 1) : value) * yScale);

                if (feature.chr1 === feature.chr2 || feature.chr === 'all') {

                    const {m1, m2} = getMidpoints(feature, this.browser.genome);

                    let pixelStart = Math.round((m1 - bpStart) / xScale);
                    let pixelEnd = Math.round((m2 - bpStart) / xScale);
                    let w = (pixelEnd - pixelStart);
                    if (w < 3) {
                        w = 3;
                        pixelStart--;
                    }

                    // Various filters
                    if (value < this.dataRange.min || value > this.dataRange.max) continue
                    if ("proportional" !== this.arcType) {
                        const showOutbound = (this.arcType === "partialInView");
                        const within = (m1 >= refStart && m2 <= refEnd);
                        let outBound = false;
                        let inBound = false;
                        if (!within && showOutbound) {
                            outBound = (refStart <= m1 && m1 <= refEnd);
                            if (!outBound) inBound = (refStart <= m2 && m2 <= refEnd);
                        }
                        if (!(within || outBound || inBound)) continue
                    }


                    const radiusX = w / 2;
                    const xc = pixelStart + w / 2;
                    feature.drawState = {xc, yc: y, radiusX, radiusY};

                    const counterClockwise = direction;

                    const color = this.getColor(feature);
                    ctx.strokeStyle = color;
                    ctx.lineWidth = feature.thickness || this.thickness || 1;

                    if (true === ctx.isSVG) {
                        ctx.strokeEllipse(xc, y, radiusX, radiusY, 0, 0, Math.PI, counterClockwise);
                    } else {
                        ctx.beginPath();
                        ctx.ellipse(xc, y, radiusX, radiusY, 0, 0, Math.PI, counterClockwise);
                        ctx.stroke();
                    }

                    if (this.alpha) {
                        const scoreFactor = this.config.useScore && Number.isFinite(feature.score) ?
                            Math.max(0.1, Math.round(scoreShade(feature.score) * 10) / 10) :
                            1;
                        ctx.fillStyle = getAlphaColor(color, scoreFactor * this.alpha);
                        if (true === ctx.isSVG) {
                            ctx.fillEllipse(xc, y, radiusX, radiusY, 0, 0, Math.PI, counterClockwise);
                        } else {
                            ctx.fill();
                        }
                    }

                    if (this.showBlocks && feature.chr !== 'all') {
                        ctx.fillStyle = color;
                        const s1 = (feature.start1 - bpStart) / xScale;
                        const e1 = (feature.end1 - bpStart) / xScale;
                        const s2 = (feature.start2 - bpStart) / xScale;
                        const e2 = (feature.end2 - bpStart) / xScale;
                        const hb = direction ? -this.blockHeight : this.blockHeight;
                        ctx.fillRect(s1, y, e1 - s1, hb);
                        ctx.fillRect(s2, y, e2 - s2, hb);
                    }

                } else {
                    // Inter chromosome
                    let pixelStart = Math.round((feature.start - bpStart) / xScale);
                    let pixelEnd = Math.round((feature.end - bpStart) / xScale);
                    if (pixelEnd < 0 || pixelStart > pixelWidth || value < this.dataRange.min || value > this.dataRange.max) continue

                    const h = Math.min(radiusY, this.height - 13);   // Leave room for text
                    let w = (pixelEnd - pixelStart);
                    if (w < 3) {
                        w = 3;
                        pixelStart--;
                    }
                    const otherChr = feature.chr === feature.chr1 ? feature.chr2 : feature.chr1;
                    ctx.font = "8px sans-serif";
                    ctx.textAlign = "center";
                    // get a sense of trans "spread"
                    ctx.fillStyle = getAlphaColor(getChrColor$1(otherChr), 0.5);
                    if (direction) {
                        // UP
                        const y = this.height - h;
                        ctx.fillRect(pixelStart, y, w, h);
                        ctx.fillText(otherChr, pixelStart + w / 2, y - 5);
                        feature.drawState = {x: pixelStart, y, w, h};
                    } else {
                        ctx.fillRect(pixelStart, 0, w, h);
                        ctx.fillText(otherChr, pixelStart + w / 2, h + 13);
                        feature.drawState = {x: pixelStart, y: 0, w, h};
                    }
                }
            }
        }
    }

    getColor(feature) {
        let color;
        if (this.colorBy) {
            const value = feature.getAttributeValue ?
                feature.getAttributeValue(this.colorBy) :
                feature[this.colorBy];
            color = this.colorTable.getColor(value);
        } else if (typeof this.color === 'function') {
            color = this.color(feature);
        } else {
            color = this.color || feature.color || DEFAULT_ARC_COLOR;
        }
        if (this.config.useScore && Number.isFinite(feature.score)) {
            color = getAlphaColor(color, scoreShade(feature.score));
        }
        return color
    }

    getScaleFactor(min, max, height, logScale) {
        const scale = logScale ? height / (Math.log10(max + 1) - (min <= 0 ? 0 : Math.log10(min + 1))) : height / (max - min);
        return scale
    }


    clearAxis(ctx, pixelWidth, pixelHeight) {
        IGVGraphics.fillRect(ctx, 0, 0, pixelWidth, pixelHeight, {'fillStyle': "rgb(255, 255, 255)"});
    }

    paintAxis(ctx, pixelWidth, pixelHeight) {
        // dataRane is interpreted differently for interactino tracks -- all arcs are drawn from "zero", irrespective of dataRange.min
        const axisRange = {min: 0, max: this.dataRange.max};
        if (this.arcType === "proportional") {
            this.painter.flipAxis = "DOWN" === this.arcOrientation;
            this.painter.dataRange = axisRange;
            this.painter.paintAxis(ctx, pixelWidth, pixelHeight);
        } else if (this.arcType === "inView" || this.arcType === "partialInView") {
            this.painter.flipAxis = "DOWN" === this.arcOrientation;
            this.painter.dataRange = axisRange;
            this.painter.paintAxis(ctx, pixelWidth, pixelHeight);
        } else {
            this.clearAxis(ctx, pixelWidth, pixelHeight);
        }
    }

    menuItemList() {

        let items = [];

        if (this.hasValue) {
            items.push("<hr/>");
            const lut =
                {
                    "nested": "Nested",
                    "proportional": "Proportional - All",
                    "inView": "Proportional - Both Ends in View",
                    "partialInView": "Proportional - One End in View"
                };
            items.push("<b>Arc Type</b>");
            for (let arcType of ["nested", "proportional", "inView", "partialInView"]) {
                items.push(
                    {
                        element: createCheckbox(lut[arcType], arcType === this.arcType),
                        click: function arcTypeHandler() {
                            this.arcType = arcType;
                            this.trackView.repaintViews();
                        }
                    });
            }
        }
        items.push("<hr/>");

        items.push({
            name: "Toggle arc direction",
            click: function toggleArcDirectionHandler() {
                this.arcOrientation = "UP" === this.arcOrientation ? "DOWN" : "UP";
                this.trackView.repaintViews();
            }
        });
        items.push({
            name: this.showBlocks ? "Hide Blocks" : "Show Blocks",
            click: function blockVisibiltyHandler() {
                this.showBlocks = !this.showBlocks;
                this.trackView.repaintViews();
            }
        });

        items.push("<hr/>");
        items.push({
            name: 'Set line thickness',
            click: function setLineThicknessHandler(ev) {
                const inputDialog = this.browser.inputDialog;
                inputDialog.present({
                    label: "Enter line thickness",
                    value: this.thickness || 1,
                    callback: value => {
                        const newThickness = parseFloat(value);
                        if (isNaN(newThickness)) {
                            window.alert("Invalid line thickness: " + value);
                        } else {
                            this.thickness = newThickness;
                            this.trackView.repaintViews();
                        }
                    }
                }, ev);
            }
        });

        items.push({
            name: 'Set alpha',
            click: function (ev) {
                const inputDialog = this.browser.inputDialog;
                inputDialog.present({
                    label: "Enter alpha transparency (0-1)",
                    value: this.alpha,
                    callback: value => {
                        const newAlpha = parseFloat(value);
                        if (isNaN(newAlpha) || newAlpha < 0 || newAlpha > 1) {
                            window.alert("Invalid alpha: " + value);
                        } else {
                            this.alpha = newAlpha;
                            this.trackView.repaintViews();
                        }
                    }
                }, ev);
            }
        });

        if (this.hasValue) {
            items.push({
                element: createCheckbox("Use score", this.config.useScore), click: () => {
                    this.config.useScore = !this.config.useScore;
                    this.valueColumn = "score";
                    this.trackView.repaintViews();
                }
            });
        }


        if (this.arcType === "proportional" || this.arcType === "inView" || this.arcType === "partialInView") {
            items = items.concat(this.numericDataMenuItems());
        }

        if (this.browser.circularView) {
            items.push('<hr/>');
            items.push({
                label: 'Add interactions to circular view',
                click: function addInteractionsHandler() {
                    for (let viewport of this.trackView.viewports) {
                        this.addChordsForViewport(viewport.referenceFrame);
                    }
                }
            });
        }

        return items
    }

    contextMenuItemList(clickState) {

        // Experimental JBrowse feature
        if (this.browser.circularView) {
            const viewport = clickState.viewport;
            const list = [];

            list.push({
                label: 'Add interactions to circular view',
                click: () => {
                    const refFrame = viewport.referenceFrame;
                    // first pass: to get all the relevant features
                    this.addChordsForViewport(refFrame);
                }
            });

            list.push('<hr/>');
            return list
        }
    }

    /**
     * Add chords to the circular view for the given viewport, represented by its reference frame
     * @param refFrame
     */
    addChordsForViewport(refFrame) {

        let inView;
        if ("all" === refFrame.chr) {
            inView = Object.values(this.featureSource.getAllFeatures()).flat();
        } else {
            const cachedFeatures =
                this.featureSource.featureCache.queryFeatures(refFrame.chr, refFrame.start, refFrame.end);
            // inView features are simply features that have been drawn, i.e. have a drawState
            inView = cachedFeatures.filter(f => f.drawState);
        }

        if (inView.length === 0) return

        const chords = makeBedPEChords(inView);
        sendChords(chords, this, refFrame, 0.5);
        //
        //
        // // for filtered set, distinguishing the chromosomes is more critical than tracks
        // const chordSetColor = IGVColor.addAlpha("all" === refFrame.chr ? this.color : getChrColor(refFrame.chr), 0.5)
        // const trackColor = IGVColor.addAlpha(this.color, 0.5)
        //
        // // name the chord set to include locus and filtering information
        // const encodedName = this.name.replaceAll(' ', '%20')
        // const chordSetName = "all" === refFrame.chr ?
        //     encodedName :
        //     `${encodedName} (${refFrame.chr}:${refFrame.start}-${refFrame.end} ; range:${this.dataRange.min}-${this.dataRange.max})`
        // this.browser.circularView.addChords(chords, {track: chordSetName, color: chordSetColor, trackColor: trackColor})
    }

    doAutoscale(features) {

        // if ("proportional" === this.arcType) {
        let max = 0;
        if (features) {
            for (let f of features) {
                const v = this.valueColumn ? f[this.valueColumn] : f.score;
                if (!Number.isNaN(v)) {
                    max = Math.max(max, v);
                }
            }
        }
        return {min: 0, max: max}
        // }
    }

    popupData(clickState, features) {

        if (features === undefined) features = this.clickedFeatures(clickState);

        const data = [];
        for (let feature of features) {

            const f = feature._ || feature;   // For "whole genome" features, which keeps a pointer to the original

            data.push({name: "Region 1", value: positionString(f.chr1, f.start1, f.end1, f.strand1)});
            data.push({name: "Region 2", value: positionString(f.chr2, f.start2, f.end2, f.strand2)});
            if (f.name) {
                data.push({name: "Name", value: f.name});
            }
            if (f.value !== undefined) {
                data.push({name: "Value", value: f.value});
            }
            if (f.score !== undefined) {
                data.push({name: "Score", value: f.score});
            }
            if (f.type !== undefined) {
                data.push({name: "Type", value: f.type});
            }


            if (f.extras && this.header && this.header.columnNames) {
                const columnNames = this.header.columnNames;
                const stdColumns = this.header.hiccups ? 6 : 10;
                for (let i = stdColumns; i < columnNames.length; i++) {
                    if (this.header.colorColumn === i) continue
                    if (columnNames[i] === 'info') {
                        extractInfoColumn(data, f.extras[i - stdColumns]);
                    } else {
                        data.push({name: columnNames[i], value: f.extras[i - stdColumns]});
                    }
                }
            }
            // For now just return the top hit
            break

            //if (data.length > 0) {
            //     data.push("<HR>");
            // }
        }
        return data
    }

    clickedFeatures(clickState) {

        // We use the cached features rather than method to avoid async load.  If the
        // feature is not already loaded this won't work,  but the user wouldn't be mousing over it either.
        const featureList = clickState.viewport.cachedFeatures;
        const candidates = [];
        if (featureList) {
            const proportional = (this.arcType === "proportional" || this.arcType === "inView" || this.arcType === "partialInView");

            for (let feature of featureList) {

                if (!feature.drawState) continue

                if (feature.chr1 === feature.chr2 || feature.chr === 'all') {
                    if (proportional) {
                        //(x-xc)^2/radiusX^2 + (y-yc)^2/radiusY^2 <= 1
                        const {xc, yc, radiusX, radiusY} = feature.drawState;
                        const dx = clickState.canvasX - xc;
                        const dy = clickState.canvasY - yc;
                        const score = (dx / radiusX) * (dx / radiusX) + (dy / radiusY) * (dy / radiusY);
                        if (score <= 1) {
                            candidates.push({score: 1 / score, feature});
                        }
                    } else {
                        const {xc, yc, r} = feature.drawState;
                        const dx = clickState.canvasX - xc;
                        const dy = clickState.canvasY - yc;
                        const score = Math.abs(Math.sqrt(dx * dx + dy * dy) - r);
                        if (score < 5) {
                            candidates.push({score, feature});
                        }
                    }
                } else {
                    const {x, y, w, h} = feature.drawState;
                    const tolerance = 5;
                    if (clickState.canvasX >= x - tolerance && clickState.canvasX <= x + w + tolerance &&
                        clickState.canvasY >= y && clickState.canvasY <= y + h) {
                        const score = -Math.abs(clickState.canvasX - (x + w / 2));
                        candidates.push({score, feature});
                        break
                    }
                }
            }
        }

        if (candidates.length > 1) {
            candidates.sort((a, b) => a.score - b.score);
        }
        return candidates.map((c) => c.feature)
    }
}

function getMidpoints(feature, genome) {
    let m1 = (feature.start1 + feature.end1) / 2;
    let m2 = (feature.start2 + feature.end2) / 2;
    if (feature.chr === 'all') {
        m1 = genome.getGenomeCoordinate(feature.chr1, m1);
        m2 = genome.getGenomeCoordinate(feature.chr2, m2);
    }
    if (m1 > m2) {
        const tmp = m1;
        m1 = m2;
        m2 = tmp;
    }
    return {m1, m2}
}

function positionString(chr, start, end, strand) {

    return strand && strand !== '.' ?
        `${chr}:${numberFormatter$1(start + 1)}-${numberFormatter$1(end)} (${strand})` :
        `${chr}:${numberFormatter$1(start + 1)}-${numberFormatter$1(end)}`
}

/**
 * Estimate theta given the ratio of track height to 1/2 the feature width (coa).  This relationship is approximately linear.
 */
function estimateTheta(x) {
    let coa = [0.01570925532366355, 0.15838444032453644, 0.3249196962329063, 0.5095254494944288, 0.7265425280053609, 0.9999999999999999];
    let theta = [0.031415926535897934, 0.3141592653589793, 0.6283185307179586, 0.9424777960769379, 1.2566370614359172, 1.5707963267948966];
    let idx;

    for (idx = 0; idx < coa.length; idx++) {
        if (coa[idx] > x) {
            break
        }
    }

    let left = idx === 0 ? 0 : coa[idx - 1];
    let right = idx < coa.length ? coa[idx] : 1;
    let r = (x - left) / (right - left);

    let thetaLeft = idx === 0 ? 0 : theta[idx - 1];
    let thetaRight = idx < theta.length ? theta[idx] : Math.PI / 2;

    return Math.min(Math.PI / 2, (thetaLeft + r * (thetaRight - thetaLeft)))

}

const colorAlphaCache = new Map();

function getAlphaColor(color, alpha) {


    const key = `${color}_${alpha}`;
    let c = colorAlphaCache.get(key);
    if (!c) {
        c = IGVColor.addAlpha(color, alpha);
        colorAlphaCache.set(key, c);
    }
    return c
}


/**
 * Called in the context of FeatureSource  (i.e. this == the feature source (a TextFeatureSource) for the track
 *
 * @param allFeatures
 * @returns {[]}
 */
function getWGFeatures(allFeatures) {

    const makeWGFeature = (f) => {
        const wg = Object.assign({}, f);
        wg.chr = "all";
        wg.start = genome.getGenomeCoordinate(f.chr1, f.start1);
        wg.end = genome.getGenomeCoordinate(f.chr2, f.end2);
        return wg
    };

    const genome = this.genome;

    // First pass -- find the max score feature
    let maxScoreFeature;
    let totalFeatureCount = 0;
    for (let c of genome.wgChromosomeNames) {
        let chrFeatures = allFeatures[c];
        if (chrFeatures) {
            for (let f of chrFeatures) {
                if (!f.dup) {
                    totalFeatureCount++;
                    if (f.score && (!maxScoreFeature || f.score > maxScoreFeature.score)) {
                        maxScoreFeature = f;
                    }
                }
            }
        }
    }

    const maxCount = this.maxWGCount;
    const nBins = maxScoreFeature && maxScoreFeature.score > 0 && totalFeatureCount > maxCount ? 5 : 1;   // TODO make a function of total # of features & maxCount?
    const featuresPerBin = Math.floor(maxCount / nBins);
    const binSize = maxScoreFeature && maxScoreFeature.score > 0 ? Math.log(maxScoreFeature.score) / nBins : Number.MAX_SAFE_INTEGER;

    let binnedFeatures = [];
    let counts = [];
    for (let i = 0; i < nBins; i++) {
        counts.push([0]);
        binnedFeatures.push([]);
    }

    for (let c of genome.wgChromosomeNames) {
        let chrFeatures = allFeatures[c];
        if (chrFeatures) {
            for (let f of chrFeatures) {
                if (!f.dup) {
                    const bin = f.score ? Math.max(0, Math.min(nBins - 1, Math.floor(Math.log(f.score) / binSize))) : 0;
                    if (binnedFeatures[bin].length < featuresPerBin) {
                        binnedFeatures[bin].push(makeWGFeature(f));
                    } else {
                        //Reservoir sampling
                        const samplingProb = featuresPerBin / (counts[bin] + 1);
                        if (Math.random() < samplingProb) {
                            const idx = Math.floor(Math.random() * (featuresPerBin - 1));
                            binnedFeatures[bin][idx] = makeWGFeature(f);
                        }
                    }
                    counts[bin]++;
                }
            }
        }
    }

    let wgFeatures;
    if (nBins === 1) {
        wgFeatures = binnedFeatures[0];
    } else {
        wgFeatures = [];
        for (let bf of binnedFeatures) {
            for (let f of bf) wgFeatures.push(f);
        }
        // Keep the feature with max score
        if (maxScoreFeature) {
            wgFeatures.push(makeWGFeature(maxScoreFeature));
        }
        wgFeatures.sort(function (a, b) {
            return a.start - b.start
        });
    }


    return wgFeatures
}

/**
 * Extract a gff style info column for popup text.  This convention used by 10X for bedpe files
 *     ALLELIC_FRAC=0.0375670840787;BLACK1=.;BLACK2=.;...
 * @param data
 * @param str
 */
function extractInfoColumn(data, str) {
    const kvs = str.split(';');
    for (let t of kvs) {
        const kv = t.split('=');
        if (kv.length === 2) {
            data.push({name: kv[0], value: kv[1]});
        }
    }

}

/**
 * Set the total chr extent -- provided for embedded features.
 *
 * @param features
 */
function fixFeatures(features) {
    const interChrDups = [];
    for (let feature of features) {
        if (feature.chr1 === feature.chr2) {
            feature.chr = feature.chr1;
            feature.start = Math.min(feature.start1, feature.start2);
            feature.end = Math.max(feature.end1, feature.end2);
        } else {
            feature.chr = feature.chr1;
            feature.start = Math.min(feature.start1, feature.start2);
            feature.end = Math.max(feature.end1, feature.end2);

            // Make copies of inter-chr features, one for each chromosome
            const f2 = Object.assign({}, feature);
            f2.dup = true;
            f2.chr = f2.chr2;
            f2.start = f2.start2;
            f2.end = f2.end2;
            interChrDups.push(f2);
        }
    }

    // Add inter-chr dups to the original features
    for (const dup of interChrDups) {
        features.push(dup);
    }
}

class RemoteFile {

    constructor(args) {
        this.config = args;
        this.url = mapUrl$2(args.path || args.url);
    }


    async read(position, length) {

        //console.log(`${position} - ${position + length} (${length})`)

        const headers = this.config.headers || {};

        if (position !== undefined && length) {
            const rangeString = "bytes=" + position + "-" + (position + length - 1);
            headers['Range'] = rangeString;
        }

        let url = this.url.slice();    // slice => copy
        if (this.config.oauthToken) {
            const token = await resolveToken(this.config.oauthToken);
            headers['Authorization'] = `Bearer ${token}`;
        }

        if (this.config.apiKey) {
            url = addParameter(url, "key", this.config.apiKey);
        }

        const response = await fetch(url, {
            method: 'GET',
            headers: headers,
            redirect: 'follow',
            mode: 'cors',
        });

        const status = response.status;

        if (status >= 400) {
            const err = Error(response.statusText);
            err.code = status;
            throw err
        } else {
            return response.arrayBuffer()
        }

        /**
         * token can be a string, a function that returns a string, or a function that returns a Promise for a string
         * @param token
         * @returns {Promise<*>}
         */
        async function resolveToken(token) {
            if (typeof token === 'function') {
                return await Promise.resolve(token())    // Normalize the result to a promise, since we don't know what the function returns
            } else {
                return token
            }
        }

    }
}


function mapUrl$2(url) {

    if (url.includes("//www.dropbox.com")) {
        return url.replace("//www.dropbox.com", "//dl.dropboxusercontent.com")
    } else if (url.startsWith("ftp://ftp.ncbi.nlm.nih.gov")) {
        return url.replace("ftp://", "https://")
    } else {
        return url
    }
}


function addParameter(url, name, value) {
    const paramSeparator = url.includes("?") ? "&" : "?";
    return url + paramSeparator + name + "=" + value
}

class BufferedFile$1 {

    constructor(args) {
        this.file = args.file;
        this.fetchSize = args.fetchSize || 16000;
        this.maxSize = args.maxSize || 1000000;
        this.buffers = [];
    }

    async read(position, length) {


        let overlappingBuffers = this.buffers.filter( b => b.overlaps(position, position + length));

        // See if any buffers completely contain request, if so we're done
        for(let buffer of overlappingBuffers) {
            if(buffer.contains(position, position + length)) {
              return buffer.slice(position, position + length)
            }
        }


        if(overlappingBuffers.length === 0) {

            // No overlap with any existing buffer
            let size = Math.max(length, this.fetchSize);

            // Find index of first buffer to the right, if any, to potentially limit size
            this.buffers.sort((a, b) => a.start - b.start);
            const idx = binarySearch(this.buffers, (b) => b.start > position, 0);
            if(idx < this.buffers.length) {
                size = Math.min(size, this.buffers[idx].start - position);
            }

            const bufferStart = position;
            const bufferData = await this.file.read(bufferStart, size);
            const buffer = new Buffer$1(bufferStart, bufferData);
            this.addBuffer(buffer);

            return buffer.slice(position, position + length)
        } else {

            // console.log("Cache hit")
            // Some overlap.   Fill gaps
            overlappingBuffers.sort((a, b) => a.start - b.start);
            const allBuffers = [];
            let currentEnd = position;
            for (let ob of overlappingBuffers) {
                if (currentEnd < ob.start) {
                    const bufferStart = currentEnd;
                    const bufferSize = ob.start - currentEnd;
                    const bufferData = await this.file.read(bufferStart, bufferSize);
                    const buffer = new Buffer$1(bufferStart, bufferData);
                    allBuffers.push(buffer);
                }
                allBuffers.push(ob);
                currentEnd = ob.end;
            }

            // Check end
            const requestedEnd = position + length;
            if (requestedEnd > currentEnd) {
                const bufferStart = currentEnd;
                const bufferSize = requestedEnd - bufferStart;
                const bufferData = await this.file.read(bufferStart, bufferSize);
                const buffer = new Buffer$1(bufferStart, bufferData);
                allBuffers.push(buffer);
            }

            const newStart = allBuffers[0].start;
            const newArrayBuffer = concatArrayBuffers(allBuffers.map(b => b.buffer));
            const newBuffer = new Buffer$1(newStart, newArrayBuffer);

            // Replace the overlapping buffers with the new composite one
            const tmp = new Set(overlappingBuffers);
            this.buffers = this.buffers.filter(b => !tmp.has(b));
            this.addBuffer(newBuffer);

            return newBuffer.slice(position, position + length)
        }

    }

    addBuffer(buffer) {

        const size = this.buffers.reduce((a,b) => a + b.size, 0) + buffer.size;
        if(size > this.maxSize) {
            // console.log(`max buffer size exceeded`)
            const overage = size - this.maxSize;
            this.buffers.sort((a,b) => a.creationTime - b.creationTime);
            let sum = 0;
            let i;
            for(i=0; i<this.buffers.length; i++) {
                sum += this.buffers[i].size;
                if(sum > overage) {
                    break
                }
            }
            // console.log('removing buffers')
            // for(let j=0; j<i; j++) console.log(`  ${this.buffers[j].toString()}`)
            this.buffers = (i < this.buffers.length - 1)  ? this.buffers.slice(i)  : [];
        }

        if(buffer.size <= this.maxSize) {
            this.buffers.push(buffer);
        }
    }



}

class Buffer$1 {

    constructor(bufferStart, buffer) {
        this.creationTime = Date.now();
        this.start = bufferStart;
        this.buffer = buffer;
    }

    slice(start, end) {
        if(start < this.start || end - start > this.buffer.byteLength) {
            throw Error("buffer bounds error")
        }
        return this.buffer.slice(start - this.start, end - this.start)
    }

    get end() {
        return this.start + this.buffer.byteLength
    }

    get size() {
        return this.buffer.byteLength
    }

    contains(start, end) {
        return start >= this.start && end <= this.end
    }

    overlaps(start, end) {
        return (start > this.start && start < this.end) || (end > this.start && end < this.end)
    }

    toString() {
        return `Buffer ${this.creationTime}   ${this.start} - ${this.end}`
    }

}

/**
 * concatenates 2 array buffers.
 * Credit: https://gist.github.com/72lions/4528834
 *
 * @private
 * @param {ArrayBuffers} buffer1 The first buffer.
 * @param {ArrayBuffers} buffer2 The second buffer.
 * @return {ArrayBuffers} The new ArrayBuffer created out of the two.
 */
function concatArrayBuffers(buffers) {
    const size = buffers.reduce((a,b) => a + b.byteLength, 0);
    const tmp = new Uint8Array(size);
    let offset = 0;
    for(let b of buffers) {
        tmp.set(new Uint8Array(b), offset);
        offset += b.byteLength;
    }
    return tmp.buffer
}

/**
 * Return 0 <= i <= array.length such that !pred(array[i - 1]) && pred(array[i]).
 *
 * returns an index 0 ≤ i ≤ array.length such that the given predicate is false for array[i - 1] and true for array[i]* *
 */
function binarySearch(array, pred, min) {
    let lo = min - 1, hi = array.length;
    while (1 + lo < hi) {
        const mi = lo + ((hi - lo) >> 1);
        if (pred(array[mi])) {
            hi = mi;
        } else {
            lo = mi;
        }
    }
    return hi
}

class BlobFile {

    constructor(blob) {
        this.file = blob;
    }

    async read(position, length) {

        if(length === 0) {
            return new ArrayBuffer()
        }

        const blob = (position != undefined && length) ?
            this.file.slice(position, position + length) :
            this.file;

        return blob.arrayBuffer()
    }
}

var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => {
  __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  return value;
};

// esm/core.js
async function _unpack_struct_from_async(structure, async_buf, offset = 0) {
  var output = /* @__PURE__ */ new Map();
  for (let [key, fmt] of structure.entries()) {
    let value = await struct.unpack_from_async("<" + fmt, async_buf, offset);
    offset += struct.calcsize(fmt);
    if (value.length == 1) {
      value = value[0];
    }
    output.set(key, value);
  }
  return output;
}
function _unpack_struct_from(structure, buf, offset = 0) {
  var output = /* @__PURE__ */ new Map();
  for (let [key, fmt] of structure.entries()) {
    let value = struct.unpack_from("<" + fmt, buf, offset);
    offset += struct.calcsize(fmt);
    if (value.length == 1) {
      value = value[0];
    }
    output.set(key, value);
  }
  return output;
}
function assert(thing) {
  if (!thing) {
    thing();
  }
}
function _structure_size(structure) {
  var fmt = "<" + Array.from(structure.values()).join("");
  return struct.calcsize(fmt);
}
function _padded_size(size, padding_multiple = 8) {
  return Math.ceil(size / padding_multiple) * padding_multiple;
}
var dtype_to_format = {
  "u": "Uint",
  "i": "Int",
  "f": "Float"
};
function dtype_getter(dtype_str) {
  var big_endian = struct._is_big_endian(dtype_str);
  var getter, nbytes;
  if (/S/.test(dtype_str)) {
    getter = "getString";
    nbytes = ((dtype_str.match(/S(\d*)/) || [])[1] || 1) | 0;
  } else {
    let [_, fstr, bytestr] = dtype_str.match(/[<>=!@]?(i|u|f)(\d*)/);
    nbytes = parseInt(bytestr || 4, 10);
    let nbits = nbytes * 8;
    getter = "get" + dtype_to_format[fstr] + nbits.toFixed();
  }
  return [getter, big_endian, nbytes];
}
var Struct = class {
  constructor() {
    this.big_endian = isBigEndian();
    this.getters = {
      "s": "getUint8",
      "b": "getInt8",
      "B": "getUint8",
      "h": "getInt16",
      "H": "getUint16",
      "i": "getInt32",
      "I": "getUint32",
      "l": "getInt32",
      "L": "getUint32",
      "q": "getInt64",
      "Q": "getUint64",
      "f": "getFloat32",
      "d": "getFloat64"
    };
    this.byte_lengths = {
      "s": 1,
      "b": 1,
      "B": 1,
      "h": 2,
      "H": 2,
      "i": 4,
      "I": 4,
      "l": 4,
      "L": 4,
      "q": 8,
      "Q": 8,
      "f": 4,
      "d": 8
    };
    let all_formats = Object.keys(this.byte_lengths).join("");
    this.fmt_size_regex = "(\\d*)([" + all_formats + "])";
  }
  calcsize(fmt) {
    var size = 0;
    var match;
    var regex = new RegExp(this.fmt_size_regex, "g");
    while ((match = regex.exec(fmt)) !== null) {
      let n = parseInt(match[1] || 1, 10);
      let f = match[2];
      let subsize = this.byte_lengths[f];
      size += n * subsize;
    }
    return size;
  }
  _is_big_endian(fmt) {
    var big_endian;
    if (/^</.test(fmt)) {
      big_endian = false;
    } else if (/^(!|>)/.test(fmt)) {
      big_endian = true;
    } else {
      big_endian = this.big_endian;
    }
    return big_endian;
  }
  async unpack_from_async(fmt, async_buf, offset) {
    offset = Number.parseInt(offset || 0);
    const total_size = this.calcsize(fmt);
    const local_buffer = await async_buf.slice(offset, offset + total_size);
    let local_offset = 0;
    var view = new DataView64(local_buffer);
    var output = [];
    var big_endian = this._is_big_endian(fmt);
    var match;
    var regex = new RegExp(this.fmt_size_regex, "g");
    while ((match = regex.exec(fmt)) !== null) {
      let n = parseInt(match[1] || 1, 10);
      let f = match[2];
      let getter = this.getters[f];
      let size = this.byte_lengths[f];
      if (f == "s") {
        output.push(new TextDecoder().decode(local_buffer.slice(local_offset, local_offset + n)));
        local_offset += n;
      } else {
        for (var i = 0; i < n; i++) {
          output.push(view[getter](local_offset, !big_endian));
          local_offset += size;
        }
      }
    }
    return output;
  }
  unpack_from(fmt, buffer, offset) {
    offset = Number.parseInt(offset || 0);
    const total_size = this.calcsize(fmt);
    const local_buffer = buffer.slice(offset, offset + total_size);
    let local_offset = 0;
    var view = new DataView64(local_buffer);
    var output = [];
    var big_endian = this._is_big_endian(fmt);
    var match;
    var regex = new RegExp(this.fmt_size_regex, "g");
    while ((match = regex.exec(fmt)) !== null) {
      let n = parseInt(match[1] || 1, 10);
      let f = match[2];
      let getter = this.getters[f];
      let size = this.byte_lengths[f];
      if (f == "s") {
        output.push(new TextDecoder().decode(local_buffer.slice(local_offset, local_offset + n)));
        local_offset += n;
      } else {
        for (var i = 0; i < n; i++) {
          output.push(view[getter](local_offset, !big_endian));
          local_offset += size;
        }
      }
    }
    return output;
  }
};
var struct = new Struct();
function isBigEndian() {
  const array = new Uint8Array(4);
  const view = new Uint32Array(array.buffer);
  return !((view[0] = 1) & array[0]);
}
var DataView64 = class extends DataView {
  getUint64(byteOffset, littleEndian) {
    const left = BigInt(this.getUint32(byteOffset, littleEndian));
    const right = BigInt(this.getUint32(byteOffset + 4, littleEndian));
    let combined = littleEndian ? left + (right << 32n) : (left << 32n) + right;
    return Number.parseInt(combined);
  }
  getInt64(byteOffset, littleEndian) {
    var low, high;
    if (littleEndian) {
      low = this.getUint32(byteOffset, true);
      high = this.getInt32(byteOffset + 4, true);
    } else {
      high = this.getInt32(byteOffset, false);
      low = this.getUint32(byteOffset + 4, false);
    }
    let combined = BigInt(low) + (BigInt(high) << 32n);
    return Number.parseInt(combined);
  }
  getString(byteOffset, littleEndian, length) {
    const str_buffer = this.buffer.slice(byteOffset, byteOffset + length);
    const decoder = new TextDecoder();
    return decoder.decode(str_buffer);
  }
  getVLENStruct(byteOffset, littleEndian, length) {
    let item_size = this.getUint32(byteOffset, littleEndian);
    let collection_address = this.getUint64(byteOffset + 4, littleEndian);
    let object_index = this.getUint32(byteOffset + 12, littleEndian);
    return [item_size, collection_address, object_index];
  }
};
function bitSize(integer) {
  return integer.toString(2).length;
}
function _unpack_integer(nbytes, buf, offset = 0, littleEndian = true) {
  const local_buffer = buf.slice(offset, offset + nbytes);
  let bytes = new Uint8Array(local_buffer);
  if (!littleEndian) {
    bytes.reverse();
  }
  let integer = bytes.reduce((accumulator, currentValue, index) => accumulator + (currentValue << index * 8), 0);
  return integer;
}

// esm/datatype-msg.js
var DatatypeMessage = class {
  constructor(buf, offset) {
    this.buf = buf;
    this.offset = offset;
    this.dtype = this.determine_dtype();
  }
  async determine_dtype() {
    let datatype_msg = await _unpack_struct_from_async(DATATYPE_MSG, this.buf, this.offset);
    this.offset += DATATYPE_MSG_SIZE;
    let datatype_class = datatype_msg.get("class_and_version") & 15;
    if (datatype_class == DATATYPE_FIXED_POINT) {
      return this._determine_dtype_fixed_point(datatype_msg);
    } else if (datatype_class == DATATYPE_FLOATING_POINT) {
      return this._determine_dtype_floating_point(datatype_msg);
    } else if (datatype_class == DATATYPE_TIME) {
      throw "Time datatype class not supported.";
    } else if (datatype_class == DATATYPE_STRING) {
      return this._determine_dtype_string(datatype_msg);
    } else if (datatype_class == DATATYPE_BITFIELD) {
      throw "Bitfield datatype class not supported.";
    } else if (datatype_class == DATATYPE_OPAQUE) {
      return {
        datatype_class: DATATYPE_OPAQUE,
        size: datatype_msg.get("size")
      };
    } else if (datatype_class == DATATYPE_COMPOUND) {
      return this._determine_dtype_compound(datatype_msg);
    } else if (datatype_class == DATATYPE_REFERENCE) {
      return ["REFERENCE", datatype_msg.get("size")];
    } else if (datatype_class == DATATYPE_ENUMERATED) {
      return this.determine_dtype();
    } else if (datatype_class == DATATYPE_ARRAY) {
      throw "Array datatype class not supported.";
    } else if (datatype_class == DATATYPE_VARIABLE_LENGTH) {
      let vlen_type = this._determine_dtype_vlen(datatype_msg);
      if (vlen_type[0] == "VLEN_SEQUENCE") {
        let base_type = this.determine_dtype();
        vlen_type = ["VLEN_SEQUENCE", base_type];
      }
      return vlen_type;
    } else {
      throw "Invalid datatype class " + datatype_class;
    }
  }
  _determine_dtype_fixed_point(datatype_msg) {
    let length_in_bytes = datatype_msg.get("size");
    if (![1, 2, 4, 8].includes(length_in_bytes)) {
      throw "Unsupported datatype size";
    }
    let signed = datatype_msg.get("class_bit_field_0") & 8;
    var dtype_char;
    if (signed > 0) {
      dtype_char = "i";
    } else {
      dtype_char = "u";
    }
    let byte_order = datatype_msg.get("class_bit_field_0") & 1;
    var byte_order_char;
    if (byte_order == 0) {
      byte_order_char = "<";
    } else {
      byte_order_char = ">";
    }
    this.offset += 4;
    return byte_order_char + dtype_char + length_in_bytes.toFixed();
  }
  _determine_dtype_floating_point(datatype_msg) {
    let length_in_bytes = datatype_msg.get("size");
    if (![1, 2, 4, 8].includes(length_in_bytes)) {
      throw "Unsupported datatype size";
    }
    let dtype_char = "f";
    let byte_order = datatype_msg.get("class_bit_field_0") & 1;
    var byte_order_char;
    if (byte_order == 0) {
      byte_order_char = "<";
    } else {
      byte_order_char = ">";
    }
    this.offset += 12;
    return byte_order_char + dtype_char + length_in_bytes.toFixed();
  }
  _determine_dtype_string(datatype_msg) {
    return "S" + datatype_msg.get("size").toFixed();
  }
  _determine_dtype_vlen(datatype_msg) {
    let vlen_type = datatype_msg.get("class_bit_field_0") & 1;
    if (vlen_type != 1) {
      return ["VLEN_SEQUENCE", 0, 0];
    }
    let padding_type = datatype_msg.get("class_bit_field_0") >> 4;
    let character_set = datatype_msg.get("class_bit_field_1") & 1;
    return ["VLEN_STRING", padding_type, character_set];
  }
  _determine_dtype_compound(datatype_msg) {
    throw "Compound type not yet implemented!";
  }
};
var DATATYPE_MSG = /* @__PURE__ */ new Map([
  ["class_and_version", "B"],
  ["class_bit_field_0", "B"],
  ["class_bit_field_1", "B"],
  ["class_bit_field_2", "B"],
  ["size", "I"]
]);
var DATATYPE_MSG_SIZE = _structure_size(DATATYPE_MSG);
var COMPOUND_PROP_DESC_V1 = /* @__PURE__ */ new Map([
  ["offset", "I"],
  ["dimensionality", "B"],
  ["reserved_0", "B"],
  ["reserved_1", "B"],
  ["reserved_2", "B"],
  ["permutation", "I"],
  ["reserved_3", "I"],
  ["dim_size_1", "I"],
  ["dim_size_2", "I"],
  ["dim_size_3", "I"],
  ["dim_size_4", "I"]
]);
_structure_size(COMPOUND_PROP_DESC_V1);
var DATATYPE_FIXED_POINT = 0;
var DATATYPE_FLOATING_POINT = 1;
var DATATYPE_TIME = 2;
var DATATYPE_STRING = 3;
var DATATYPE_BITFIELD = 4;
var DATATYPE_OPAQUE = 5;
var DATATYPE_COMPOUND = 6;
var DATATYPE_REFERENCE = 7;
var DATATYPE_ENUMERATED = 8;
var DATATYPE_VARIABLE_LENGTH = 9;
var DATATYPE_ARRAY = 10;
function zero$1$2(buf) {
  let len = buf.length;
  while (--len >= 0) {
    buf[len] = 0;
  }
}
var MIN_MATCH$1$2 = 3;
var MAX_MATCH$1$2 = 258;
var LENGTH_CODES$1$2 = 29;
var LITERALS$1$2 = 256;
var L_CODES$1$2 = LITERALS$1$2 + 1 + LENGTH_CODES$1$2;
var D_CODES$1$2 = 30;
var DIST_CODE_LEN$2 = 512;
var static_ltree$2 = new Array((L_CODES$1$2 + 2) * 2);
zero$1$2(static_ltree$2);
var static_dtree$2 = new Array(D_CODES$1$2 * 2);
zero$1$2(static_dtree$2);
var _dist_code$2 = new Array(DIST_CODE_LEN$2);
zero$1$2(_dist_code$2);
var _length_code$2 = new Array(MAX_MATCH$1$2 - MIN_MATCH$1$2 + 1);
zero$1$2(_length_code$2);
var base_length$2 = new Array(LENGTH_CODES$1$2);
zero$1$2(base_length$2);
var base_dist$2 = new Array(D_CODES$1$2);
zero$1$2(base_dist$2);
var adler32$2 = (adler, buf, len, pos) => {
  let s1 = adler & 65535 | 0, s2 = adler >>> 16 & 65535 | 0, n = 0;
  while (len !== 0) {
    n = len > 2e3 ? 2e3 : len;
    len -= n;
    do {
      s1 = s1 + buf[pos++] | 0;
      s2 = s2 + s1 | 0;
    } while (--n);
    s1 %= 65521;
    s2 %= 65521;
  }
  return s1 | s2 << 16 | 0;
};
var adler32_1$2 = adler32$2;
var makeTable$2 = () => {
  let c, table = [];
  for (var n = 0; n < 256; n++) {
    c = n;
    for (var k = 0; k < 8; k++) {
      c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
    }
    table[n] = c;
  }
  return table;
};
var crcTable$2 = new Uint32Array(makeTable$2());
var crc32$2 = (crc, buf, len, pos) => {
  const t = crcTable$2;
  const end = pos + len;
  crc ^= -1;
  for (let i = pos; i < end; i++) {
    crc = crc >>> 8 ^ t[(crc ^ buf[i]) & 255];
  }
  return crc ^ -1;
};
var crc32_1$2 = crc32$2;
var messages$2 = {
  2: "need dictionary",
  1: "stream end",
  0: "",
  "-1": "file error",
  "-2": "stream error",
  "-3": "data error",
  "-4": "insufficient memory",
  "-5": "buffer error",
  "-6": "incompatible version"
};
var constants$2$2 = {
  Z_NO_FLUSH: 0,
  Z_PARTIAL_FLUSH: 1,
  Z_SYNC_FLUSH: 2,
  Z_FULL_FLUSH: 3,
  Z_FINISH: 4,
  Z_BLOCK: 5,
  Z_TREES: 6,
  Z_OK: 0,
  Z_STREAM_END: 1,
  Z_NEED_DICT: 2,
  Z_ERRNO: -1,
  Z_STREAM_ERROR: -2,
  Z_DATA_ERROR: -3,
  Z_MEM_ERROR: -4,
  Z_BUF_ERROR: -5,
  Z_NO_COMPRESSION: 0,
  Z_BEST_SPEED: 1,
  Z_BEST_COMPRESSION: 9,
  Z_DEFAULT_COMPRESSION: -1,
  Z_FILTERED: 1,
  Z_HUFFMAN_ONLY: 2,
  Z_RLE: 3,
  Z_FIXED: 4,
  Z_DEFAULT_STRATEGY: 0,
  Z_BINARY: 0,
  Z_TEXT: 1,
  Z_UNKNOWN: 2,
  Z_DEFLATED: 8
};
var _has$2 = (obj, key) => {
  return Object.prototype.hasOwnProperty.call(obj, key);
};
var assign$2 = function(obj) {
  const sources = Array.prototype.slice.call(arguments, 1);
  while (sources.length) {
    const source = sources.shift();
    if (!source) {
      continue;
    }
    if (typeof source !== "object") {
      throw new TypeError(source + "must be non-object");
    }
    for (const p in source) {
      if (_has$2(source, p)) {
        obj[p] = source[p];
      }
    }
  }
  return obj;
};
var flattenChunks$2 = (chunks) => {
  let len = 0;
  for (let i = 0, l = chunks.length; i < l; i++) {
    len += chunks[i].length;
  }
  const result = new Uint8Array(len);
  for (let i = 0, pos = 0, l = chunks.length; i < l; i++) {
    let chunk = chunks[i];
    result.set(chunk, pos);
    pos += chunk.length;
  }
  return result;
};
var common$2 = {
  assign: assign$2,
  flattenChunks: flattenChunks$2
};
var STR_APPLY_UIA_OK$2 = true;
try {
  String.fromCharCode.apply(null, new Uint8Array(1));
} catch (__) {
  STR_APPLY_UIA_OK$2 = false;
}
var _utf8len$2 = new Uint8Array(256);
for (let q = 0; q < 256; q++) {
  _utf8len$2[q] = q >= 252 ? 6 : q >= 248 ? 5 : q >= 240 ? 4 : q >= 224 ? 3 : q >= 192 ? 2 : 1;
}
_utf8len$2[254] = _utf8len$2[254] = 1;
var string2buf$2 = (str) => {
  if (typeof TextEncoder === "function" && TextEncoder.prototype.encode) {
    return new TextEncoder().encode(str);
  }
  let buf, c, c2, m_pos, i, str_len = str.length, buf_len = 0;
  for (m_pos = 0; m_pos < str_len; m_pos++) {
    c = str.charCodeAt(m_pos);
    if ((c & 64512) === 55296 && m_pos + 1 < str_len) {
      c2 = str.charCodeAt(m_pos + 1);
      if ((c2 & 64512) === 56320) {
        c = 65536 + (c - 55296 << 10) + (c2 - 56320);
        m_pos++;
      }
    }
    buf_len += c < 128 ? 1 : c < 2048 ? 2 : c < 65536 ? 3 : 4;
  }
  buf = new Uint8Array(buf_len);
  for (i = 0, m_pos = 0; i < buf_len; m_pos++) {
    c = str.charCodeAt(m_pos);
    if ((c & 64512) === 55296 && m_pos + 1 < str_len) {
      c2 = str.charCodeAt(m_pos + 1);
      if ((c2 & 64512) === 56320) {
        c = 65536 + (c - 55296 << 10) + (c2 - 56320);
        m_pos++;
      }
    }
    if (c < 128) {
      buf[i++] = c;
    } else if (c < 2048) {
      buf[i++] = 192 | c >>> 6;
      buf[i++] = 128 | c & 63;
    } else if (c < 65536) {
      buf[i++] = 224 | c >>> 12;
      buf[i++] = 128 | c >>> 6 & 63;
      buf[i++] = 128 | c & 63;
    } else {
      buf[i++] = 240 | c >>> 18;
      buf[i++] = 128 | c >>> 12 & 63;
      buf[i++] = 128 | c >>> 6 & 63;
      buf[i++] = 128 | c & 63;
    }
  }
  return buf;
};
var buf2binstring$2 = (buf, len) => {
  if (len < 65534) {
    if (buf.subarray && STR_APPLY_UIA_OK$2) {
      return String.fromCharCode.apply(null, buf.length === len ? buf : buf.subarray(0, len));
    }
  }
  let result = "";
  for (let i = 0; i < len; i++) {
    result += String.fromCharCode(buf[i]);
  }
  return result;
};
var buf2string$2 = (buf, max) => {
  const len = max || buf.length;
  if (typeof TextDecoder === "function" && TextDecoder.prototype.decode) {
    return new TextDecoder().decode(buf.subarray(0, max));
  }
  let i, out;
  const utf16buf = new Array(len * 2);
  for (out = 0, i = 0; i < len; ) {
    let c = buf[i++];
    if (c < 128) {
      utf16buf[out++] = c;
      continue;
    }
    let c_len = _utf8len$2[c];
    if (c_len > 4) {
      utf16buf[out++] = 65533;
      i += c_len - 1;
      continue;
    }
    c &= c_len === 2 ? 31 : c_len === 3 ? 15 : 7;
    while (c_len > 1 && i < len) {
      c = c << 6 | buf[i++] & 63;
      c_len--;
    }
    if (c_len > 1) {
      utf16buf[out++] = 65533;
      continue;
    }
    if (c < 65536) {
      utf16buf[out++] = c;
    } else {
      c -= 65536;
      utf16buf[out++] = 55296 | c >> 10 & 1023;
      utf16buf[out++] = 56320 | c & 1023;
    }
  }
  return buf2binstring$2(utf16buf, out);
};
var utf8border$2 = (buf, max) => {
  max = max || buf.length;
  if (max > buf.length) {
    max = buf.length;
  }
  let pos = max - 1;
  while (pos >= 0 && (buf[pos] & 192) === 128) {
    pos--;
  }
  if (pos < 0) {
    return max;
  }
  if (pos === 0) {
    return max;
  }
  return pos + _utf8len$2[buf[pos]] > max ? pos : max;
};
var strings$2 = {
  string2buf: string2buf$2,
  buf2string: buf2string$2,
  utf8border: utf8border$2
};
function ZStream$2() {
  this.input = null;
  this.next_in = 0;
  this.avail_in = 0;
  this.total_in = 0;
  this.output = null;
  this.next_out = 0;
  this.avail_out = 0;
  this.total_out = 0;
  this.msg = "";
  this.state = null;
  this.data_type = 2;
  this.adler = 0;
}
var zstream$2 = ZStream$2;
var BAD$1$2 = 30;
var TYPE$1$2 = 12;
var inffast$2 = function inflate_fast(strm, start) {
  let _in;
  let last;
  let _out;
  let beg;
  let end;
  let dmax;
  let wsize;
  let whave;
  let wnext;
  let s_window;
  let hold;
  let bits;
  let lcode;
  let dcode;
  let lmask;
  let dmask;
  let here;
  let op;
  let len;
  let dist;
  let from;
  let from_source;
  let input, output;
  const state = strm.state;
  _in = strm.next_in;
  input = strm.input;
  last = _in + (strm.avail_in - 5);
  _out = strm.next_out;
  output = strm.output;
  beg = _out - (start - strm.avail_out);
  end = _out + (strm.avail_out - 257);
  dmax = state.dmax;
  wsize = state.wsize;
  whave = state.whave;
  wnext = state.wnext;
  s_window = state.window;
  hold = state.hold;
  bits = state.bits;
  lcode = state.lencode;
  dcode = state.distcode;
  lmask = (1 << state.lenbits) - 1;
  dmask = (1 << state.distbits) - 1;
  top:
    do {
      if (bits < 15) {
        hold += input[_in++] << bits;
        bits += 8;
        hold += input[_in++] << bits;
        bits += 8;
      }
      here = lcode[hold & lmask];
      dolen:
        for (; ; ) {
          op = here >>> 24;
          hold >>>= op;
          bits -= op;
          op = here >>> 16 & 255;
          if (op === 0) {
            output[_out++] = here & 65535;
          } else if (op & 16) {
            len = here & 65535;
            op &= 15;
            if (op) {
              if (bits < op) {
                hold += input[_in++] << bits;
                bits += 8;
              }
              len += hold & (1 << op) - 1;
              hold >>>= op;
              bits -= op;
            }
            if (bits < 15) {
              hold += input[_in++] << bits;
              bits += 8;
              hold += input[_in++] << bits;
              bits += 8;
            }
            here = dcode[hold & dmask];
            dodist:
              for (; ; ) {
                op = here >>> 24;
                hold >>>= op;
                bits -= op;
                op = here >>> 16 & 255;
                if (op & 16) {
                  dist = here & 65535;
                  op &= 15;
                  if (bits < op) {
                    hold += input[_in++] << bits;
                    bits += 8;
                    if (bits < op) {
                      hold += input[_in++] << bits;
                      bits += 8;
                    }
                  }
                  dist += hold & (1 << op) - 1;
                  if (dist > dmax) {
                    strm.msg = "invalid distance too far back";
                    state.mode = BAD$1$2;
                    break top;
                  }
                  hold >>>= op;
                  bits -= op;
                  op = _out - beg;
                  if (dist > op) {
                    op = dist - op;
                    if (op > whave) {
                      if (state.sane) {
                        strm.msg = "invalid distance too far back";
                        state.mode = BAD$1$2;
                        break top;
                      }
                    }
                    from = 0;
                    from_source = s_window;
                    if (wnext === 0) {
                      from += wsize - op;
                      if (op < len) {
                        len -= op;
                        do {
                          output[_out++] = s_window[from++];
                        } while (--op);
                        from = _out - dist;
                        from_source = output;
                      }
                    } else if (wnext < op) {
                      from += wsize + wnext - op;
                      op -= wnext;
                      if (op < len) {
                        len -= op;
                        do {
                          output[_out++] = s_window[from++];
                        } while (--op);
                        from = 0;
                        if (wnext < len) {
                          op = wnext;
                          len -= op;
                          do {
                            output[_out++] = s_window[from++];
                          } while (--op);
                          from = _out - dist;
                          from_source = output;
                        }
                      }
                    } else {
                      from += wnext - op;
                      if (op < len) {
                        len -= op;
                        do {
                          output[_out++] = s_window[from++];
                        } while (--op);
                        from = _out - dist;
                        from_source = output;
                      }
                    }
                    while (len > 2) {
                      output[_out++] = from_source[from++];
                      output[_out++] = from_source[from++];
                      output[_out++] = from_source[from++];
                      len -= 3;
                    }
                    if (len) {
                      output[_out++] = from_source[from++];
                      if (len > 1) {
                        output[_out++] = from_source[from++];
                      }
                    }
                  } else {
                    from = _out - dist;
                    do {
                      output[_out++] = output[from++];
                      output[_out++] = output[from++];
                      output[_out++] = output[from++];
                      len -= 3;
                    } while (len > 2);
                    if (len) {
                      output[_out++] = output[from++];
                      if (len > 1) {
                        output[_out++] = output[from++];
                      }
                    }
                  }
                } else if ((op & 64) === 0) {
                  here = dcode[(here & 65535) + (hold & (1 << op) - 1)];
                  continue dodist;
                } else {
                  strm.msg = "invalid distance code";
                  state.mode = BAD$1$2;
                  break top;
                }
                break;
              }
          } else if ((op & 64) === 0) {
            here = lcode[(here & 65535) + (hold & (1 << op) - 1)];
            continue dolen;
          } else if (op & 32) {
            state.mode = TYPE$1$2;
            break top;
          } else {
            strm.msg = "invalid literal/length code";
            state.mode = BAD$1$2;
            break top;
          }
          break;
        }
    } while (_in < last && _out < end);
  len = bits >> 3;
  _in -= len;
  bits -= len << 3;
  hold &= (1 << bits) - 1;
  strm.next_in = _in;
  strm.next_out = _out;
  strm.avail_in = _in < last ? 5 + (last - _in) : 5 - (_in - last);
  strm.avail_out = _out < end ? 257 + (end - _out) : 257 - (_out - end);
  state.hold = hold;
  state.bits = bits;
  return;
};
var MAXBITS$2 = 15;
var ENOUGH_LENS$1$2 = 852;
var ENOUGH_DISTS$1$2 = 592;
var CODES$1$2 = 0;
var LENS$1$2 = 1;
var DISTS$1$2 = 2;
var lbase$2 = new Uint16Array([
  3,
  4,
  5,
  6,
  7,
  8,
  9,
  10,
  11,
  13,
  15,
  17,
  19,
  23,
  27,
  31,
  35,
  43,
  51,
  59,
  67,
  83,
  99,
  115,
  131,
  163,
  195,
  227,
  258,
  0,
  0
]);
var lext$2 = new Uint8Array([
  16,
  16,
  16,
  16,
  16,
  16,
  16,
  16,
  17,
  17,
  17,
  17,
  18,
  18,
  18,
  18,
  19,
  19,
  19,
  19,
  20,
  20,
  20,
  20,
  21,
  21,
  21,
  21,
  16,
  72,
  78
]);
var dbase$2 = new Uint16Array([
  1,
  2,
  3,
  4,
  5,
  7,
  9,
  13,
  17,
  25,
  33,
  49,
  65,
  97,
  129,
  193,
  257,
  385,
  513,
  769,
  1025,
  1537,
  2049,
  3073,
  4097,
  6145,
  8193,
  12289,
  16385,
  24577,
  0,
  0
]);
var dext$2 = new Uint8Array([
  16,
  16,
  16,
  16,
  17,
  17,
  18,
  18,
  19,
  19,
  20,
  20,
  21,
  21,
  22,
  22,
  23,
  23,
  24,
  24,
  25,
  25,
  26,
  26,
  27,
  27,
  28,
  28,
  29,
  29,
  64,
  64
]);
var inflate_table$2 = (type, lens, lens_index, codes, table, table_index, work, opts) => {
  const bits = opts.bits;
  let len = 0;
  let sym = 0;
  let min = 0, max = 0;
  let root = 0;
  let curr = 0;
  let drop = 0;
  let left = 0;
  let used = 0;
  let huff = 0;
  let incr;
  let fill;
  let low;
  let mask;
  let next;
  let base = null;
  let base_index = 0;
  let end;
  const count = new Uint16Array(MAXBITS$2 + 1);
  const offs = new Uint16Array(MAXBITS$2 + 1);
  let extra = null;
  let extra_index = 0;
  let here_bits, here_op, here_val;
  for (len = 0; len <= MAXBITS$2; len++) {
    count[len] = 0;
  }
  for (sym = 0; sym < codes; sym++) {
    count[lens[lens_index + sym]]++;
  }
  root = bits;
  for (max = MAXBITS$2; max >= 1; max--) {
    if (count[max] !== 0) {
      break;
    }
  }
  if (root > max) {
    root = max;
  }
  if (max === 0) {
    table[table_index++] = 1 << 24 | 64 << 16 | 0;
    table[table_index++] = 1 << 24 | 64 << 16 | 0;
    opts.bits = 1;
    return 0;
  }
  for (min = 1; min < max; min++) {
    if (count[min] !== 0) {
      break;
    }
  }
  if (root < min) {
    root = min;
  }
  left = 1;
  for (len = 1; len <= MAXBITS$2; len++) {
    left <<= 1;
    left -= count[len];
    if (left < 0) {
      return -1;
    }
  }
  if (left > 0 && (type === CODES$1$2 || max !== 1)) {
    return -1;
  }
  offs[1] = 0;
  for (len = 1; len < MAXBITS$2; len++) {
    offs[len + 1] = offs[len] + count[len];
  }
  for (sym = 0; sym < codes; sym++) {
    if (lens[lens_index + sym] !== 0) {
      work[offs[lens[lens_index + sym]]++] = sym;
    }
  }
  if (type === CODES$1$2) {
    base = extra = work;
    end = 19;
  } else if (type === LENS$1$2) {
    base = lbase$2;
    base_index -= 257;
    extra = lext$2;
    extra_index -= 257;
    end = 256;
  } else {
    base = dbase$2;
    extra = dext$2;
    end = -1;
  }
  huff = 0;
  sym = 0;
  len = min;
  next = table_index;
  curr = root;
  drop = 0;
  low = -1;
  used = 1 << root;
  mask = used - 1;
  if (type === LENS$1$2 && used > ENOUGH_LENS$1$2 || type === DISTS$1$2 && used > ENOUGH_DISTS$1$2) {
    return 1;
  }
  for (; ; ) {
    here_bits = len - drop;
    if (work[sym] < end) {
      here_op = 0;
      here_val = work[sym];
    } else if (work[sym] > end) {
      here_op = extra[extra_index + work[sym]];
      here_val = base[base_index + work[sym]];
    } else {
      here_op = 32 + 64;
      here_val = 0;
    }
    incr = 1 << len - drop;
    fill = 1 << curr;
    min = fill;
    do {
      fill -= incr;
      table[next + (huff >> drop) + fill] = here_bits << 24 | here_op << 16 | here_val | 0;
    } while (fill !== 0);
    incr = 1 << len - 1;
    while (huff & incr) {
      incr >>= 1;
    }
    if (incr !== 0) {
      huff &= incr - 1;
      huff += incr;
    } else {
      huff = 0;
    }
    sym++;
    if (--count[len] === 0) {
      if (len === max) {
        break;
      }
      len = lens[lens_index + work[sym]];
    }
    if (len > root && (huff & mask) !== low) {
      if (drop === 0) {
        drop = root;
      }
      next += min;
      curr = len - drop;
      left = 1 << curr;
      while (curr + drop < max) {
        left -= count[curr + drop];
        if (left <= 0) {
          break;
        }
        curr++;
        left <<= 1;
      }
      used += 1 << curr;
      if (type === LENS$1$2 && used > ENOUGH_LENS$1$2 || type === DISTS$1$2 && used > ENOUGH_DISTS$1$2) {
        return 1;
      }
      low = huff & mask;
      table[low] = root << 24 | curr << 16 | next - table_index | 0;
    }
  }
  if (huff !== 0) {
    table[next + huff] = len - drop << 24 | 64 << 16 | 0;
  }
  opts.bits = root;
  return 0;
};
var inftrees$2 = inflate_table$2;
var CODES$3 = 0;
var LENS$3 = 1;
var DISTS$3 = 2;
var {
  Z_FINISH: Z_FINISH$1$2,
  Z_BLOCK: Z_BLOCK$3,
  Z_TREES: Z_TREES$2,
  Z_OK: Z_OK$1$2,
  Z_STREAM_END: Z_STREAM_END$1$2,
  Z_NEED_DICT: Z_NEED_DICT$1$2,
  Z_STREAM_ERROR: Z_STREAM_ERROR$1$2,
  Z_DATA_ERROR: Z_DATA_ERROR$1$2,
  Z_MEM_ERROR: Z_MEM_ERROR$1$2,
  Z_BUF_ERROR: Z_BUF_ERROR$3,
  Z_DEFLATED: Z_DEFLATED$4
} = constants$2$2;
var HEAD$2 = 1;
var FLAGS$2 = 2;
var TIME$2 = 3;
var OS$2 = 4;
var EXLEN$2 = 5;
var EXTRA$2 = 6;
var NAME$2 = 7;
var COMMENT$2 = 8;
var HCRC$2 = 9;
var DICTID$2 = 10;
var DICT$2 = 11;
var TYPE$3 = 12;
var TYPEDO$2 = 13;
var STORED$2 = 14;
var COPY_$2 = 15;
var COPY$2 = 16;
var TABLE$2 = 17;
var LENLENS$2 = 18;
var CODELENS$2 = 19;
var LEN_$2 = 20;
var LEN$2 = 21;
var LENEXT$2 = 22;
var DIST$2 = 23;
var DISTEXT$2 = 24;
var MATCH$2 = 25;
var LIT$2 = 26;
var CHECK$2 = 27;
var LENGTH$2 = 28;
var DONE$2 = 29;
var BAD$3 = 30;
var MEM$2 = 31;
var SYNC$2 = 32;
var ENOUGH_LENS$3 = 852;
var ENOUGH_DISTS$3 = 592;
var MAX_WBITS$3 = 15;
var DEF_WBITS$2 = MAX_WBITS$3;
var zswap32$2 = (q) => {
  return (q >>> 24 & 255) + (q >>> 8 & 65280) + ((q & 65280) << 8) + ((q & 255) << 24);
};
function InflateState$2() {
  this.mode = 0;
  this.last = false;
  this.wrap = 0;
  this.havedict = false;
  this.flags = 0;
  this.dmax = 0;
  this.check = 0;
  this.total = 0;
  this.head = null;
  this.wbits = 0;
  this.wsize = 0;
  this.whave = 0;
  this.wnext = 0;
  this.window = null;
  this.hold = 0;
  this.bits = 0;
  this.length = 0;
  this.offset = 0;
  this.extra = 0;
  this.lencode = null;
  this.distcode = null;
  this.lenbits = 0;
  this.distbits = 0;
  this.ncode = 0;
  this.nlen = 0;
  this.ndist = 0;
  this.have = 0;
  this.next = null;
  this.lens = new Uint16Array(320);
  this.work = new Uint16Array(288);
  this.lendyn = null;
  this.distdyn = null;
  this.sane = 0;
  this.back = 0;
  this.was = 0;
}
var inflateResetKeep$2 = (strm) => {
  if (!strm || !strm.state) {
    return Z_STREAM_ERROR$1$2;
  }
  const state = strm.state;
  strm.total_in = strm.total_out = state.total = 0;
  strm.msg = "";
  if (state.wrap) {
    strm.adler = state.wrap & 1;
  }
  state.mode = HEAD$2;
  state.last = 0;
  state.havedict = 0;
  state.dmax = 32768;
  state.head = null;
  state.hold = 0;
  state.bits = 0;
  state.lencode = state.lendyn = new Int32Array(ENOUGH_LENS$3);
  state.distcode = state.distdyn = new Int32Array(ENOUGH_DISTS$3);
  state.sane = 1;
  state.back = -1;
  return Z_OK$1$2;
};
var inflateReset$2 = (strm) => {
  if (!strm || !strm.state) {
    return Z_STREAM_ERROR$1$2;
  }
  const state = strm.state;
  state.wsize = 0;
  state.whave = 0;
  state.wnext = 0;
  return inflateResetKeep$2(strm);
};
var inflateReset2$2 = (strm, windowBits) => {
  let wrap;
  if (!strm || !strm.state) {
    return Z_STREAM_ERROR$1$2;
  }
  const state = strm.state;
  if (windowBits < 0) {
    wrap = 0;
    windowBits = -windowBits;
  } else {
    wrap = (windowBits >> 4) + 1;
    if (windowBits < 48) {
      windowBits &= 15;
    }
  }
  if (windowBits && (windowBits < 8 || windowBits > 15)) {
    return Z_STREAM_ERROR$1$2;
  }
  if (state.window !== null && state.wbits !== windowBits) {
    state.window = null;
  }
  state.wrap = wrap;
  state.wbits = windowBits;
  return inflateReset$2(strm);
};
var inflateInit2$2 = (strm, windowBits) => {
  if (!strm) {
    return Z_STREAM_ERROR$1$2;
  }
  const state = new InflateState$2();
  strm.state = state;
  state.window = null;
  const ret = inflateReset2$2(strm, windowBits);
  if (ret !== Z_OK$1$2) {
    strm.state = null;
  }
  return ret;
};
var inflateInit$2 = (strm) => {
  return inflateInit2$2(strm, DEF_WBITS$2);
};
var virgin$2 = true;
var lenfix$2;
var distfix$2;
var fixedtables$2 = (state) => {
  if (virgin$2) {
    lenfix$2 = new Int32Array(512);
    distfix$2 = new Int32Array(32);
    let sym = 0;
    while (sym < 144) {
      state.lens[sym++] = 8;
    }
    while (sym < 256) {
      state.lens[sym++] = 9;
    }
    while (sym < 280) {
      state.lens[sym++] = 7;
    }
    while (sym < 288) {
      state.lens[sym++] = 8;
    }
    inftrees$2(LENS$3, state.lens, 0, 288, lenfix$2, 0, state.work, { bits: 9 });
    sym = 0;
    while (sym < 32) {
      state.lens[sym++] = 5;
    }
    inftrees$2(DISTS$3, state.lens, 0, 32, distfix$2, 0, state.work, { bits: 5 });
    virgin$2 = false;
  }
  state.lencode = lenfix$2;
  state.lenbits = 9;
  state.distcode = distfix$2;
  state.distbits = 5;
};
var updatewindow$2 = (strm, src, end, copy) => {
  let dist;
  const state = strm.state;
  if (state.window === null) {
    state.wsize = 1 << state.wbits;
    state.wnext = 0;
    state.whave = 0;
    state.window = new Uint8Array(state.wsize);
  }
  if (copy >= state.wsize) {
    state.window.set(src.subarray(end - state.wsize, end), 0);
    state.wnext = 0;
    state.whave = state.wsize;
  } else {
    dist = state.wsize - state.wnext;
    if (dist > copy) {
      dist = copy;
    }
    state.window.set(src.subarray(end - copy, end - copy + dist), state.wnext);
    copy -= dist;
    if (copy) {
      state.window.set(src.subarray(end - copy, end), 0);
      state.wnext = copy;
      state.whave = state.wsize;
    } else {
      state.wnext += dist;
      if (state.wnext === state.wsize) {
        state.wnext = 0;
      }
      if (state.whave < state.wsize) {
        state.whave += dist;
      }
    }
  }
  return 0;
};
var inflate$2$2 = (strm, flush) => {
  let state;
  let input, output;
  let next;
  let put;
  let have, left;
  let hold;
  let bits;
  let _in, _out;
  let copy;
  let from;
  let from_source;
  let here = 0;
  let here_bits, here_op, here_val;
  let last_bits, last_op, last_val;
  let len;
  let ret;
  const hbuf = new Uint8Array(4);
  let opts;
  let n;
  const order = new Uint8Array([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
  if (!strm || !strm.state || !strm.output || !strm.input && strm.avail_in !== 0) {
    return Z_STREAM_ERROR$1$2;
  }
  state = strm.state;
  if (state.mode === TYPE$3) {
    state.mode = TYPEDO$2;
  }
  put = strm.next_out;
  output = strm.output;
  left = strm.avail_out;
  next = strm.next_in;
  input = strm.input;
  have = strm.avail_in;
  hold = state.hold;
  bits = state.bits;
  _in = have;
  _out = left;
  ret = Z_OK$1$2;
  inf_leave:
    for (; ; ) {
      switch (state.mode) {
        case HEAD$2:
          if (state.wrap === 0) {
            state.mode = TYPEDO$2;
            break;
          }
          while (bits < 16) {
            if (have === 0) {
              break inf_leave;
            }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          if (state.wrap & 2 && hold === 35615) {
            state.check = 0;
            hbuf[0] = hold & 255;
            hbuf[1] = hold >>> 8 & 255;
            state.check = crc32_1$2(state.check, hbuf, 2, 0);
            hold = 0;
            bits = 0;
            state.mode = FLAGS$2;
            break;
          }
          state.flags = 0;
          if (state.head) {
            state.head.done = false;
          }
          if (!(state.wrap & 1) || (((hold & 255) << 8) + (hold >> 8)) % 31) {
            strm.msg = "incorrect header check";
            state.mode = BAD$3;
            break;
          }
          if ((hold & 15) !== Z_DEFLATED$4) {
            strm.msg = "unknown compression method";
            state.mode = BAD$3;
            break;
          }
          hold >>>= 4;
          bits -= 4;
          len = (hold & 15) + 8;
          if (state.wbits === 0) {
            state.wbits = len;
          } else if (len > state.wbits) {
            strm.msg = "invalid window size";
            state.mode = BAD$3;
            break;
          }
          state.dmax = 1 << state.wbits;
          strm.adler = state.check = 1;
          state.mode = hold & 512 ? DICTID$2 : TYPE$3;
          hold = 0;
          bits = 0;
          break;
        case FLAGS$2:
          while (bits < 16) {
            if (have === 0) {
              break inf_leave;
            }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          state.flags = hold;
          if ((state.flags & 255) !== Z_DEFLATED$4) {
            strm.msg = "unknown compression method";
            state.mode = BAD$3;
            break;
          }
          if (state.flags & 57344) {
            strm.msg = "unknown header flags set";
            state.mode = BAD$3;
            break;
          }
          if (state.head) {
            state.head.text = hold >> 8 & 1;
          }
          if (state.flags & 512) {
            hbuf[0] = hold & 255;
            hbuf[1] = hold >>> 8 & 255;
            state.check = crc32_1$2(state.check, hbuf, 2, 0);
          }
          hold = 0;
          bits = 0;
          state.mode = TIME$2;
        case TIME$2:
          while (bits < 32) {
            if (have === 0) {
              break inf_leave;
            }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          if (state.head) {
            state.head.time = hold;
          }
          if (state.flags & 512) {
            hbuf[0] = hold & 255;
            hbuf[1] = hold >>> 8 & 255;
            hbuf[2] = hold >>> 16 & 255;
            hbuf[3] = hold >>> 24 & 255;
            state.check = crc32_1$2(state.check, hbuf, 4, 0);
          }
          hold = 0;
          bits = 0;
          state.mode = OS$2;
        case OS$2:
          while (bits < 16) {
            if (have === 0) {
              break inf_leave;
            }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          if (state.head) {
            state.head.xflags = hold & 255;
            state.head.os = hold >> 8;
          }
          if (state.flags & 512) {
            hbuf[0] = hold & 255;
            hbuf[1] = hold >>> 8 & 255;
            state.check = crc32_1$2(state.check, hbuf, 2, 0);
          }
          hold = 0;
          bits = 0;
          state.mode = EXLEN$2;
        case EXLEN$2:
          if (state.flags & 1024) {
            while (bits < 16) {
              if (have === 0) {
                break inf_leave;
              }
              have--;
              hold += input[next++] << bits;
              bits += 8;
            }
            state.length = hold;
            if (state.head) {
              state.head.extra_len = hold;
            }
            if (state.flags & 512) {
              hbuf[0] = hold & 255;
              hbuf[1] = hold >>> 8 & 255;
              state.check = crc32_1$2(state.check, hbuf, 2, 0);
            }
            hold = 0;
            bits = 0;
          } else if (state.head) {
            state.head.extra = null;
          }
          state.mode = EXTRA$2;
        case EXTRA$2:
          if (state.flags & 1024) {
            copy = state.length;
            if (copy > have) {
              copy = have;
            }
            if (copy) {
              if (state.head) {
                len = state.head.extra_len - state.length;
                if (!state.head.extra) {
                  state.head.extra = new Uint8Array(state.head.extra_len);
                }
                state.head.extra.set(input.subarray(next, next + copy), len);
              }
              if (state.flags & 512) {
                state.check = crc32_1$2(state.check, input, copy, next);
              }
              have -= copy;
              next += copy;
              state.length -= copy;
            }
            if (state.length) {
              break inf_leave;
            }
          }
          state.length = 0;
          state.mode = NAME$2;
        case NAME$2:
          if (state.flags & 2048) {
            if (have === 0) {
              break inf_leave;
            }
            copy = 0;
            do {
              len = input[next + copy++];
              if (state.head && len && state.length < 65536) {
                state.head.name += String.fromCharCode(len);
              }
            } while (len && copy < have);
            if (state.flags & 512) {
              state.check = crc32_1$2(state.check, input, copy, next);
            }
            have -= copy;
            next += copy;
            if (len) {
              break inf_leave;
            }
          } else if (state.head) {
            state.head.name = null;
          }
          state.length = 0;
          state.mode = COMMENT$2;
        case COMMENT$2:
          if (state.flags & 4096) {
            if (have === 0) {
              break inf_leave;
            }
            copy = 0;
            do {
              len = input[next + copy++];
              if (state.head && len && state.length < 65536) {
                state.head.comment += String.fromCharCode(len);
              }
            } while (len && copy < have);
            if (state.flags & 512) {
              state.check = crc32_1$2(state.check, input, copy, next);
            }
            have -= copy;
            next += copy;
            if (len) {
              break inf_leave;
            }
          } else if (state.head) {
            state.head.comment = null;
          }
          state.mode = HCRC$2;
        case HCRC$2:
          if (state.flags & 512) {
            while (bits < 16) {
              if (have === 0) {
                break inf_leave;
              }
              have--;
              hold += input[next++] << bits;
              bits += 8;
            }
            if (hold !== (state.check & 65535)) {
              strm.msg = "header crc mismatch";
              state.mode = BAD$3;
              break;
            }
            hold = 0;
            bits = 0;
          }
          if (state.head) {
            state.head.hcrc = state.flags >> 9 & 1;
            state.head.done = true;
          }
          strm.adler = state.check = 0;
          state.mode = TYPE$3;
          break;
        case DICTID$2:
          while (bits < 32) {
            if (have === 0) {
              break inf_leave;
            }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          strm.adler = state.check = zswap32$2(hold);
          hold = 0;
          bits = 0;
          state.mode = DICT$2;
        case DICT$2:
          if (state.havedict === 0) {
            strm.next_out = put;
            strm.avail_out = left;
            strm.next_in = next;
            strm.avail_in = have;
            state.hold = hold;
            state.bits = bits;
            return Z_NEED_DICT$1$2;
          }
          strm.adler = state.check = 1;
          state.mode = TYPE$3;
        case TYPE$3:
          if (flush === Z_BLOCK$3 || flush === Z_TREES$2) {
            break inf_leave;
          }
        case TYPEDO$2:
          if (state.last) {
            hold >>>= bits & 7;
            bits -= bits & 7;
            state.mode = CHECK$2;
            break;
          }
          while (bits < 3) {
            if (have === 0) {
              break inf_leave;
            }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          state.last = hold & 1;
          hold >>>= 1;
          bits -= 1;
          switch (hold & 3) {
            case 0:
              state.mode = STORED$2;
              break;
            case 1:
              fixedtables$2(state);
              state.mode = LEN_$2;
              if (flush === Z_TREES$2) {
                hold >>>= 2;
                bits -= 2;
                break inf_leave;
              }
              break;
            case 2:
              state.mode = TABLE$2;
              break;
            case 3:
              strm.msg = "invalid block type";
              state.mode = BAD$3;
          }
          hold >>>= 2;
          bits -= 2;
          break;
        case STORED$2:
          hold >>>= bits & 7;
          bits -= bits & 7;
          while (bits < 32) {
            if (have === 0) {
              break inf_leave;
            }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          if ((hold & 65535) !== (hold >>> 16 ^ 65535)) {
            strm.msg = "invalid stored block lengths";
            state.mode = BAD$3;
            break;
          }
          state.length = hold & 65535;
          hold = 0;
          bits = 0;
          state.mode = COPY_$2;
          if (flush === Z_TREES$2) {
            break inf_leave;
          }
        case COPY_$2:
          state.mode = COPY$2;
        case COPY$2:
          copy = state.length;
          if (copy) {
            if (copy > have) {
              copy = have;
            }
            if (copy > left) {
              copy = left;
            }
            if (copy === 0) {
              break inf_leave;
            }
            output.set(input.subarray(next, next + copy), put);
            have -= copy;
            next += copy;
            left -= copy;
            put += copy;
            state.length -= copy;
            break;
          }
          state.mode = TYPE$3;
          break;
        case TABLE$2:
          while (bits < 14) {
            if (have === 0) {
              break inf_leave;
            }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          state.nlen = (hold & 31) + 257;
          hold >>>= 5;
          bits -= 5;
          state.ndist = (hold & 31) + 1;
          hold >>>= 5;
          bits -= 5;
          state.ncode = (hold & 15) + 4;
          hold >>>= 4;
          bits -= 4;
          if (state.nlen > 286 || state.ndist > 30) {
            strm.msg = "too many length or distance symbols";
            state.mode = BAD$3;
            break;
          }
          state.have = 0;
          state.mode = LENLENS$2;
        case LENLENS$2:
          while (state.have < state.ncode) {
            while (bits < 3) {
              if (have === 0) {
                break inf_leave;
              }
              have--;
              hold += input[next++] << bits;
              bits += 8;
            }
            state.lens[order[state.have++]] = hold & 7;
            hold >>>= 3;
            bits -= 3;
          }
          while (state.have < 19) {
            state.lens[order[state.have++]] = 0;
          }
          state.lencode = state.lendyn;
          state.lenbits = 7;
          opts = { bits: state.lenbits };
          ret = inftrees$2(CODES$3, state.lens, 0, 19, state.lencode, 0, state.work, opts);
          state.lenbits = opts.bits;
          if (ret) {
            strm.msg = "invalid code lengths set";
            state.mode = BAD$3;
            break;
          }
          state.have = 0;
          state.mode = CODELENS$2;
        case CODELENS$2:
          while (state.have < state.nlen + state.ndist) {
            for (; ; ) {
              here = state.lencode[hold & (1 << state.lenbits) - 1];
              here_bits = here >>> 24;
              here_op = here >>> 16 & 255;
              here_val = here & 65535;
              if (here_bits <= bits) {
                break;
              }
              if (have === 0) {
                break inf_leave;
              }
              have--;
              hold += input[next++] << bits;
              bits += 8;
            }
            if (here_val < 16) {
              hold >>>= here_bits;
              bits -= here_bits;
              state.lens[state.have++] = here_val;
            } else {
              if (here_val === 16) {
                n = here_bits + 2;
                while (bits < n) {
                  if (have === 0) {
                    break inf_leave;
                  }
                  have--;
                  hold += input[next++] << bits;
                  bits += 8;
                }
                hold >>>= here_bits;
                bits -= here_bits;
                if (state.have === 0) {
                  strm.msg = "invalid bit length repeat";
                  state.mode = BAD$3;
                  break;
                }
                len = state.lens[state.have - 1];
                copy = 3 + (hold & 3);
                hold >>>= 2;
                bits -= 2;
              } else if (here_val === 17) {
                n = here_bits + 3;
                while (bits < n) {
                  if (have === 0) {
                    break inf_leave;
                  }
                  have--;
                  hold += input[next++] << bits;
                  bits += 8;
                }
                hold >>>= here_bits;
                bits -= here_bits;
                len = 0;
                copy = 3 + (hold & 7);
                hold >>>= 3;
                bits -= 3;
              } else {
                n = here_bits + 7;
                while (bits < n) {
                  if (have === 0) {
                    break inf_leave;
                  }
                  have--;
                  hold += input[next++] << bits;
                  bits += 8;
                }
                hold >>>= here_bits;
                bits -= here_bits;
                len = 0;
                copy = 11 + (hold & 127);
                hold >>>= 7;
                bits -= 7;
              }
              if (state.have + copy > state.nlen + state.ndist) {
                strm.msg = "invalid bit length repeat";
                state.mode = BAD$3;
                break;
              }
              while (copy--) {
                state.lens[state.have++] = len;
              }
            }
          }
          if (state.mode === BAD$3) {
            break;
          }
          if (state.lens[256] === 0) {
            strm.msg = "invalid code -- missing end-of-block";
            state.mode = BAD$3;
            break;
          }
          state.lenbits = 9;
          opts = { bits: state.lenbits };
          ret = inftrees$2(LENS$3, state.lens, 0, state.nlen, state.lencode, 0, state.work, opts);
          state.lenbits = opts.bits;
          if (ret) {
            strm.msg = "invalid literal/lengths set";
            state.mode = BAD$3;
            break;
          }
          state.distbits = 6;
          state.distcode = state.distdyn;
          opts = { bits: state.distbits };
          ret = inftrees$2(DISTS$3, state.lens, state.nlen, state.ndist, state.distcode, 0, state.work, opts);
          state.distbits = opts.bits;
          if (ret) {
            strm.msg = "invalid distances set";
            state.mode = BAD$3;
            break;
          }
          state.mode = LEN_$2;
          if (flush === Z_TREES$2) {
            break inf_leave;
          }
        case LEN_$2:
          state.mode = LEN$2;
        case LEN$2:
          if (have >= 6 && left >= 258) {
            strm.next_out = put;
            strm.avail_out = left;
            strm.next_in = next;
            strm.avail_in = have;
            state.hold = hold;
            state.bits = bits;
            inffast$2(strm, _out);
            put = strm.next_out;
            output = strm.output;
            left = strm.avail_out;
            next = strm.next_in;
            input = strm.input;
            have = strm.avail_in;
            hold = state.hold;
            bits = state.bits;
            if (state.mode === TYPE$3) {
              state.back = -1;
            }
            break;
          }
          state.back = 0;
          for (; ; ) {
            here = state.lencode[hold & (1 << state.lenbits) - 1];
            here_bits = here >>> 24;
            here_op = here >>> 16 & 255;
            here_val = here & 65535;
            if (here_bits <= bits) {
              break;
            }
            if (have === 0) {
              break inf_leave;
            }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          if (here_op && (here_op & 240) === 0) {
            last_bits = here_bits;
            last_op = here_op;
            last_val = here_val;
            for (; ; ) {
              here = state.lencode[last_val + ((hold & (1 << last_bits + last_op) - 1) >> last_bits)];
              here_bits = here >>> 24;
              here_op = here >>> 16 & 255;
              here_val = here & 65535;
              if (last_bits + here_bits <= bits) {
                break;
              }
              if (have === 0) {
                break inf_leave;
              }
              have--;
              hold += input[next++] << bits;
              bits += 8;
            }
            hold >>>= last_bits;
            bits -= last_bits;
            state.back += last_bits;
          }
          hold >>>= here_bits;
          bits -= here_bits;
          state.back += here_bits;
          state.length = here_val;
          if (here_op === 0) {
            state.mode = LIT$2;
            break;
          }
          if (here_op & 32) {
            state.back = -1;
            state.mode = TYPE$3;
            break;
          }
          if (here_op & 64) {
            strm.msg = "invalid literal/length code";
            state.mode = BAD$3;
            break;
          }
          state.extra = here_op & 15;
          state.mode = LENEXT$2;
        case LENEXT$2:
          if (state.extra) {
            n = state.extra;
            while (bits < n) {
              if (have === 0) {
                break inf_leave;
              }
              have--;
              hold += input[next++] << bits;
              bits += 8;
            }
            state.length += hold & (1 << state.extra) - 1;
            hold >>>= state.extra;
            bits -= state.extra;
            state.back += state.extra;
          }
          state.was = state.length;
          state.mode = DIST$2;
        case DIST$2:
          for (; ; ) {
            here = state.distcode[hold & (1 << state.distbits) - 1];
            here_bits = here >>> 24;
            here_op = here >>> 16 & 255;
            here_val = here & 65535;
            if (here_bits <= bits) {
              break;
            }
            if (have === 0) {
              break inf_leave;
            }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          if ((here_op & 240) === 0) {
            last_bits = here_bits;
            last_op = here_op;
            last_val = here_val;
            for (; ; ) {
              here = state.distcode[last_val + ((hold & (1 << last_bits + last_op) - 1) >> last_bits)];
              here_bits = here >>> 24;
              here_op = here >>> 16 & 255;
              here_val = here & 65535;
              if (last_bits + here_bits <= bits) {
                break;
              }
              if (have === 0) {
                break inf_leave;
              }
              have--;
              hold += input[next++] << bits;
              bits += 8;
            }
            hold >>>= last_bits;
            bits -= last_bits;
            state.back += last_bits;
          }
          hold >>>= here_bits;
          bits -= here_bits;
          state.back += here_bits;
          if (here_op & 64) {
            strm.msg = "invalid distance code";
            state.mode = BAD$3;
            break;
          }
          state.offset = here_val;
          state.extra = here_op & 15;
          state.mode = DISTEXT$2;
        case DISTEXT$2:
          if (state.extra) {
            n = state.extra;
            while (bits < n) {
              if (have === 0) {
                break inf_leave;
              }
              have--;
              hold += input[next++] << bits;
              bits += 8;
            }
            state.offset += hold & (1 << state.extra) - 1;
            hold >>>= state.extra;
            bits -= state.extra;
            state.back += state.extra;
          }
          if (state.offset > state.dmax) {
            strm.msg = "invalid distance too far back";
            state.mode = BAD$3;
            break;
          }
          state.mode = MATCH$2;
        case MATCH$2:
          if (left === 0) {
            break inf_leave;
          }
          copy = _out - left;
          if (state.offset > copy) {
            copy = state.offset - copy;
            if (copy > state.whave) {
              if (state.sane) {
                strm.msg = "invalid distance too far back";
                state.mode = BAD$3;
                break;
              }
            }
            if (copy > state.wnext) {
              copy -= state.wnext;
              from = state.wsize - copy;
            } else {
              from = state.wnext - copy;
            }
            if (copy > state.length) {
              copy = state.length;
            }
            from_source = state.window;
          } else {
            from_source = output;
            from = put - state.offset;
            copy = state.length;
          }
          if (copy > left) {
            copy = left;
          }
          left -= copy;
          state.length -= copy;
          do {
            output[put++] = from_source[from++];
          } while (--copy);
          if (state.length === 0) {
            state.mode = LEN$2;
          }
          break;
        case LIT$2:
          if (left === 0) {
            break inf_leave;
          }
          output[put++] = state.length;
          left--;
          state.mode = LEN$2;
          break;
        case CHECK$2:
          if (state.wrap) {
            while (bits < 32) {
              if (have === 0) {
                break inf_leave;
              }
              have--;
              hold |= input[next++] << bits;
              bits += 8;
            }
            _out -= left;
            strm.total_out += _out;
            state.total += _out;
            if (_out) {
              strm.adler = state.check = state.flags ? crc32_1$2(state.check, output, _out, put - _out) : adler32_1$2(state.check, output, _out, put - _out);
            }
            _out = left;
            if ((state.flags ? hold : zswap32$2(hold)) !== state.check) {
              strm.msg = "incorrect data check";
              state.mode = BAD$3;
              break;
            }
            hold = 0;
            bits = 0;
          }
          state.mode = LENGTH$2;
        case LENGTH$2:
          if (state.wrap && state.flags) {
            while (bits < 32) {
              if (have === 0) {
                break inf_leave;
              }
              have--;
              hold += input[next++] << bits;
              bits += 8;
            }
            if (hold !== (state.total & 4294967295)) {
              strm.msg = "incorrect length check";
              state.mode = BAD$3;
              break;
            }
            hold = 0;
            bits = 0;
          }
          state.mode = DONE$2;
        case DONE$2:
          ret = Z_STREAM_END$1$2;
          break inf_leave;
        case BAD$3:
          ret = Z_DATA_ERROR$1$2;
          break inf_leave;
        case MEM$2:
          return Z_MEM_ERROR$1$2;
        case SYNC$2:
        default:
          return Z_STREAM_ERROR$1$2;
      }
    }
  strm.next_out = put;
  strm.avail_out = left;
  strm.next_in = next;
  strm.avail_in = have;
  state.hold = hold;
  state.bits = bits;
  if (state.wsize || _out !== strm.avail_out && state.mode < BAD$3 && (state.mode < CHECK$2 || flush !== Z_FINISH$1$2)) {
    if (updatewindow$2(strm, strm.output, strm.next_out, _out - strm.avail_out))
      ;
  }
  _in -= strm.avail_in;
  _out -= strm.avail_out;
  strm.total_in += _in;
  strm.total_out += _out;
  state.total += _out;
  if (state.wrap && _out) {
    strm.adler = state.check = state.flags ? crc32_1$2(state.check, output, _out, strm.next_out - _out) : adler32_1$2(state.check, output, _out, strm.next_out - _out);
  }
  strm.data_type = state.bits + (state.last ? 64 : 0) + (state.mode === TYPE$3 ? 128 : 0) + (state.mode === LEN_$2 || state.mode === COPY_$2 ? 256 : 0);
  if ((_in === 0 && _out === 0 || flush === Z_FINISH$1$2) && ret === Z_OK$1$2) {
    ret = Z_BUF_ERROR$3;
  }
  return ret;
};
var inflateEnd$2 = (strm) => {
  if (!strm || !strm.state) {
    return Z_STREAM_ERROR$1$2;
  }
  let state = strm.state;
  if (state.window) {
    state.window = null;
  }
  strm.state = null;
  return Z_OK$1$2;
};
var inflateGetHeader$2 = (strm, head) => {
  if (!strm || !strm.state) {
    return Z_STREAM_ERROR$1$2;
  }
  const state = strm.state;
  if ((state.wrap & 2) === 0) {
    return Z_STREAM_ERROR$1$2;
  }
  state.head = head;
  head.done = false;
  return Z_OK$1$2;
};
var inflateSetDictionary$2 = (strm, dictionary) => {
  const dictLength = dictionary.length;
  let state;
  let dictid;
  let ret;
  if (!strm || !strm.state) {
    return Z_STREAM_ERROR$1$2;
  }
  state = strm.state;
  if (state.wrap !== 0 && state.mode !== DICT$2) {
    return Z_STREAM_ERROR$1$2;
  }
  if (state.mode === DICT$2) {
    dictid = 1;
    dictid = adler32_1$2(dictid, dictionary, dictLength, 0);
    if (dictid !== state.check) {
      return Z_DATA_ERROR$1$2;
    }
  }
  ret = updatewindow$2(strm, dictionary, dictLength, dictLength);
  if (ret) {
    state.mode = MEM$2;
    return Z_MEM_ERROR$1$2;
  }
  state.havedict = 1;
  return Z_OK$1$2;
};
var inflateReset_1$2 = inflateReset$2;
var inflateReset2_1$2 = inflateReset2$2;
var inflateResetKeep_1$2 = inflateResetKeep$2;
var inflateInit_1$2 = inflateInit$2;
var inflateInit2_1$2 = inflateInit2$2;
var inflate_2$1$2 = inflate$2$2;
var inflateEnd_1$2 = inflateEnd$2;
var inflateGetHeader_1$2 = inflateGetHeader$2;
var inflateSetDictionary_1$2 = inflateSetDictionary$2;
var inflateInfo$2 = "pako inflate (from Nodeca project)";
var inflate_1$2$2 = {
  inflateReset: inflateReset_1$2,
  inflateReset2: inflateReset2_1$2,
  inflateResetKeep: inflateResetKeep_1$2,
  inflateInit: inflateInit_1$2,
  inflateInit2: inflateInit2_1$2,
  inflate: inflate_2$1$2,
  inflateEnd: inflateEnd_1$2,
  inflateGetHeader: inflateGetHeader_1$2,
  inflateSetDictionary: inflateSetDictionary_1$2,
  inflateInfo: inflateInfo$2
};
function GZheader$2() {
  this.text = 0;
  this.time = 0;
  this.xflags = 0;
  this.os = 0;
  this.extra = null;
  this.extra_len = 0;
  this.name = "";
  this.comment = "";
  this.hcrc = 0;
  this.done = false;
}
var gzheader$2 = GZheader$2;
var toString$5 = Object.prototype.toString;
var {
  Z_NO_FLUSH: Z_NO_FLUSH$4,
  Z_FINISH: Z_FINISH$5,
  Z_OK: Z_OK$5,
  Z_STREAM_END: Z_STREAM_END$5,
  Z_NEED_DICT: Z_NEED_DICT$3,
  Z_STREAM_ERROR: Z_STREAM_ERROR$4,
  Z_DATA_ERROR: Z_DATA_ERROR$4,
  Z_MEM_ERROR: Z_MEM_ERROR$3
} = constants$2$2;
function Inflate$1$2(options) {
  this.options = common$2.assign({
    chunkSize: 1024 * 64,
    windowBits: 15,
    to: ""
  }, options || {});
  const opt = this.options;
  if (opt.raw && opt.windowBits >= 0 && opt.windowBits < 16) {
    opt.windowBits = -opt.windowBits;
    if (opt.windowBits === 0) {
      opt.windowBits = -15;
    }
  }
  if (opt.windowBits >= 0 && opt.windowBits < 16 && !(options && options.windowBits)) {
    opt.windowBits += 32;
  }
  if (opt.windowBits > 15 && opt.windowBits < 48) {
    if ((opt.windowBits & 15) === 0) {
      opt.windowBits |= 15;
    }
  }
  this.err = 0;
  this.msg = "";
  this.ended = false;
  this.chunks = [];
  this.strm = new zstream$2();
  this.strm.avail_out = 0;
  let status = inflate_1$2$2.inflateInit2(this.strm, opt.windowBits);
  if (status !== Z_OK$5) {
    throw new Error(messages$2[status]);
  }
  this.header = new gzheader$2();
  inflate_1$2$2.inflateGetHeader(this.strm, this.header);
  if (opt.dictionary) {
    if (typeof opt.dictionary === "string") {
      opt.dictionary = strings$2.string2buf(opt.dictionary);
    } else if (toString$5.call(opt.dictionary) === "[object ArrayBuffer]") {
      opt.dictionary = new Uint8Array(opt.dictionary);
    }
    if (opt.raw) {
      status = inflate_1$2$2.inflateSetDictionary(this.strm, opt.dictionary);
      if (status !== Z_OK$5) {
        throw new Error(messages$2[status]);
      }
    }
  }
}
Inflate$1$2.prototype.push = function(data, flush_mode) {
  const strm = this.strm;
  const chunkSize = this.options.chunkSize;
  const dictionary = this.options.dictionary;
  let status, _flush_mode, last_avail_out;
  if (this.ended)
    return false;
  if (flush_mode === ~~flush_mode)
    _flush_mode = flush_mode;
  else
    _flush_mode = flush_mode === true ? Z_FINISH$5 : Z_NO_FLUSH$4;
  if (toString$5.call(data) === "[object ArrayBuffer]") {
    strm.input = new Uint8Array(data);
  } else {
    strm.input = data;
  }
  strm.next_in = 0;
  strm.avail_in = strm.input.length;
  for (; ; ) {
    if (strm.avail_out === 0) {
      strm.output = new Uint8Array(chunkSize);
      strm.next_out = 0;
      strm.avail_out = chunkSize;
    }
    status = inflate_1$2$2.inflate(strm, _flush_mode);
    if (status === Z_NEED_DICT$3 && dictionary) {
      status = inflate_1$2$2.inflateSetDictionary(strm, dictionary);
      if (status === Z_OK$5) {
        status = inflate_1$2$2.inflate(strm, _flush_mode);
      } else if (status === Z_DATA_ERROR$4) {
        status = Z_NEED_DICT$3;
      }
    }
    while (strm.avail_in > 0 && status === Z_STREAM_END$5 && strm.state.wrap > 0 && data[strm.next_in] !== 0) {
      inflate_1$2$2.inflateReset(strm);
      status = inflate_1$2$2.inflate(strm, _flush_mode);
    }
    switch (status) {
      case Z_STREAM_ERROR$4:
      case Z_DATA_ERROR$4:
      case Z_NEED_DICT$3:
      case Z_MEM_ERROR$3:
        this.onEnd(status);
        this.ended = true;
        return false;
    }
    last_avail_out = strm.avail_out;
    if (strm.next_out) {
      if (strm.avail_out === 0 || status === Z_STREAM_END$5) {
        if (this.options.to === "string") {
          let next_out_utf8 = strings$2.utf8border(strm.output, strm.next_out);
          let tail = strm.next_out - next_out_utf8;
          let utf8str = strings$2.buf2string(strm.output, next_out_utf8);
          strm.next_out = tail;
          strm.avail_out = chunkSize - tail;
          if (tail)
            strm.output.set(strm.output.subarray(next_out_utf8, next_out_utf8 + tail), 0);
          this.onData(utf8str);
        } else {
          this.onData(strm.output.length === strm.next_out ? strm.output : strm.output.subarray(0, strm.next_out));
        }
      }
    }
    if (status === Z_OK$5 && last_avail_out === 0)
      continue;
    if (status === Z_STREAM_END$5) {
      status = inflate_1$2$2.inflateEnd(this.strm);
      this.onEnd(status);
      this.ended = true;
      return true;
    }
    if (strm.avail_in === 0)
      break;
  }
  return true;
};
Inflate$1$2.prototype.onData = function(chunk) {
  this.chunks.push(chunk);
};
Inflate$1$2.prototype.onEnd = function(status) {
  if (status === Z_OK$5) {
    if (this.options.to === "string") {
      this.result = this.chunks.join("");
    } else {
      this.result = common$2.flattenChunks(this.chunks);
    }
  }
  this.chunks = [];
  this.err = status;
  this.msg = this.strm.msg;
};
function inflate$1$2(input, options) {
  const inflator = new Inflate$1$2(options);
  inflator.push(input);
  if (inflator.err)
    throw inflator.msg || messages$2[inflator.err];
  return inflator.result;
}
function inflateRaw$1$2(input, options) {
  options = options || {};
  options.raw = true;
  return inflate$1$2(input, options);
}
var Inflate_1$1$2 = Inflate$1$2;
var inflate_2$3 = inflate$1$2;
var inflateRaw_1$1$2 = inflateRaw$1$2;
var ungzip$1$2 = inflate$1$2;
var constants$4 = constants$2$2;
var inflate_1$1$2 = {
  Inflate: Inflate_1$1$2,
  inflate: inflate_2$3,
  inflateRaw: inflateRaw_1$1$2,
  ungzip: ungzip$1$2,
  constants: constants$4
};
var { Inflate: Inflate$3, inflate: inflate$4, inflateRaw: inflateRaw$3, ungzip: ungzip$3 } = inflate_1$1$2;
var inflate_1 = inflate$4;
var ungzip_1$2 = ungzip$3;

// esm/filters.js
var zlib_decompress = function(buf, itemsize) {
  let input_array = new Uint8Array(buf);
  return inflate_1(input_array).buffer;
};
var unshuffle = function(buf, itemsize) {
  let buffer_size = buf.byteLength;
  let unshuffled_view = new Uint8Array(buffer_size);
  let step = Math.floor(buffer_size / itemsize);
  let shuffled_view = new DataView(buf);
  for (var j = 0; j < itemsize; j++) {
    for (var i = 0; i < step; i++) {
      unshuffled_view[j + i * itemsize] = shuffled_view.getUint8(j * step + i);
    }
  }
  return unshuffled_view.buffer;
};
var fletch32 = function(buf, itemsize) {
  _verify_fletcher32(buf);
  return buf.slice(0, -4);
};
function _verify_fletcher32(chunk_buffer) {
  var odd_chunk_buffer = chunk_buffer.byteLength % 2 != 0;
  var data_length = chunk_buffer.byteLength - 4;
  var view = new DataView(chunk_buffer);
  var sum1 = 0;
  var sum2 = 0;
  for (var offset = 0; offset < data_length - 1; offset += 2) {
    let datum = view.getUint16(offset, true);
    sum1 = (sum1 + datum) % 65535;
    sum2 = (sum2 + sum1) % 65535;
  }
  if (odd_chunk_buffer) {
    let datum = view.getUint8(data_length - 1);
    sum1 = (sum1 + datum) % 65535;
    sum2 = (sum2 + sum1) % 65535;
  }
  var [ref_sum1, ref_sum2] = struct.unpack_from(">HH", chunk_buffer, data_length);
  ref_sum1 = ref_sum1 % 65535;
  ref_sum2 = ref_sum2 % 65535;
  if (sum1 != ref_sum1 || sum2 != ref_sum2) {
    throw 'ValueError("fletcher32 checksum invalid")';
  }
  return true;
}
var GZIP_DEFLATE_FILTER = 1;
var SHUFFLE_FILTER = 2;
var FLETCH32_FILTER = 3;
var Filters = /* @__PURE__ */ new Map([
  [GZIP_DEFLATE_FILTER, zlib_decompress],
  [SHUFFLE_FILTER, unshuffle],
  [FLETCH32_FILTER, fletch32]
]);

// esm/btree.js
var AbstractBTree = class {
  constructor(fh, offset) {
    this.fh = fh;
    this.offset = offset;
    this.depth = null;
  }
  async init() {
    this.all_nodes = /* @__PURE__ */ new Map();
    await this._read_root_node();
    await this._read_children();
  }
  async _read_children() {
    let node_level = this.depth;
    while (node_level > 0) {
      for (var parent_node of this.all_nodes.get(node_level)) {
        for (var child_addr of parent_node.get("addresses")) {
          this._add_node(await this._read_node(child_addr, node_level - 1));
        }
      }
      node_level--;
    }
  }
  async _read_root_node() {
    let root_node = await this._read_node(this.offset, null);
    this._add_node(root_node);
    this.depth = root_node.get("node_level");
  }
  _add_node(node) {
    let node_level = node.get("node_level");
    if (this.all_nodes.has(node_level)) {
      this.all_nodes.get(node_level).push(node);
    } else {
      this.all_nodes.set(node_level, [node]);
    }
  }
  async _read_node(offset, node_level) {
    let node = await this._read_node_header(offset, node_level);
    node.set("keys", []);
    node.set("addresses", []);
    return node;
  }
  async _read_node_header(offset) {
    throw "NotImplementedError: must define _read_node_header in implementation class";
  }
};
var BTreeV1 = class extends AbstractBTree {
  constructor() {
    super(...arguments);
    __publicField(this, "B_LINK_NODE", /* @__PURE__ */ new Map([
      ["signature", "4s"],
      ["node_type", "B"],
      ["node_level", "B"],
      ["entries_used", "H"],
      ["left_sibling", "Q"],
      ["right_sibling", "Q"]
    ]));
  }
  async _read_node_header(offset, node_level) {
    let node = await _unpack_struct_from_async(this.B_LINK_NODE, this.fh, offset);
    if (node_level != null) {
      if (node.get("node_level") != node_level) {
        throw "node level does not match";
      }
    }
    return node;
  }
};
var BTreeV1Groups = class extends BTreeV1 {
  constructor(fh, offset) {
    super(fh, offset);
    __publicField(this, "NODE_TYPE", 0);
    this.ready = this.init();
  }
  async _read_node(offset, node_level) {
    let node = await this._read_node_header(offset, node_level);
    offset += _structure_size(this.B_LINK_NODE);
    let keys = [];
    let addresses = [];
    let entries_used = node.get("entries_used");
    for (var i = 0; i < entries_used; i++) {
      let key = (await struct.unpack_from_async("<Q", this.fh, offset))[0];
      offset += 8;
      let address = (await struct.unpack_from_async("<Q", this.fh, offset))[0];
      offset += 8;
      keys.push(key);
      addresses.push(address);
    }
    keys.push((await struct.unpack_from_async("<Q", this.fh, offset))[0]);
    node.set("keys", keys);
    node.set("addresses", addresses);
    return node;
  }
  symbol_table_addresses() {
    var all_address = [];
    var root_nodes = this.all_nodes.get(0);
    for (var node of root_nodes) {
      all_address = all_address.concat(node.get("addresses"));
    }
    return all_address;
  }
};
var BTreeV1RawDataChunks = class extends BTreeV1 {
  constructor(fh, offset, dims) {
    super(fh, offset);
    __publicField(this, "NODE_TYPE", 1);
    this.dims = dims;
    this.ready = this.init();
  }
  async _read_node(offset, node_level) {
    let node = await this._read_node_header(offset, node_level);
    offset += _structure_size(this.B_LINK_NODE);
    var keys = [];
    var addresses = [];
    let entries_used = node.get("entries_used");
    for (var i = 0; i < entries_used; i++) {
      let [chunk_size, filter_mask] = await struct.unpack_from_async("<II", this.fh, offset);
      offset += 8;
      let fmt = "<" + this.dims.toFixed() + "Q";
      let fmt_size = struct.calcsize(fmt);
      let chunk_offset = await struct.unpack_from_async(fmt, this.fh, offset);
      offset += fmt_size;
      let chunk_address = (await struct.unpack_from_async("<Q", this.fh, offset))[0];
      offset += 8;
      keys.push(/* @__PURE__ */ new Map([
        ["chunk_size", chunk_size],
        ["filter_mask", filter_mask],
        ["chunk_offset", chunk_offset]
      ]));
      addresses.push(chunk_address);
    }
    node.set("keys", keys);
    node.set("addresses", addresses);
    return node;
  }
  async construct_data_from_chunks(chunk_shape, data_shape, dtype, filter_pipeline) {
    var item_getter, item_big_endian, item_size;
    if (dtype instanceof Array) {
      let dtype_class = dtype[0];
      if (dtype_class == "REFERENCE") {
        let size = dtype[1];
        if (size != 8) {
          throw "NotImplementedError('Unsupported Reference type')";
        }
        dtype = "<u8";
        item_getter = "getUint64";
        item_big_endian = false;
        item_size = 8;
      } else if (dtype_class == "VLEN_STRING" || dtype_class == "VLEN_SEQUENCE") {
        item_getter = "getVLENStruct";
        item_big_endian = false;
        item_size = 16;
      } else {
        throw "NotImplementedError('datatype not implemented')";
      }
    } else {
      [item_getter, item_big_endian, item_size] = dtype_getter(dtype);
    }
    var data_size = data_shape.reduce(function(a, b) {
      return a * b;
    }, 1);
    var chunk_size = chunk_shape.reduce(function(a, b) {
      return a * b;
    }, 1);
    let dims = data_shape.length;
    var current_stride = 1;
    chunk_shape.slice().map(function(d2) {
      let s = current_stride;
      current_stride *= d2;
      return s;
    });
    var current_stride = 1;
    var data_strides = data_shape.slice().reverse().map(function(d2) {
      let s = current_stride;
      current_stride *= d2;
      return s;
    }).reverse();
    var data = new Array(data_size);
    let chunk_buffer_size = chunk_size * item_size;
    for (var node of this.all_nodes.get(0)) {
      let node_keys = node.get("keys");
      let node_addresses = node.get("addresses");
      let nkeys = node_keys.length;
      for (var ik = 0; ik < nkeys; ik++) {
        let node_key = node_keys[ik];
        let addr = node_addresses[ik];
        var chunk_buffer;
        if (filter_pipeline == null) {
          chunk_buffer = await this.fh.slice(addr, addr + chunk_buffer_size);
        } else {
          chunk_buffer = await this.fh.slice(addr, addr + node_key.get("chunk_size"));
          let filter_mask = node_key.get("filter_mask");
          chunk_buffer = this._filter_chunk(chunk_buffer, filter_mask, filter_pipeline, item_size);
        }
        var chunk_offset = node_key.get("chunk_offset").slice();
        var apos = chunk_offset.slice();
        var cpos = apos.map(function() {
          return 0;
        });
        var cview = new DataView64(chunk_buffer);
        for (var ci = 0; ci < chunk_size; ci++) {
          for (var d = dims - 1; d >= 0; d--) {
            if (cpos[d] >= chunk_shape[d]) {
              cpos[d] = 0;
              apos[d] = chunk_offset[d];
              if (d > 0) {
                cpos[d - 1] += 1;
                apos[d - 1] += 1;
              }
            } else {
              break;
            }
          }
          let inbounds = apos.slice(0, -1).every(function(p, d2) {
            return p < data_shape[d2];
          });
          if (inbounds) {
            let cb_offset = ci * item_size;
            let datum = cview[item_getter](cb_offset, !item_big_endian, item_size);
            let ai = apos.slice(0, -1).reduce(function(prev, curr, index) {
              return curr * data_strides[index] + prev;
            }, 0);
            data[ai] = datum;
          }
          cpos[dims - 1] += 1;
          apos[dims - 1] += 1;
        }
      }
    }
    return data;
  }
  _filter_chunk(chunk_buffer, filter_mask, filter_pipeline, itemsize) {
    let num_filters = filter_pipeline.length;
    let buf = chunk_buffer.slice();
    for (var filter_index = num_filters - 1; filter_index >= 0; filter_index--) {
      if (filter_mask & 1 << filter_index) {
        continue;
      }
      let pipeline_entry = filter_pipeline[filter_index];
      let filter_id = pipeline_entry.get("filter_id");
      let client_data = pipeline_entry.get("client_data");
      if (Filters.has(filter_id)) {
        buf = Filters.get(filter_id)(buf, itemsize, client_data);
      } else {
        throw 'NotImplementedError("Filter with id:' + filter_id.toFixed() + ' not supported")';
      }
    }
    return buf;
  }
};
var BTreeV2 = class extends AbstractBTree {
  constructor(fh, offset) {
    super(fh, offset);
    __publicField(this, "B_TREE_HEADER", /* @__PURE__ */ new Map([
      ["signature", "4s"],
      ["version", "B"],
      ["node_type", "B"],
      ["node_size", "I"],
      ["record_size", "H"],
      ["depth", "H"],
      ["split_percent", "B"],
      ["merge_percent", "B"],
      ["root_address", "Q"],
      ["root_nrecords", "H"],
      ["total_nrecords", "Q"]
    ]));
    __publicField(this, "B_LINK_NODE", /* @__PURE__ */ new Map([
      ["signature", "4s"],
      ["version", "B"],
      ["node_type", "B"]
    ]));
    this.ready = this.init();
  }
  async _read_root_node() {
    let h = await this._read_tree_header(this.offset);
    this.address_formats = this._calculate_address_formats(h);
    this.header = h;
    this.depth = h.get("depth");
    let address = [h.get("root_address"), h.get("root_nrecords"), h.get("total_nrecords")];
    let root_node = await this._read_node(address, this.depth);
    this._add_node(root_node);
  }
  async _read_tree_header(offset) {
    let header = await _unpack_struct_from_async(this.B_TREE_HEADER, this.fh, this.offset);
    return header;
  }
  _calculate_address_formats(header) {
    let node_size = header.get("node_size");
    let record_size = header.get("record_size");
    let nrecords_max = 0;
    let ntotalrecords_max = 0;
    let address_formats = /* @__PURE__ */ new Map();
    let max_depth = header.get("depth");
    for (var node_level = 0; node_level <= max_depth; node_level++) {
      let offset_fmt = "";
      let num1_fmt = "";
      let num2_fmt = "";
      let offset_size, num1_size, num2_size;
      if (node_level == 0) {
        offset_size = 0;
        num1_size = 0;
        num2_size = 0;
      } else if (node_level == 1) {
        offset_size = 8;
        offset_fmt = "<Q";
        num1_size = this._required_bytes(nrecords_max);
        num1_fmt = this._int_format(num1_size);
        num2_size = 0;
      } else {
        offset_size = 8;
        offset_fmt = "<Q";
        num1_size = this._required_bytes(nrecords_max);
        num1_fmt = this._int_format(num1_size);
        num2_size = this._required_bytes(ntotalrecords_max);
        num2_fmt = this._int_format(num2_size);
      }
      address_formats.set(node_level, [
        offset_size,
        num1_size,
        num2_size,
        offset_fmt,
        num1_fmt,
        num2_fmt
      ]);
      if (node_level < max_depth) {
        let addr_size = offset_size + num1_size + num2_size;
        nrecords_max = this._nrecords_max(node_size, record_size, addr_size);
        if (ntotalrecords_max > 0) {
          ntotalrecords_max *= nrecords_max;
        } else {
          ntotalrecords_max = nrecords_max;
        }
      }
    }
    return address_formats;
  }
  _nrecords_max(node_size, record_size, addr_size) {
    return Math.floor((node_size - 10 - addr_size) / (record_size + addr_size));
  }
  _required_bytes(integer) {
    return Math.ceil(bitSize(integer) / 8);
  }
  _int_format(bytelength) {
    return ["<B", "<H", "<I", "<Q"][bytelength - 1];
  }
  async _read_node(address, node_level) {
    let [offset, nrecords, ntotalrecords] = address;
    let node = this._read_node_header(offset, node_level);
    offset += _structure_size(this.B_LINK_NODE);
    let record_size = this.header.get("record_size");
    let keys = [];
    for (let i = 0; i < nrecords; i++) {
      let record = await this._parse_record(this.fh, offset, record_size);
      offset += record_size;
      keys.push(record);
    }
    let addresses = [];
    let fmts = this.address_formats.get(node_level);
    if (node_level != 0) {
      let [offset_size, num1_size, num2_size, offset_fmt, num1_fmt, num2_fmt] = fmts;
      for (let j = 0; j <= nrecords; j++) {
        let address_offset = (await struct.unpack_from_async(offset_fmt, this.fh, offset))[0];
        offset += offset_size;
        let num1 = (await struct.unpack_from_async(num1_fmt, this.fh, offset))[0];
        offset += num1_size;
        let num2 = num1;
        if (num2_size > 0) {
          num2 = (await struct.unpack_from_async(num2_fmt, this.fh, offset))[0];
          offset += num2_size;
        }
        addresses.push([address_offset, num1, num2]);
      }
    }
    node.set("keys", keys);
    node.set("addresses", addresses);
    return node;
  }
  async _read_node_header(offset, node_level) {
    let node = await _unpack_struct_from_async(this.B_LINK_NODE, this.fh, offset);
    node.set("node_level", node_level);
    return node;
  }
  *iter_records() {
    for (let nodelist of this.all_nodes.values()) {
      for (let node of nodelist) {
        for (let key of node.get("keys")) {
          yield key;
        }
      }
    }
  }
  _parse_record(record) {
    throw "NotImplementedError";
  }
};
var BTreeV2GroupNames = class extends BTreeV2 {
  constructor() {
    super(...arguments);
    __publicField(this, "NODE_TYPE", 5);
  }
  async _parse_record(buf, offset, size) {
    let namehash = (await struct.unpack_from_async("<I", buf, offset))[0];
    offset += 4;
    const heapid = await buf.slice(offset, offset + 7);
    return /* @__PURE__ */ new Map([["namehash", namehash], ["heapid", heapid]]);
  }
};
var BTreeV2GroupOrders = class extends BTreeV2 {
  constructor() {
    super(...arguments);
    __publicField(this, "NODE_TYPE", 6);
  }
  async _parse_record(buf, offset, size) {
    let creationorder = (await struct.unpack_from_async("<Q", buf, offset))[0];
    offset += 8;
    const heapid = await buf.slice(offset, offset + 7);
    return /* @__PURE__ */ new Map([["creationorder", creationorder], ["heapid", heapid]]);
  }
};

// esm/misc-low-level.js
var SuperBlock = class {
  constructor(fh, offset) {
    this.ready = this.init(fh, offset);
  }
  async init(fh, offset) {
    let version_hint = await struct.unpack_from_async("<B", fh, offset + 8);
    var contents;
    if (version_hint == 0) {
      contents = await _unpack_struct_from_async(SUPERBLOCK_V0, fh, offset);
      this._end_of_sblock = offset + SUPERBLOCK_V0_SIZE;
    } else if (version_hint == 2 || version_hint == 3) {
      contents = await _unpack_struct_from_async(SUPERBLOCK_V2_V3, fh, offset);
      this._end_of_sblock = offset + SUPERBLOCK_V2_V3_SIZE;
    } else {
      throw "unsupported superblock version: " + version_hint.toFixed();
    }
    if (contents.get("format_signature") != FORMAT_SIGNATURE) {
      throw "Incorrect file signature: " + contents.get("format_signature");
    }
    if (contents.get("offset_size") != 8 || contents.get("length_size") != 8) {
      throw "File uses non-64-bit addressing";
    }
    this.version = contents.get("superblock_version");
    this._contents = contents;
    this._root_symbol_table = null;
    this._fh = fh;
  }
  async get_offset_to_dataobjects() {
    if (this.version == 0) {
      var sym_table = new SymbolTable(this._fh, this._end_of_sblock, true);
      await sym_table.ready;
      this._root_symbol_table = sym_table;
      return sym_table.group_offset;
    } else if (this.version == 2 || this.version == 3) {
      return this._contents.get("root_group_address");
    } else {
      throw "Not implemented version = " + this.version.toFixed();
    }
  }
};
var Heap = class {
  constructor(fh, offset) {
    this.ready = this.init(fh, offset);
  }
  async init(fh, offset) {
    let local_heap = await _unpack_struct_from_async(LOCAL_HEAP, fh, offset);
    assert(local_heap.get("signature") == "HEAP");
    assert(local_heap.get("version") == 0);
    let data_offset = local_heap.get("address_of_data_segment");
    let heap_data = await fh.slice(data_offset, data_offset + local_heap.get("data_segment_size"));
    local_heap.set("heap_data", heap_data);
    this._contents = local_heap;
    this.data = heap_data;
  }
  get_object_name(offset) {
    let end = new Uint8Array(this.data).indexOf(0, offset);
    let name_size = end - offset;
    let name = struct.unpack_from("<" + name_size.toFixed() + "s", this.data, offset)[0];
    return name;
  }
};
var SymbolTable = class {
  constructor(fh, offset, root = false) {
    this.ready = this.init(fh, offset, root);
  }
  async init(fh, offset, root) {
    var node;
    if (root) {
      node = /* @__PURE__ */ new Map([["symbols", 1]]);
    } else {
      node = await _unpack_struct_from_async(SYMBOL_TABLE_NODE, fh, offset);
      if (node.get("signature") != "SNOD") {
        throw "incorrect node type";
      }
      offset += SYMBOL_TABLE_NODE_SIZE;
    }
    var entries = [];
    var n_symbols = node.get("symbols");
    for (var i = 0; i < n_symbols; i++) {
      entries.push(await _unpack_struct_from_async(SYMBOL_TABLE_ENTRY, fh, offset));
      offset += SYMBOL_TABLE_ENTRY_SIZE;
    }
    if (root) {
      this.group_offset = entries[0].get("object_header_address");
    }
    this.entries = entries;
    this._contents = node;
    return this;
  }
  assign_name(heap) {
    this.entries.forEach(function(entry) {
      let offset = entry.get("link_name_offset");
      let link_name = heap.get_object_name(offset);
      entry.set("link_name", link_name);
    });
  }
  get_links(heap) {
    var links = {};
    this.entries.forEach(function(e) {
      let cache_type = e.get("cache_type");
      let link_name = e.get("link_name");
      if (cache_type == 0 || cache_type == 1) {
        links[link_name] = e.get("object_header_address");
      } else if (cache_type == 2) {
        let scratch = e.get("scratch");
        let buf = new ArrayBuffer(4);
        let bufView = new Uint8Array(buf);
        for (var i = 0; i < 4; i++) {
          bufView[i] = scratch.charCodeAt(i);
        }
        let offset = struct.unpack_from("<I", buf, 0)[0];
        links[link_name] = heap.get_object_name(offset);
      }
    });
    return links;
  }
};
var GlobalHeap = class {
  constructor(fh, offset) {
    this.ready = this.init(fh, offset);
  }
  async init(fh, offset) {
    let header = await _unpack_struct_from_async(GLOBAL_HEAP_HEADER, fh, offset);
    offset += GLOBAL_HEAP_HEADER_SIZE;
    let heap_data_size = header.get("collection_size") - GLOBAL_HEAP_HEADER_SIZE;
    let heap_data = await fh.slice(offset, offset + heap_data_size);
    this.heap_data = heap_data;
    this._header = header;
    this._objects = null;
  }
  get objects() {
    if (this._objects == null) {
      this._objects = /* @__PURE__ */ new Map();
      var offset = 0;
      while (offset <= this.heap_data.byteLength - GLOBAL_HEAP_OBJECT_SIZE) {
        let info = _unpack_struct_from(GLOBAL_HEAP_OBJECT, this.heap_data, offset);
        if (info.get("object_index") == 0) {
          break;
        }
        offset += GLOBAL_HEAP_OBJECT_SIZE;
        let obj_data = this.heap_data.slice(offset, offset + info.get("object_size"));
        this._objects.set(info.get("object_index"), obj_data);
        offset += _padded_size(info.get("object_size"));
      }
    }
    return this._objects;
  }
};
var FractalHeap = class {
  constructor(fh, offset) {
    this.fh = fh;
    this.ready = this.init(offset);
  }
  async init(offset) {
    let header = await _unpack_struct_from_async(FRACTAL_HEAP_HEADER, this.fh, offset);
    offset += _structure_size(FRACTAL_HEAP_HEADER);
    assert(header.get("signature") == "FRHP");
    assert(header.get("version") == 0);
    if (header.get("filter_info_size") > 0) {
      throw "Filter info size not supported on FractalHeap";
    }
    if (header.get("btree_address_huge_objects") == UNDEFINED_ADDRESS) {
      header.set("btree_address_huge_objects", null);
    } else {
      throw "Huge objects not implemented in FractalHeap";
    }
    if (header.get("root_block_address") == UNDEFINED_ADDRESS) {
      header.set("root_block_address", null);
    }
    let nbits = header.get("log2_maximum_heap_size");
    let block_offset_size = this._min_size_nbits(nbits);
    let h = /* @__PURE__ */ new Map([
      ["signature", "4s"],
      ["version", "B"],
      ["heap_header_adddress", "Q"],
      ["block_offset", `${block_offset_size}B`]
    ]);
    this.indirect_block_header = new Map(h);
    this.indirect_block_header_size = _structure_size(h);
    if ((header.get("flags") & 2) == 2) {
      h.set("checksum", "I");
    }
    this.direct_block_header = h;
    this.direct_block_header_size = _structure_size(h);
    let maximum_dblock_size = header.get("maximum_direct_block_size");
    this._managed_object_offset_size = this._min_size_nbits(nbits);
    let value = Math.min(maximum_dblock_size, header.get("max_managed_object_size"));
    this._managed_object_length_size = this._min_size_integer(value);
    let start_block_size = header.get("starting_block_size");
    let table_width = header.get("table_width");
    if (!(start_block_size > 0)) {
      throw "Starting block size == 0 not implemented";
    }
    let log2_maximum_dblock_size = Number(Math.floor(Math.log2(maximum_dblock_size)));
    assert(1n << BigInt(log2_maximum_dblock_size) == maximum_dblock_size);
    let log2_start_block_size = Number(Math.floor(Math.log2(start_block_size)));
    assert(1n << BigInt(log2_start_block_size) == start_block_size);
    this._max_direct_nrows = log2_maximum_dblock_size - log2_start_block_size + 2;
    let log2_table_width = Math.floor(Math.log2(table_width));
    assert(1 << log2_table_width == table_width);
    this._indirect_nrows_sub = log2_table_width + log2_start_block_size - 1;
    this.header = header;
    this.nobjects = header.get("managed_object_count") + header.get("huge_object_count") + header.get("tiny_object_count");
    let managed = [];
    let root_address = header.get("root_block_address");
    let nrows = 0;
    if (root_address != null) {
      nrows = header.get("indirect_current_rows_count");
    }
    if (nrows > 0) {
      for await (let data of this._iter_indirect_block(this.fh, root_address, nrows)) {
        managed.push(data);
      }
    } else {
      let data = await this._read_direct_block(this.fh, root_address, start_block_size);
      managed.push(data);
    }
    let data_size = managed.reduce((p, c) => p + c.byteLength, 0);
    let combined = new Uint8Array(data_size);
    let moffset = 0;
    managed.forEach((m) => {
      combined.set(new Uint8Array(m), moffset);
      moffset += m.byteLength;
    });
    this.managed = combined.buffer;
  }
  async _read_direct_block(fh, offset, block_size) {
    let data = await fh.slice(offset, offset + block_size);
    let header = _unpack_struct_from(this.direct_block_header, data);
    assert(header.get("signature") == "FHDB");
    return data;
  }
  get_data(heapid) {
    let firstbyte = struct.unpack_from("<B", heapid, 0)[0];
    let idtype = firstbyte >> 4 & 3;
    let version = firstbyte >> 6;
    let data_offset = 1;
    if (idtype == 0) {
      assert(version == 0);
      let nbytes = this._managed_object_offset_size;
      let offset = _unpack_integer(nbytes, heapid, data_offset);
      data_offset += nbytes;
      nbytes = this._managed_object_length_size;
      let size = _unpack_integer(nbytes, heapid, data_offset);
      return this.managed.slice(offset, offset + size);
    } else if (idtype == 1) {
      throw "tiny objectID not supported in FractalHeap";
    } else if (idtype == 2) {
      throw "huge objectID not supported in FractalHeap";
    } else {
      throw "unknown objectID type in FractalHeap";
    }
  }
  _min_size_integer(integer) {
    return this._min_size_nbits(bitSize(integer));
  }
  _min_size_nbits(nbits) {
    return Math.ceil(nbits / 8);
  }
  async *_iter_indirect_block(fh, offset, nrows) {
    let header = await _unpack_struct_from_async(this.indirect_block_header, fh, offset);
    offset += this.indirect_block_header_size;
    assert(header.get("signature") == "FHIB");
    let block_offset_bytes = header.get("block_offset");
    let block_offset = block_offset_bytes.reduce((p, c, i) => p + (c << i * 8), 0);
    header.set("block_offset", block_offset);
    let [ndirect, nindirect] = this._indirect_info(nrows);
    let direct_blocks = [];
    for (let i = 0; i < ndirect; i++) {
      let address = (await struct.unpack_from_async("<Q", fh, offset))[0];
      offset += 8;
      if (address == UNDEFINED_ADDRESS) {
        break;
      }
      let block_size = this._calc_block_size(i);
      direct_blocks.push([address, block_size]);
    }
    let indirect_blocks = [];
    for (let i = ndirect; i < ndirect + nindirect; i++) {
      let address = (await struct.unpack_from_async("<Q", fh, offset))[0];
      offset += 8;
      if (address == UNDEFINED_ADDRESS) {
        break;
      }
      let block_size = this._calc_block_size(i);
      let nrows2 = this._iblock_nrows_from_block_size(block_size);
      indirect_blocks.push([address, nrows2]);
    }
    for (let [address, block_size] of direct_blocks) {
      let obj = await this._read_direct_block(fh, address, block_size);
      yield obj;
    }
    for (let [address, nrows2] of indirect_blocks) {
      for await (let obj of this._iter_indirect_block(fh, address, nrows2)) {
        yield obj;
      }
    }
  }
  _calc_block_size(iblock) {
    let row = Math.floor(iblock / this.header.get("table_width"));
    return 2 ** Math.max(row - 1, 0) * this.header.get("starting_block_size");
  }
  _iblock_nrows_from_block_size(block_size) {
    let log2_block_size = Math.floor(Math.log2(block_size));
    assert(2 ** log2_block_size == block_size);
    return log2_block_size - this._indirect_nrows_sub;
  }
  _indirect_info(nrows) {
    let table_width = this.header.get("table_width");
    let nobjects = nrows * table_width;
    let ndirect_max = this._max_direct_nrows * table_width;
    let ndirect, nindirect;
    if (nrows <= ndirect_max) {
      ndirect = nobjects;
      nindirect = 0;
    } else {
      ndirect = ndirect_max;
      nindirect = nobjects - ndirect_max;
    }
    return [ndirect, nindirect];
  }
  _int_format(bytelength) {
    return ["B", "H", "I", "Q"][bytelength - 1];
  }
};
var FORMAT_SIGNATURE = struct.unpack_from("8s", new Uint8Array([137, 72, 68, 70, 13, 10, 26, 10]).buffer)[0];
var UNDEFINED_ADDRESS = struct.unpack_from("<Q", new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255]).buffer)[0];
var SUPERBLOCK_V0 = /* @__PURE__ */ new Map([
  ["format_signature", "8s"],
  ["superblock_version", "B"],
  ["free_storage_version", "B"],
  ["root_group_version", "B"],
  ["reserved_0", "B"],
  ["shared_header_version", "B"],
  ["offset_size", "B"],
  ["length_size", "B"],
  ["reserved_1", "B"],
  ["group_leaf_node_k", "H"],
  ["group_internal_node_k", "H"],
  ["file_consistency_flags", "L"],
  ["base_address_lower", "Q"],
  ["free_space_address", "Q"],
  ["end_of_file_address", "Q"],
  ["driver_information_address", "Q"]
]);
var SUPERBLOCK_V0_SIZE = _structure_size(SUPERBLOCK_V0);
var SUPERBLOCK_V2_V3 = /* @__PURE__ */ new Map([
  ["format_signature", "8s"],
  ["superblock_version", "B"],
  ["offset_size", "B"],
  ["length_size", "B"],
  ["file_consistency_flags", "B"],
  ["base_address", "Q"],
  ["superblock_extension_address", "Q"],
  ["end_of_file_address", "Q"],
  ["root_group_address", "Q"],
  ["superblock_checksum", "I"]
]);
var SUPERBLOCK_V2_V3_SIZE = _structure_size(SUPERBLOCK_V2_V3);
var SYMBOL_TABLE_ENTRY = /* @__PURE__ */ new Map([
  ["link_name_offset", "Q"],
  ["object_header_address", "Q"],
  ["cache_type", "I"],
  ["reserved", "I"],
  ["scratch", "16s"]
]);
var SYMBOL_TABLE_ENTRY_SIZE = _structure_size(SYMBOL_TABLE_ENTRY);
var SYMBOL_TABLE_NODE = /* @__PURE__ */ new Map([
  ["signature", "4s"],
  ["version", "B"],
  ["reserved_0", "B"],
  ["symbols", "H"]
]);
var SYMBOL_TABLE_NODE_SIZE = _structure_size(SYMBOL_TABLE_NODE);
var LOCAL_HEAP = /* @__PURE__ */ new Map([
  ["signature", "4s"],
  ["version", "B"],
  ["reserved", "3s"],
  ["data_segment_size", "Q"],
  ["offset_to_free_list", "Q"],
  ["address_of_data_segment", "Q"]
]);
var GLOBAL_HEAP_HEADER = /* @__PURE__ */ new Map([
  ["signature", "4s"],
  ["version", "B"],
  ["reserved", "3s"],
  ["collection_size", "Q"]
]);
var GLOBAL_HEAP_HEADER_SIZE = _structure_size(GLOBAL_HEAP_HEADER);
var GLOBAL_HEAP_OBJECT = /* @__PURE__ */ new Map([
  ["object_index", "H"],
  ["reference_count", "H"],
  ["reserved", "I"],
  ["object_size", "Q"]
]);
var GLOBAL_HEAP_OBJECT_SIZE = _structure_size(GLOBAL_HEAP_OBJECT);
var FRACTAL_HEAP_HEADER = /* @__PURE__ */ new Map([
  ["signature", "4s"],
  ["version", "B"],
  ["object_index_size", "H"],
  ["filter_info_size", "H"],
  ["flags", "B"],
  ["max_managed_object_size", "I"],
  ["next_huge_object_index", "Q"],
  ["btree_address_huge_objects", "Q"],
  ["managed_freespace_size", "Q"],
  ["freespace_manager_address", "Q"],
  ["managed_space_size", "Q"],
  ["managed_alloc_size", "Q"],
  ["next_directblock_iterator_address", "Q"],
  ["managed_object_count", "Q"],
  ["huge_objects_total_size", "Q"],
  ["huge_object_count", "Q"],
  ["tiny_objects_total_size", "Q"],
  ["tiny_object_count", "Q"],
  ["table_width", "H"],
  ["starting_block_size", "Q"],
  ["maximum_direct_block_size", "Q"],
  ["log2_maximum_heap_size", "H"],
  ["indirect_starting_rows_count", "H"],
  ["root_block_address", "Q"],
  ["indirect_current_rows_count", "H"]
]);

// esm/dataobjects.js
var DataObjects = class {
  constructor(fh, offset) {
    this.ready = this.init(fh, offset);
  }
  async init(fh, offset) {
    let version_hint = (await struct.unpack_from_async("<B", fh, offset))[0];
    if (version_hint == 1) {
      var [msgs, msg_data, header] = await this._parse_v1_objects(fh, offset);
    } else if (version_hint == "O".charCodeAt(0)) {
      var [msgs, msg_data, header] = await this._parse_v2_objects(fh, offset);
    } else {
      throw "InvalidHDF5File('unknown Data Object Header')";
    }
    this.fh = fh;
    this.msgs = msgs;
    this.msg_data = msg_data;
    this.offset = offset;
    this._global_heaps = {};
    this._header = header;
    this._filter_pipeline = null;
    this._chunk_params_set = false;
    this._chunks = null;
    this._chunk_dims = null;
    this._chunk_address = null;
  }
  get dtype() {
    let msg = this.find_msg_type(DATATYPE_MSG_TYPE)[0];
    let msg_offset = msg.get("offset_to_message");
    return new DatatypeMessage(this.fh, msg_offset).dtype;
  }
  get chunks() {
    return this._get_chunk_params().then(() => {
      return this._chunks;
    });
  }
  get shape() {
    let msg = this.find_msg_type(DATASPACE_MSG_TYPE)[0];
    let msg_offset = msg.get("offset_to_message");
    return determine_data_shape(this.fh, msg_offset);
  }
  async get_filter_pipeline() {
    if (this._filter_pipeline != null) {
      return this._filter_pipeline;
    }
    let filter_msgs = this.find_msg_type(DATA_STORAGE_FILTER_PIPELINE_MSG_TYPE);
    if (!filter_msgs.length) {
      this._filter_pipeline = null;
      return this._filter_pipeline;
    }
    var offset = filter_msgs[0].get("offset_to_message");
    let [version, nfilters] = await struct.unpack_from_async("<BB", this.fh, offset);
    offset += struct.calcsize("<BB");
    var filters = [];
    if (version == 1) {
      await struct.unpack_from_async("<HI", this.fh, offset);
      offset += struct.calcsize("<HI");
      for (var _ = 0; _ < nfilters; _++) {
        let filter_info = await _unpack_struct_from_async(FILTER_PIPELINE_DESCR_V1, this.fh, offset);
        offset += FILTER_PIPELINE_DESCR_V1_SIZE;
        let padded_name_length = _padded_size(filter_info.get("name_length"), 8);
        let fmt = "<" + padded_name_length.toFixed() + "s";
        let filter_name = (await struct.unpack_from_async(fmt, this.fh, offset))[0];
        filter_info.set("filter_name", filter_name);
        offset += padded_name_length;
        fmt = "<" + filter_info.get("client_data_values").toFixed() + "I";
        let client_data = await struct.unpack_from_async(fmt, this.fh, offset);
        filter_info.set("client_data", client_data);
        offset += 4 * filter_info.get("client_data_values");
        if (filter_info.get("client_data_values") % 2) {
          offset += 4;
        }
        filters.push(filter_info);
      }
    } else if (version == 2) {
      for (let nf = 0; nf < nfilters; nf++) {
        let filter_info = /* @__PURE__ */ new Map();
        let buf = this.fh;
        let filter_id = (await struct.unpack_from_async("<H", buf, offset))[0];
        offset += 2;
        filter_info.set("filter_id", filter_id);
        let name_length = 0;
        if (filter_id > 255) {
          name_length = (await struct.unpack_from_async("<H", buf, offset))[0];
          offset += 2;
        }
        let flags = (await struct.unpack_from_async("<H", buf, offset))[0];
        offset += 2;
        let optional = (flags & 1) > 0;
        filter_info.set("optional", optional);
        let num_client_values = (await struct.unpack_from_async("<H", buf, offset))[0];
        offset += 2;
        let name;
        if (name_length > 0) {
          name = (await struct.unpack_from_async(`${name_length}s`, buf, offset))[0];
          offset += name_length;
        }
        filter_info.set("name", name);
        let client_values = await struct.unpack_from_async(`<${num_client_values}i`, buf, offset);
        offset += 4 * num_client_values;
        filter_info.set("client_data_values", client_values);
        filters.push(filter_info);
      }
    } else {
      throw `version ${version} is not supported`;
    }
    this._filter_pipeline = filters;
    return this._filter_pipeline;
  }
  find_msg_type(msg_type) {
    return this.msgs.filter(function(m) {
      return m.get("type") == msg_type;
    });
  }
  async get_attributes() {
    let attrs = {};
    let attr_msgs = this.find_msg_type(ATTRIBUTE_MSG_TYPE);
    for (let msg of attr_msgs) {
      let offset = msg.get("offset_to_message");
      let [name, value] = await this.unpack_attribute(offset);
      attrs[name] = value;
    }
    return attrs;
  }
  async get_fillvalue() {
    let msg = this.find_msg_type(FILLVALUE_MSG_TYPE)[0];
    var offset = msg.get("offset_to_message");
    var is_defined;
    let version = (await struct.unpack_from_async("<B", this.fh, offset))[0];
    var info, size, fillvalue;
    if (version == 1 || version == 2) {
      info = await _unpack_struct_from_async(FILLVAL_MSG_V1V2, this.fh, offset);
      offset += FILLVAL_MSG_V1V2_SIZE;
      is_defined = info.get("fillvalue_defined");
    } else if (version == 3) {
      info = await _unpack_struct_from_async(FILLVAL_MSG_V3, this.fh, offset);
      offset += FILLVAL_MSG_V3_SIZE;
      is_defined = info.get("flags") & 32;
    } else {
      throw 'InvalidHDF5File("Unknown fillvalue msg version: "' + String(version);
    }
    if (is_defined) {
      size = (await struct.unpack_from_async("<I", this.fh, offset))[0];
      offset += 4;
    } else {
      size = 0;
    }
    if (size) {
      let [getter, big_endian, size2] = dtype_getter(await this.dtype);
      let payload_view = new DataView64(await this.fh.slice(offset, offset + size2));
      fillvalue = payload_view[getter](offset, !big_endian, size2);
    } else {
      fillvalue = 0;
    }
    return fillvalue;
  }
  async unpack_attribute(offset) {
    let version = (await struct.unpack_from_async("<B", this.fh, offset))[0];
    var attr_map, padding_multiple;
    if (version == 1) {
      attr_map = await _unpack_struct_from_async(ATTR_MSG_HEADER_V1, this.fh, offset);
      assert(attr_map.get("version") == 1);
      offset += ATTR_MSG_HEADER_V1_SIZE;
      padding_multiple = 8;
    } else if (version == 3) {
      attr_map = await _unpack_struct_from_async(ATTR_MSG_HEADER_V3, this.fh, offset);
      assert(attr_map.get("version") == 3);
      offset += ATTR_MSG_HEADER_V3_SIZE;
      padding_multiple = 1;
    } else {
      throw "unsupported attribute message version: " + version;
    }
    let name_size = attr_map.get("name_size");
    let name = (await struct.unpack_from_async("<" + name_size.toFixed() + "s", this.fh, offset))[0];
    name = name.replace(/\x00$/, "");
    offset += _padded_size(name_size, padding_multiple);
    var dtype;
    try {
      dtype = await new DatatypeMessage(this.fh, offset).dtype;
    } catch (e) {
      console.warn("Attribute " + name + " type not implemented, set to null.");
      return [name, null];
    }
    offset += _padded_size(attr_map.get("datatype_size"), padding_multiple);
    let shape = await this.determine_data_shape(this.fh, offset);
    let items = shape.reduce(function(a, b) {
      return a * b;
    }, 1);
    offset += _padded_size(attr_map.get("dataspace_size"), padding_multiple);
    if (dtype.datatype_class === 5) {
      value = await this.fh.slice(offset, offset + dtype.size);
    } else {
      var value = await this._attr_value(dtype, this.fh, items, offset);
      if (shape.length == 0) {
        value = value[0];
      }
    }
    return [name, value];
  }
  async determine_data_shape(buf, offset) {
    let version = (await struct.unpack_from_async("<B", buf, offset))[0];
    var header;
    if (version == 1) {
      header = await _unpack_struct_from_async(DATASPACE_MSG_HEADER_V1, buf, offset);
      assert(header.get("version") == 1);
      offset += DATASPACE_MSG_HEADER_V1_SIZE;
    } else if (version == 2) {
      header = await _unpack_struct_from_async(DATASPACE_MSG_HEADER_V2, buf, offset);
      assert(header.get("version") == 2);
      offset += DATASPACE_MSG_HEADER_V2_SIZE;
    } else {
      throw "unknown dataspace message version";
    }
    let ndims = header.get("dimensionality");
    let dim_sizes = await struct.unpack_from_async("<" + ndims.toFixed() + "Q", buf, offset);
    return dim_sizes;
  }
  async _attr_value(dtype, buf, count, offset) {
    var value = new Array(count);
    if (dtype instanceof Array) {
      let dtype_class = dtype[0];
      for (var i = 0; i < count; i++) {
        if (dtype_class == "VLEN_STRING") {
          let character_set = dtype[2];
          var [vlen, vlen_data] = await this._vlen_size_and_data(buf, offset);
          const encoding = character_set == 0 ? "ascii" : "utf-8";
          const decoder = new TextDecoder(encoding);
          value[i] = decoder.decode(vlen_data);
          offset += 16;
        } else if (dtype_class == "REFERENCE") {
          var address = await struct.unpack_from_async("<Q", buf, offset);
          value[i] = address;
          offset += 8;
        } else if (dtype_class == "VLEN_SEQUENCE") {
          let base_dtype = dtype[1];
          var [vlen, vlen_data] = this._vlen_size_and_data(buf, offset);
          value[i] = this._attr_value(base_dtype, vlen_data, vlen, 0);
          offset += 16;
        } else {
          throw "NotImplementedError";
        }
      }
    } else {
      let [getter, big_endian, size] = dtype_getter(dtype);
      const arrayBuffer = await buf.slice(offset, offset + count * size);
      let view = new DataView64(arrayBuffer, 0);
      let bufferOffset = 0;
      for (var i = 0; i < count; i++) {
        value[i] = view[getter](bufferOffset, !big_endian, size);
        bufferOffset += size;
      }
    }
    return value;
  }
  async _vlen_size_and_data(buf, offset) {
    let vlen_size = (await struct.unpack_from_async("<I", buf, offset))[0];
    let gheap_id = await _unpack_struct_from_async(GLOBAL_HEAP_ID, buf, offset + 4);
    let gheap_address = gheap_id.get("collection_address");
    assert(gheap_id.get("collection_address") < Number.MAX_SAFE_INTEGER);
    var gheap;
    if (!(gheap_address in this._global_heaps)) {
      gheap = new GlobalHeap(this.fh, gheap_address);
      await gheap.ready;
      this._global_heaps[gheap_address] = gheap;
    }
    gheap = this._global_heaps[gheap_address];
    let vlen_data = gheap.objects.get(gheap_id.get("object_index"));
    return [vlen_size, vlen_data];
  }
  async _parse_v1_objects(fh, offset) {
    let header = await _unpack_struct_from_async(OBJECT_HEADER_V1, fh, offset);
    assert(header.get("version") == 1);
    let total_header_messages = header.get("total_header_messages");
    var block_size = header.get("object_header_size");
    var block_offset = offset + _structure_size(OBJECT_HEADER_V1);
    var msg_data = await fh.slice(block_offset, block_offset + block_size);
    var object_header_blocks = [[block_offset, block_size]];
    var current_block = 0;
    var local_offset = 0;
    var msgs = new Array(total_header_messages);
    for (var i = 0; i < total_header_messages; i++) {
      if (local_offset >= block_size) {
        [block_offset, block_size] = object_header_blocks[++current_block];
        local_offset = 0;
      }
      let msg = await _unpack_struct_from_async(HEADER_MSG_INFO_V1, fh, block_offset + local_offset);
      let offset_to_message = block_offset + local_offset + HEADER_MSG_INFO_V1_SIZE;
      msg.set("offset_to_message", offset_to_message);
      if (msg.get("type") == OBJECT_CONTINUATION_MSG_TYPE) {
        var [fh_off, size] = await struct.unpack_from_async("<QQ", fh, offset_to_message);
        object_header_blocks.push([fh_off, size]);
      }
      local_offset += HEADER_MSG_INFO_V1_SIZE + msg.get("size");
      msgs[i] = msg;
    }
    return [msgs, msg_data, header];
  }
  async _parse_v2_objects(buf, offset) {
    var [header, creation_order_size, block_offset] = await this._parse_v2_header(buf, offset);
    offset = block_offset;
    var msgs = [];
    var block_size = header.get("size_of_chunk_0");
    var msg_data = buf.slice(offset, offset += block_size);
    var object_header_blocks = [[block_offset, block_size]];
    var current_block = 0;
    var local_offset = 0;
    while (true) {
      if (local_offset >= block_size - HEADER_MSG_INFO_V2_SIZE) {
        let next_block = object_header_blocks[++current_block];
        if (next_block == null) {
          break;
        }
        [block_offset, block_size] = next_block;
        local_offset = 0;
      }
      let msg = await _unpack_struct_from_async(HEADER_MSG_INFO_V2, buf, block_offset + local_offset);
      let offset_to_message = block_offset + local_offset + HEADER_MSG_INFO_V2_SIZE + creation_order_size;
      msg.set("offset_to_message", offset_to_message);
      if (msg.get("type") == OBJECT_CONTINUATION_MSG_TYPE) {
        var [fh_off, size] = await struct.unpack_from_async("<QQ", buf, offset_to_message);
        object_header_blocks.push([fh_off + 4, size - 4]);
      }
      local_offset += HEADER_MSG_INFO_V2_SIZE + msg.get("size") + creation_order_size;
      msgs.push(msg);
    }
    return [msgs, msg_data, header];
  }
  async _parse_v2_header(buf, offset) {
    let header = await _unpack_struct_from_async(OBJECT_HEADER_V2, buf, offset);
    var creation_order_size;
    offset += _structure_size(OBJECT_HEADER_V2);
    assert(header.get("version") == 2);
    if (header.get("flags") & 4) {
      creation_order_size = 2;
    } else {
      creation_order_size = 0;
    }
    assert((header.get("flags") & 16) == 0);
    if (header.get("flags") & 32) {
      let times = await struct.unpack_from_async("<4I", buf, offset);
      offset += 16;
      header.set("access_time", times[0]);
      header.set("modification_time", times[1]);
      header.set("change_time", times[2]);
      header.set("birth_time", times[3]);
    }
    let chunk_fmt = ["<B", "<H", "<I", "<Q"][header.get("flags") & 3];
    header.set("size_of_chunk_0", (await struct.unpack_from_async(chunk_fmt, buf, offset))[0]);
    offset += struct.calcsize(chunk_fmt);
    return [header, creation_order_size, offset];
  }
  async find_link(name) {
    if (this._links) {
      for (link of this._links) {
        if (name === link[0]) {
          return link;
        }
      }
    } else {
      const links = [];
      for await (const link2 of this.iter_links()) {
        if (name === link2[0]) {
          return link2;
        }
        links.push(link2);
      }
      this._links = links;
    }
    return void 0;
  }
  async get_links() {
    const links = [];
    for await (const link2 of this.iter_links()) {
      links.push(link2);
    }
    return Object.fromEntries(links);
  }
  async *iter_links() {
    for (let msg of this.msgs) {
      if (msg.get("type") == SYMBOL_TABLE_MSG_TYPE) {
        yield* this._iter_links_from_symbol_tables(msg);
      } else if (msg.get("type") == LINK_MSG_TYPE) {
        yield this._get_link_from_link_msg(msg);
      } else if (msg.get("type") == LINK_INFO_MSG_TYPE) {
        yield* this._iter_link_from_link_info_msg(msg);
      }
    }
  }
  async *_iter_links_from_symbol_tables(sym_tbl_msg) {
    assert(sym_tbl_msg.get("size") == 16);
    let data = await _unpack_struct_from_async(SYMBOL_TABLE_MSG, this.fh, sym_tbl_msg.get("offset_to_message"));
    yield* this._iter_links_btree_v1(data.get("btree_address"), data.get("heap_address"));
  }
  async *_iter_links_btree_v1(btree_address, heap_address) {
    let btree = new BTreeV1Groups(this.fh, btree_address);
    await btree.ready;
    let heap = new Heap(this.fh, heap_address);
    await heap.ready;
    for (let symbol_table_address of btree.symbol_table_addresses()) {
      let table = new SymbolTable(this.fh, symbol_table_address);
      await table.ready;
      table.assign_name(heap);
      yield* Object.entries(table.get_links(heap));
    }
  }
  async _get_link_from_link_msg(link_msg) {
    let offset = link_msg.get("offset_to_message");
    return await this._decode_link_msg(this.fh, offset)[1];
  }
  async _decode_link_msg(data, offset) {
    let [version, flags] = await struct.unpack_from_async("<BB", data, offset);
    offset += 2;
    assert(version == 1);
    let size_of_length_of_link_name = 2 ** (flags & 3);
    let link_type_field_present = (flags & 2 ** 3) > 0;
    let link_name_character_set_field_present = (flags & 2 ** 4) > 0;
    let ordered = (flags & 2 ** 2) > 0;
    let link_type;
    if (link_type_field_present) {
      link_type = (await struct.unpack_from_async("<B", data, offset))[0];
      offset += 1;
    } else {
      link_type = 0;
    }
    assert([0, 1].includes(link_type));
    let creationorder;
    if (ordered) {
      creationorder = (await struct.unpack_from_async("<Q", data, offset))[0];
      offset += 8;
    }
    let link_name_character_set = 0;
    if (link_name_character_set_field_present) {
      link_name_character_set = (await struct.unpack_from_async("<B", data, offset))[0];
      offset += 1;
    }
    let encoding = link_name_character_set == 0 ? "ascii" : "utf-8";
    let name_size_fmt = ["<B", "<H", "<I", "<Q"][flags & 3];
    let name_size = (await struct.unpack_from_async(name_size_fmt, data, offset))[0];
    offset += size_of_length_of_link_name;
    let name = new TextDecoder(encoding).decode(await data.slice(offset, offset + name_size));
    offset += name_size;
    let address;
    if (link_type == 0) {
      address = (await struct.unpack_from_async("<Q", data, offset))[0];
    } else if (link_type == 1) {
      let length_of_soft_link_value = (await struct.unpack_from_async("<H", data, offset))[0];
      offset += 2;
      address = new TextDecoder(encoding).decode(await data.slice(offset, offset + length_of_soft_link_value));
    }
    return [creationorder, [name, address]];
  }
  async *_iter_link_from_link_info_msg(info_msg) {
    let offset = info_msg.get("offset_to_message");
    let data = await this._decode_link_info_msg(this.fh, offset);
    let heap_address = data.get("heap_address");
    let name_btree_address = data.get("name_btree_address");
    let order_btree_address = data.get("order_btree_address");
    if (name_btree_address != null) {
      yield* this._iter_links_btree_v2(name_btree_address, order_btree_address, heap_address);
    }
  }
  async *_iter_links_btree_v2(name_btree_address, order_btree_address, heap_address) {
    let heap = new FractalHeap(this.fh, heap_address);
    await heap.ready;
    let btree;
    const ordered = order_btree_address != null;
    if (ordered) {
      btree = new BTreeV2GroupOrders(this.fh, order_btree_address);
      await btree.ready;
    } else {
      btree = new BTreeV2GroupNames(this.fh, name_btree_address);
      await btree.ready;
    }
    let items = /* @__PURE__ */ new Map();
    for (let record of btree.iter_records()) {
      let data = heap.get_data(record.get("heapid"));
      let [creationorder, item] = await this._decode_link_msg(data, 0);
      const key = ordered ? creationorder : item[0];
      items.set(key, item);
    }
    let sorted_keys = Array.from(items.keys()).sort();
    for (let key of sorted_keys) {
      yield items.get(key);
    }
  }
  async _decode_link_info_msg(data, offset) {
    let [version, flags] = await struct.unpack_from_async("<BB", data, offset);
    assert(version == 0);
    offset += 2;
    if ((flags & 1) > 0) {
      offset += 8;
    }
    let fmt = (flags & 2) > 0 ? LINK_INFO_MSG2 : LINK_INFO_MSG1;
    let link_info = await _unpack_struct_from_async(fmt, data, offset);
    let output = /* @__PURE__ */ new Map();
    for (let [k, v] of link_info.entries()) {
      output.set(k, v == UNDEFINED_ADDRESS2 ? null : v);
    }
    return output;
  }
  get is_dataset() {
    return this.find_msg_type(DATASPACE_MSG_TYPE).length > 0;
  }
  async get_data() {
    let msg = this.find_msg_type(DATA_STORAGE_MSG_TYPE)[0];
    let msg_offset = msg.get("offset_to_message");
    var [version, dims, layout_class, property_offset] = await this._get_data_message_properties(msg_offset);
    if (layout_class == 0) {
      throw "Compact storage of DataObject not implemented";
    } else if (layout_class == 1) {
      return this._get_contiguous_data(property_offset);
    } else if (layout_class == 2) {
      return this._get_chunked_data(msg_offset);
    }
  }
  async _get_data_message_properties(msg_offset) {
    let dims, layout_class, property_offset;
    let [version, arg1, arg2] = await struct.unpack_from_async("<BBB", this.fh, msg_offset);
    if (version == 1 || version == 2) {
      dims = arg1;
      layout_class = arg2;
      property_offset = msg_offset;
      property_offset += struct.calcsize("<BBB");
      property_offset += struct.calcsize("<BI");
      assert(layout_class == 1 || layout_class == 2);
    } else if (version == 3 || version == 4) {
      layout_class = arg1;
      property_offset = msg_offset;
      property_offset += struct.calcsize("<BB");
    }
    assert(version >= 1 && version <= 4);
    return [version, dims, layout_class, property_offset];
  }
  async _get_contiguous_data(property_offset) {
    let [data_offset] = await struct.unpack_from_async("<Q", this.fh, property_offset);
    const shape = await this.shape;
    const dtype = await this.dtype;
    if (data_offset == UNDEFINED_ADDRESS2) {
      let size = shape.reduce(function(a, b) {
        return a * b;
      }, 1);
      return new Array(size);
    }
    var fullsize = shape.reduce(function(a, b) {
      return a * b;
    }, 1);
    if (!(dtype instanceof Array)) {
      if (/[<>=!@\|]?(i|u|f|S)(\d*)/.test(dtype)) {
        let [item_getter, item_is_big_endian, item_size] = dtype_getter(dtype);
        let output = new Array(fullsize);
        const local_buffer = await this.fh.slice(data_offset, data_offset + item_size * fullsize);
        let view = new DataView64(local_buffer);
        for (var i = 0; i < fullsize; i++) {
          output[i] = view[item_getter](i * item_size, !item_is_big_endian, item_size);
        }
        return output;
      } else if (dtype.datatype_class === 5) {
        return this.fh.slice(data_offset, data_offset + dtype.size);
      } else {
        throw "not Implemented - no proper dtype defined";
      }
    } else {
      let dtype_class = dtype[0];
      if (dtype_class == "REFERENCE") {
        let size = dtype[1];
        if (size != 8) {
          throw "NotImplementedError('Unsupported Reference type')";
        }
        let ref_addresses = await this.fh.slice(data_offset, data_offset + fullsize);
        return ref_addresses;
      } else if (dtype_class == "VLEN_STRING") {
        let character_set = dtype[2];
        const encoding = character_set == 0 ? "ascii" : "utf-8";
        const decoder = new TextDecoder(encoding);
        var value = [];
        for (var i = 0; i < fullsize; i++) {
          const [vlen, vlen_data] = await this._vlen_size_and_data(this.fh, data_offset);
          value[i] = decoder.decode(vlen_data);
          data_offset += 16;
        }
        return value;
      } else {
        throw "NotImplementedError('datatype not implemented')";
      }
    }
  }
  async _get_chunked_data(offset) {
    await this._get_chunk_params();
    if (this._chunk_address == UNDEFINED_ADDRESS2) {
      return [];
    }
    var chunk_btree = new BTreeV1RawDataChunks(this.fh, this._chunk_address, this._chunk_dims);
    await chunk_btree.ready;
    const dtype = await this.dtype;
    const shape = await this.shape;
    const chunks = await this.chunks;
    const filter_pipeline = await this.get_filter_pipeline();
    let data = await chunk_btree.construct_data_from_chunks(chunks, shape, dtype, filter_pipeline);
    if (dtype instanceof Array && /^VLEN/.test(dtype[0])) {
      let dtype_class = dtype[0];
      for (var i = 0; i < data.length; i++) {
        let [item_size, gheap_address, object_index] = data[i];
        var gheap;
        if (!(gheap_address in this._global_heaps)) {
          gheap = new GlobalHeap(this.fh, gheap_address);
          await gheap.ready;
          this._global_heaps[gheap_address] = gheap;
        } else {
          gheap = this._global_heaps[gheap_address];
        }
        let vlen_data = gheap.objects.get(object_index);
        if (dtype_class == "VLEN_STRING") {
          let character_set = dtype[2];
          const encoding = character_set == 0 ? "ascii" : "utf-8";
          const decoder = new TextDecoder(encoding);
          data[i] = decoder.decode(vlen_data);
        }
      }
    }
    return data;
  }
  async _get_chunk_params() {
    if (this._chunk_params_set) {
      return;
    }
    this._chunk_params_set = true;
    var msg = this.find_msg_type(DATA_STORAGE_MSG_TYPE)[0];
    var offset = msg.get("offset_to_message");
    var [version, dims, layout_class, property_offset] = await this._get_data_message_properties(offset);
    if (layout_class != 2) {
      return;
    }
    var data_offset;
    if (version == 1 || version == 2) {
      var address = (await struct.unpack_from_async("<Q", this.fh, property_offset))[0];
      data_offset = property_offset + struct.calcsize("<Q");
    } else if (version == 3) {
      var [dims, address] = await struct.unpack_from_async("<BQ", this.fh, property_offset);
      data_offset = property_offset + struct.calcsize("<BQ");
    }
    assert(version >= 1 && version <= 3);
    var fmt = "<" + (dims - 1).toFixed() + "I";
    var chunk_shape = await struct.unpack_from_async(fmt, this.fh, data_offset);
    this._chunks = chunk_shape;
    this._chunk_dims = dims;
    this._chunk_address = address;
    return;
  }
};
async function determine_data_shape(buf, offset) {
  let version = (await struct.unpack_from_async("<B", buf, offset))[0];
  var header;
  if (version == 1) {
    header = await _unpack_struct_from_async(DATASPACE_MSG_HEADER_V1, buf, offset);
    assert(header.get("version") == 1);
    offset += DATASPACE_MSG_HEADER_V1_SIZE;
  } else if (version == 2) {
    header = await _unpack_struct_from_async(DATASPACE_MSG_HEADER_V2, buf, offset);
    assert(header.get("version") == 2);
    offset += DATASPACE_MSG_HEADER_V2_SIZE;
  } else {
    throw "InvalidHDF5File('unknown dataspace message version')";
  }
  let ndims = header.get("dimensionality");
  let dim_sizes = await struct.unpack_from_async("<" + (ndims * 2).toFixed() + "I", buf, offset);
  return dim_sizes.filter(function(s, i) {
    return i % 2 == 0;
  });
}
var UNDEFINED_ADDRESS2 = struct.unpack_from("<Q", new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255]).buffer);
var GLOBAL_HEAP_ID = /* @__PURE__ */ new Map([
  ["collection_address", "Q"],
  ["object_index", "I"]
]);
_structure_size(GLOBAL_HEAP_ID);
var ATTR_MSG_HEADER_V1 = /* @__PURE__ */ new Map([
  ["version", "B"],
  ["reserved", "B"],
  ["name_size", "H"],
  ["datatype_size", "H"],
  ["dataspace_size", "H"]
]);
var ATTR_MSG_HEADER_V1_SIZE = _structure_size(ATTR_MSG_HEADER_V1);
var ATTR_MSG_HEADER_V3 = /* @__PURE__ */ new Map([
  ["version", "B"],
  ["flags", "B"],
  ["name_size", "H"],
  ["datatype_size", "H"],
  ["dataspace_size", "H"],
  ["character_set_encoding", "B"]
]);
var ATTR_MSG_HEADER_V3_SIZE = _structure_size(ATTR_MSG_HEADER_V3);
var OBJECT_HEADER_V1 = /* @__PURE__ */ new Map([
  ["version", "B"],
  ["reserved", "B"],
  ["total_header_messages", "H"],
  ["object_reference_count", "I"],
  ["object_header_size", "I"],
  ["padding", "I"]
]);
var OBJECT_HEADER_V2 = /* @__PURE__ */ new Map([
  ["signature", "4s"],
  ["version", "B"],
  ["flags", "B"]
]);
var DATASPACE_MSG_HEADER_V1 = /* @__PURE__ */ new Map([
  ["version", "B"],
  ["dimensionality", "B"],
  ["flags", "B"],
  ["reserved_0", "B"],
  ["reserved_1", "I"]
]);
var DATASPACE_MSG_HEADER_V1_SIZE = _structure_size(DATASPACE_MSG_HEADER_V1);
var DATASPACE_MSG_HEADER_V2 = /* @__PURE__ */ new Map([
  ["version", "B"],
  ["dimensionality", "B"],
  ["flags", "B"],
  ["type", "B"]
]);
var DATASPACE_MSG_HEADER_V2_SIZE = _structure_size(DATASPACE_MSG_HEADER_V2);
var HEADER_MSG_INFO_V1 = /* @__PURE__ */ new Map([
  ["type", "H"],
  ["size", "H"],
  ["flags", "B"],
  ["reserved", "3s"]
]);
var HEADER_MSG_INFO_V1_SIZE = _structure_size(HEADER_MSG_INFO_V1);
var HEADER_MSG_INFO_V2 = /* @__PURE__ */ new Map([
  ["type", "B"],
  ["size", "H"],
  ["flags", "B"]
]);
var HEADER_MSG_INFO_V2_SIZE = _structure_size(HEADER_MSG_INFO_V2);
var SYMBOL_TABLE_MSG = /* @__PURE__ */ new Map([
  ["btree_address", "Q"],
  ["heap_address", "Q"]
]);
var LINK_INFO_MSG1 = /* @__PURE__ */ new Map([
  ["heap_address", "Q"],
  ["name_btree_address", "Q"]
]);
var LINK_INFO_MSG2 = /* @__PURE__ */ new Map([
  ["heap_address", "Q"],
  ["name_btree_address", "Q"],
  ["order_btree_address", "Q"]
]);
var FILLVAL_MSG_V1V2 = /* @__PURE__ */ new Map([
  ["version", "B"],
  ["space_allocation_time", "B"],
  ["fillvalue_write_time", "B"],
  ["fillvalue_defined", "B"]
]);
var FILLVAL_MSG_V1V2_SIZE = _structure_size(FILLVAL_MSG_V1V2);
var FILLVAL_MSG_V3 = /* @__PURE__ */ new Map([
  ["version", "B"],
  ["flags", "B"]
]);
var FILLVAL_MSG_V3_SIZE = _structure_size(FILLVAL_MSG_V3);
var FILTER_PIPELINE_DESCR_V1 = /* @__PURE__ */ new Map([
  ["filter_id", "H"],
  ["name_length", "H"],
  ["flags", "H"],
  ["client_data_values", "H"]
]);
var FILTER_PIPELINE_DESCR_V1_SIZE = _structure_size(FILTER_PIPELINE_DESCR_V1);
var DATASPACE_MSG_TYPE = 1;
var LINK_INFO_MSG_TYPE = 2;
var DATATYPE_MSG_TYPE = 3;
var FILLVALUE_MSG_TYPE = 5;
var LINK_MSG_TYPE = 6;
var DATA_STORAGE_MSG_TYPE = 8;
var DATA_STORAGE_FILTER_PIPELINE_MSG_TYPE = 11;
var ATTRIBUTE_MSG_TYPE = 12;
var OBJECT_CONTINUATION_MSG_TYPE = 16;
var SYMBOL_TABLE_MSG_TYPE = 17;

// esm/high-level.js
var Group = class {
  constructor(name, parent) {
    if (parent == null) {
      this.parent = this;
      this.file = this;
    } else {
      this.parent = parent;
      this.file = parent.file;
    }
    this.name = name;
  }
  async init(dataobjects) {
    const index = this.file.index;
    if (index && this.name in index) {
      this._links = index[this.name];
    } else {
      this._links = await dataobjects.get_links();
    }
    this._dataobjects = dataobjects;
    this._attrs = null;
    this._keys = null;
  }
  get keys() {
    if (this._keys == null) {
      this._keys = Object.keys(this._links);
    }
    return this._keys.slice();
  }
  get values() {
    return this.keys.map((k) => this.get(k));
  }
  length() {
    return this.keys.length;
  }
  _dereference(ref) {
    if (!ref) {
      throw "cannot deference null reference";
    }
    let obj = this.file._get_object_by_address(ref);
    if (obj == null) {
      throw "reference not found in file";
    }
    return obj;
  }
  async get(y) {
    if (typeof y == "number") {
      return this._dereference(y);
    }
    var path = normpath(y);
    if (path == "/") {
      return this.file;
    }
    if (path == ".") {
      return this;
    }
    if (/^\//.test(path)) {
      return this.file.get(path.slice(1));
    }
    if (posix_dirname(path) != "") {
      var [next_obj, additional_obj] = path.split(/\/(.*)/);
    } else {
      var next_obj = path;
      var additional_obj = ".";
    }
    if (!(next_obj in this._links)) {
      throw next_obj + " not found in group";
    }
    var obj_name = normpath(this.name + "/" + next_obj);
    let link_target = this._links[next_obj];
    if (typeof link_target == "string") {
      try {
        return this.get(link_target);
      } catch (error) {
        return null;
      }
    }
    var dataobjs = new DataObjects(this.file._fh, link_target);
    await dataobjs.ready;
    if (dataobjs.is_dataset) {
      if (additional_obj != ".") {
        throw obj_name + " is a dataset, not a group";
      }
      return new Dataset(obj_name, dataobjs, this);
    } else {
      var new_group = new Group(obj_name, this);
      await new_group.init(dataobjs);
      return new_group.get(additional_obj);
    }
  }
  visit(func) {
    return this.visititems((name, obj) => func(name));
  }
  visititems(func) {
    var root_name_length = this.name.length;
    if (!/\/$/.test(this.name)) {
      root_name_length += 1;
    }
    var queue = this.values.slice();
    while (queue) {
      let obj = queue.shift();
      if (queue.length == 1)
        ;
      let name = obj.name.slice(root_name_length);
      let ret = func(name, obj);
      if (ret != null) {
        return ret;
      }
      if (obj instanceof Group) {
        queue = queue.concat(obj.values);
      }
    }
    return null;
  }
  get attrs() {
    if (this._attrs == null) {
      this._attrs = this._dataobjects.get_attributes();
    }
    return this._attrs;
  }
};
var File$1 = class extends Group {
  constructor(fh, filename, options) {
    super("/", null);
    this.ready = this.init(fh, filename, options);
  }
  async init(fh, filename, options) {
    var superblock = new SuperBlock(fh, 0);
    await superblock.ready;
    var offset = await superblock.get_offset_to_dataobjects();
    var dataobjects = new DataObjects(fh, offset);
    await dataobjects.ready;
    this.parent = this;
    this.file = this;
    this.name = "/";
    this._dataobjects = dataobjects;
    this._attrs = null;
    this._keys = null;
    this._fh = fh;
    this.filename = filename || "";
    this.mode = "r";
    this.userblock_size = 0;
    if (options && options.index) {
      this.index = options.index;
    } else {
      let index_offset;
      if (options && options.indexOffset) {
        index_offset = options.indexOffset;
      } else {
        const attrs = await this.attrs;
        if (attrs.hasOwnProperty("_index_offset")) {
          index_offset = attrs["_index_offset"];
        } else {
          const indexName = this.indexName || "_index";
          const index_link = await dataobjects.find_link(indexName);
          if (index_link) {
            index_offset = index_link[1];
          }
        }
      }
      if (index_offset) {
        try {
          const dataobject = new DataObjects(fh, index_offset);
          await dataobject.ready;
          const comp_index_data = await dataobject.get_data();
          const inflated = ungzip_1$2(comp_index_data);
          const json = new TextDecoder().decode(inflated);
          this.index = JSON.parse(json);
        } catch (e) {
          console.error(`Error loading index by offset ${e}`);
        }
      }
    }
    if (this.index && this.name in this.index) {
      this._links = this.index[this.name];
    } else {
      this._links = await dataobjects.get_links();
    }
  }
  _get_object_by_address(obj_addr) {
    if (this._dataobjects.offset == obj_addr) {
      return this;
    }
    return this.visititems((y) => {
      y._dataobjects.offset == obj_addr ? y : null;
    });
  }
};
var Dataset = class extends Array {
  constructor(name, dataobjects, parent) {
    super();
    this.parent = parent;
    this.file = parent.file;
    this.name = name;
    this._dataobjects = dataobjects;
    this._attrs = null;
    this._astype = null;
  }
  get value() {
    var data = this._dataobjects.get_data();
    if (this._astype == null) {
      return this.getValue(data);
    }
    return data.astype(this._astype);
  }
  get shape() {
    return this._dataobjects.shape;
  }
  get attrs() {
    return this._dataobjects.get_attributes();
  }
  get dtype() {
    return this._dataobjects.dtype;
  }
  get fillvalue() {
    return this._dataobjects.get_fillvalue();
  }
  async to_array() {
    const value = await this.value;
    const shape = await this.shape;
    return create_nested_array(value, shape);
  }
  async getValue(data) {
    const dtype = await this.dtype;
    if ((typeof dtype === "string" || dtype instanceof String) && dtype.startsWith("S")) {
      return (await data).map((s) => {
        let idx = s.indexOf("\0");
        return idx >= 0 ? s.substring(0, idx) : s;
      });
    } else {
      return data;
    }
  }
};
function posix_dirname(p) {
  let sep = "/";
  let i = p.lastIndexOf(sep) + 1;
  let head = p.slice(0, i);
  let all_sep = new RegExp("^" + sep + "+$");
  let end_sep = new RegExp(sep + "$");
  if (head && !all_sep.test(head)) {
    head = head.replace(end_sep, "");
  }
  return head;
}
function normpath(path) {
  return path.replace(/\/(\/)+/g, "/");
}
function create_nested_array(value, shape) {
  const total_length = value.length;
  const dims_product = shape.reduce((previous, current) => previous * current, 1);
  if (total_length !== dims_product) {
    console.warn(`shape product: ${dims_product} does not match length of flattened array: ${total_length}`);
  }
  let output = value;
  const subdims = shape.slice(1).reverse();
  for (let dim of subdims) {
    const new_output = [];
    const { length } = output;
    let cursor = 0;
    while (cursor < length) {
      new_output.push(output.slice(cursor, cursor += dim));
    }
    output = new_output;
  }
  return output;
}
/*! pako 2.0.4 https://github.com/nodeca/pako @license (MIT AND Zlib) */

async function openH5File(options) {

    // Some clients (notably igv-webapp) pass a File reference in the url field.  Fix this
    if (options.url && isBlobLike(options.url)) {
        options.file = options.url;
        options.url = undefined;
    }

    const isRemote = options.url !== undefined;
    let fileReader = options.reader ? options.reader : getReaderFor(options);

    // Set default options appropriate for spacewalk
    const fetchSize = options.fetchSize || 2000;
    const maxSize = options.maxSize || 200000;

    if (isRemote) {
        fileReader = new BufferedFile$1({file: fileReader, fetchSize, maxSize});
    }
    const asyncBuffer = new AsyncBuffer(fileReader);

    // Optional external index -- this is not common
    const index = await readExternalIndex(options);
    const indexOffset = options.indexOffset;

    // Create HDF5 file
    const filename = getFilenameFor(options);
    const hdfFile = new File$1(asyncBuffer, filename, {index, indexOffset});
    await hdfFile.ready;
    return hdfFile
}

async function readExternalIndex(options) {

    if (options.index) {
        return options.index
    } else {
        let indexReader;
        if (options.indexReader) {
            indexReader = options.indexReader;
        } else {
            let indexOptions;
            if (options.indexURL) {
                indexOptions = Object.assign({url: options.indexURL}, options);
            } else if (options.indexPath) {
                indexOptions = Object.assign({path: options.indexPath}, options);
            } else if (options.indexFile) {
                indexOptions = Object.assign({file: options.indexFile}, options);
            } else {
                return undefined
            }
            indexReader = getReaderFor(indexOptions);
        }
        const indexFileContents = await indexReader.read();
        const indexFileJson = new TextDecoder().decode(indexFileContents);
        return JSON.parse(indexFileJson)
    }

}


function getReaderFor(options) {
    if (options.url) { // An absolute or relative URL
        return new RemoteFile(options)
    } else if (options.path) { // A file path
        return new NodeLocalFile(options)
    } else if (options.file) { // A Browser file blob
        return new BlobFile(options.file)
    } else {
        throw Error("One of 'url', 'path (node only)', or 'file (browser only)' must be specified")
    }
}

function getFilenameFor(options) {
    if (options.url) {
        return getFilename(options.url)
    } else if (options.path) {
        return getFilename(options.path)
    } else if (options.file) {
        return options.file.name
    }
}

/**
 * Wraps an io.Reader in an interface jsfive-async expects*
 */
class AsyncBuffer {
    constructor(fileReader) {
        this.fileReader = fileReader;
    }

    async slice(start, end) {
        return this.fileReader.read(start, end - start)
    }
}

function getFilename(pathOrURL) {
    const idx = pathOrURL.lastIndexOf("/");
    return idx > 0 ? pathOrURL.substring(idx + 1) : pathOrURL
}

function isBlobLike(obj) {
    return typeof obj.slice === 'function' && typeof obj.arrayBuffer === 'function'
}

class SignalNames{
    /**
     * 
     * @param {string} chrom - chromosome name
     * @param {integer} bin_size - bin size
     */
    constructor(chrom, bin_size){
        this.chrom = chrom;
        this.signal_bin_size = bin_size;

        let rd_flag = "";
        this.signals = {
            'raw_RD': `his_rd_p_${this.chrom}_${this.signal_bin_size}${rd_flag}`,
            'gc_RD': `his_rd_p_${this.chrom}_${this.signal_bin_size}_GC`,
            'gc_partition' : `his_rd_p_${this.chrom}_${this.signal_bin_size}_partition_GC_merge`,
            'baf': `snp_likelihood_${this.chrom}_${this.signal_bin_size}_mask`,
            'baf_i1': `snp_i1_${this.chrom}_${this.signal_bin_size}_mask`,
            'Mosaic_segments' : `his_rd_p_${this.chrom}_${this.signal_bin_size}_partition_GC_mosaic_segments_2d`,
            'Mosaic_calls': `his_rd_p_${this.chrom}_${this.signal_bin_size}_partition_GC_mosaic_call_2d`
        };
    }
}


class HDF5Reader {
    /**
     * 
     * @param {string} h5_file - path for the pytor file
     * @param {integer} bin_size - bin size
     */
    constructor(config, bin_size=100000){

        this.config = config;
        this.bin_size = bin_size;
        this.h5_obj = undefined;
        this.pytorKeys = [];
        this.availableBins = [];
    }
    
    async fetch(){

        if(!this.h5_obj) {
            const options = Object.assign(this.config,  {fetchSize: 1000000, maxSize: 200000000});
            this.h5_obj = await openH5File(options);
        }
        return this.h5_obj
    }

    /**
     * 
     * @returns - a list of keys of the pytor file
     */
    async get_keys(){
        let h5_obj = await this.fetch();
        return h5_obj.keys
    }

    async get_rd_signal(bin_size = this.bin_size, chrom=undefined){
        // Fetch the pytor file and get keys
        const h5Obj = await this.fetch();
        this.pytorKeys = h5Obj.keys;

        // get available bin sizes
        const signalBin = new ParseSignals(this.pytorKeys);
        this.availableBins = signalBin.getAllBins();
        
        // check if the user provided bin is available, else set the last bin_size
        if(! this.availableBins.includes(bin_size)){
            bin_size = this.availableBins.at(-1);    
        }
        
        // get rd chromosomes and rd stat
        const rdChromosomes = await this.getChromosomes(chrom);

        let rd_stat = await this.rd_stat(bin_size);

        // prepare wig formatted file for all chromosome
        const wigFeatures = await this.getWigFeatures(rdChromosomes, bin_size, rd_stat);

        this.setCallers(wigFeatures);
        return { [bin_size]: wigFeatures };
    }

    async getWigFeatures(rdChromosomes, binSize, rdStat) {
        const wigFeatures = {
            RD_Raw: [],
            RD_Raw_gc_coor: [],
            ReadDepth: [],
            "2D": [],
            BAF1: [],
            BAF2: []
        };

        for (const chrom of rdChromosomes) {
            const signalNameObj = new SignalNames(chrom, binSize);

            wigFeatures.RD_Raw.push(...await this.get_chr_signal(chrom, binSize, signalNameObj.signals.raw_RD, rdStat));
            wigFeatures.RD_Raw_gc_coor.push(...await this.get_chr_signal(chrom, binSize, signalNameObj.signals.gc_RD, rdStat));
            wigFeatures.ReadDepth.push(...await this.get_chr_signal(chrom, binSize, signalNameObj.signals.gc_partition, rdStat));

            wigFeatures["2D"].push(...await this.rd_call_combined(chrom, binSize, rdStat, signalNameObj));

            const [baf1, baf2] = await this.getBafSignals(chrom, binSize, signalNameObj.signals.baf_i1);
            wigFeatures.BAF1.push(...baf1);
            wigFeatures.BAF2.push(...baf2);
        }

        return wigFeatures;
    }

    async getChromosomes(refChroms) {
        // return chromosome names if they exists in the rd_chromosomes
        const rdChroms_obj = await this.h5_obj.get("rd_chromosomes");
        const rdChroms = await rdChroms_obj.value;
        if(!refChroms){
            return rdChroms 
        }else {
            let refChromsSet = new Set(refChroms);
            return rdChroms.filter(item => refChromsSet.has(item));
            
        }
    }

    setCallers(wigFeatures) {
        this.callers = [];
        if (wigFeatures.ReadDepth.length) this.callers.push('ReadDepth');
        if (wigFeatures["2D"].length) this.callers.push('2D');
    }

    decode_segments(segments_arr){
        let max = 2 ** 32 - 1;
        let segments = [];
        let l = [];
        for (let x of segments_arr){
            if(x == max){
                segments.push(l);
                l = [];
            } else {
                l.push(x);
            }
        }
        return segments
    }

    async  rd_call_combined(chrom, bin_size, rd_stat, signal_name_obj){
        let chr_wig = [];
        
        let segments;
        let mosaic_call_segments = signal_name_obj.signals['Mosaic_segments'];
        if (this.pytorKeys.includes(mosaic_call_segments)){
            const chrom_dataset = await this.h5_obj.get(mosaic_call_segments);
            let chrom_data = await chrom_dataset.value;
            segments = this.decode_segments(chrom_data);
            
        }

        let mosaic_calls = signal_name_obj.signals['Mosaic_calls'];
        if (this.pytorKeys.includes(mosaic_calls)){
            const segments_call_dataset = await this.h5_obj.get(mosaic_calls);
            let segments_call = await segments_call_dataset.to_array(); //create_nested_array(value, shape)
            segments.forEach((ind_segment, segment_idx) => {
                ind_segment.forEach((bin_value, bin_idx) =>{
                    chr_wig.push({chr:chrom, start: bin_value*bin_size, end: (bin_value+1) * bin_size, value: (segments_call[0][segment_idx]/rd_stat[4]) *2});
                });
            });
        }

        return chr_wig
        
    }
    
    /**
     * returns a list for rd statistics information 
     * @param {integer} bin_size - bin_size 
     * @returns - array - read depth statistics array
     */
    async rd_stat(bin_size){
    
        let rd_stat_signal =  `rd_stat_${bin_size}_auto`;
        let rd_stat;
        if (this.pytorKeys.includes(rd_stat_signal)){
            const rd_stat_dataset = await this.h5_obj.get(rd_stat_signal);
            rd_stat = await rd_stat_dataset.value;
        }
        return rd_stat
    }

    
    async get_chr_signal(chrom, bin_size, signal_name, rd_stat){
        /* return a list of dictionary for a chromosome */
        let chr_wig = [];
        
        if (this.pytorKeys.includes(signal_name)){
            const chrom_dataset = await this.h5_obj.get(signal_name);
            
            let chrom_data = await chrom_dataset.value;
            chrom_data.forEach((bin_value, bin_idx) => {
                chr_wig.push({chr:chrom, start: bin_idx*bin_size, end: (bin_idx+1) * bin_size, value: (bin_value/rd_stat[4]) *2});
            });
        }
        return chr_wig
    }


    async getBafSignals(chrom, binSize, signalName, scalingFactor = -1) {
        const chrWig1 = [];
        const chrWig2 = [];
        
        if (this.pytorKeys.includes(signalName)) {
            const chromDataset = await this.h5_obj.get(signalName);
            const chromData = await chromDataset.to_array();

            chromData.forEach((lh, binIdx) => {
                if (!isNaN(lh)) {
                    chrWig1.push({
                        chr: chrom,
                        start: binIdx * binSize,
                        end: (binIdx + 1) * binSize,
                        value: scalingFactor * (0.5 - lh)
                    });
                    if (lh !== 0.5) {
                        chrWig2.push({
                            chr: chrom,
                            start: binIdx * binSize,
                            end: (binIdx + 1) * binSize,
                            value: scalingFactor * (0.5 + lh)
                        });
                    }
                }
            });
        }
        return [chrWig1, chrWig2];
    }

}

class ParseSignals {
    /**
     * @param {string[]} signals - List of keys in pytor files.
     */
    constructor(signals) {
        this.signals = signals;
    }

    getAllBins() {
        const rdBins = this.getRdBins();
        const snpBins = this.getSnpBins();
        return [...new Set([...rdBins, ...snpBins])].sort((a, b) => a - b);    }

    getRdBins() {
        return this.extractBins(/^his_rd_p_(.*)_(\d+)$/);
    }

    getSnpBins() {
        return this.extractBins(/^snp_likelihood_(.*)_(\d+)_mask$/);
    }

    extractBins(regex) {
        return [...new Set(
            this.signals
                .map(val => val.match(regex))
                .filter(match => match !== null)
                .map(match => Number(match[2]))
        )];
    }
}

class GetFit {
    /**
     * Creates an instance of GetFit.
     * @param {Object} allBins - An object containing all the bins with their respective data.
     */
    constructor(allBins) {
        this.allBins = allBins; // Stores all bins data
    }

    /**
     * Extracts bin scores greater than zero from all bins.
     * @returns {Array} An array of bin scores.
     */

    getValues() {
        const bins = Object.values(this.allBins).reduce(
            (binResult, bin) => { return binResult.concat(bin.filter(a => a.binScore > 0).map(a => a.binScore)) }, []);
        return bins
    }

    /**
     * Calculates the mean of the given data.
     * @param {Array} data - The data array to calculate the mean from.
     * @returns {number} The mean value of the data.
     */
    getMean(data) {
        return (data.reduce(function (a, b) { return a + b; }) / data.length);
    }

    fit_data() {
        let rd_list = this.getValues();
        let distParmas = getDistParams(rd_list);
        return distParmas
    }


    histogram(data, bins) {
        const step = bins[1] - bins[0];
        const hist_bins = [];

        data.forEach((value, index) => {
            bins.forEach((bin_value, bin_index) => {
                if (!hist_bins[bin_value]) {
                    hist_bins[bin_value] = { count: 0 };
                }
                if (bin_value <= value && value < bin_value + step) {
                    hist_bins[bin_value].count++;
                    return false;
                }
            });
        });
        const dist_p = [];
        hist_bins.forEach((bin, index) => { dist_p.push(bin.count); });
        return dist_p
    }

}


function range_function(start, stop, step) {
    const data_array = Array(Math.ceil((stop - start) / step))
        .fill(start)
        .map((x, y) => x + y * step);
    return data_array;
}

function filterOutliers(someArray) {

    if (someArray.length < 4)
        return someArray;

    let values, q1, q3, iqr, maxValue, minValue;

    values = someArray.slice().sort((a, b) => a - b); //copy array fast and sort

    if ((values.length / 4) % 1 === 0) { //find quartiles
        q1 = 1 / 2 * (values[(values.length / 4)] + values[(values.length / 4) + 1]);
        q3 = 1 / 2 * (values[(values.length * (3 / 4))] + values[(values.length * (3 / 4)) + 1]);
    } else {
        q1 = values[Math.floor(values.length / 4 + 1)];
        q3 = values[Math.ceil(values.length * (3 / 4) + 1)];
    }

    iqr = q3 - q1;
    maxValue = q3 + iqr * 1.5;
    minValue = q1 - iqr * 1.5;

    return values.filter((x) => (x >= minValue) && (x <= maxValue));
}

function getDistParams(bins) {
    let filteredBins = filterOutliers(bins);
    const n = filteredBins.length;
    const mean = filteredBins.reduce((a, b) => a + b) / n;
    const std = Math.sqrt(filteredBins.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n);
    return [mean, std]
}

function linspace(a, b, n) {
    if (typeof n === "undefined") n = Math.max(Math.round(b - a) + 1, 1);
    if (n < 2) {
        return n === 1 ? [a] : [];
    }
    var ret = Array(n);
    n--;
    for (let i = n; i >= 0; i--) {
        ret[i] = (i * b + (n - i) * a) / n;
    }
    return ret;
}

var g_utils = { range_function, getDistParams, linspace, GetFit, filterOutliers };

/** 
* Evaluates the cumulative distribution function (CDF) for a Student's t distribution with degrees of freedom `v` at a value `t`.
*
* @params {number} t - value for the t test
* @params {PositiveNumber} v - degree of freedom
* @returns {Probability} evaluated CDF
*/
function TdistributionCDF(t, v){
    if (isNaN( t ) || isNaN( v ) || v <= 0.0) {
		return NaN;
	}
	if ( t === 0.0 ) {
		return 0.5;
	}
    return 1/2 + (1/2 * (incompbeta(1/2*v, 1/2, 1) - incompbeta(1/2*v, 1/2, v/(v+t*t)))) * Math.sign( t)

}


/** 
* incompbeta(a,b,x) evaluates incomplete beta function, here a, b > 0 and 0 <= x <= 1. This function requires contfractbeta(a,b,x, ITMAX = 200) 
*  code translated from  https://malishoaib.wordpress.com/2014/04/15/the-beautiful-beta-functions-in-raw-python/
*
* @params
* @params
* @params
* @returns 
*/
function incompbeta(a, b, x){
     
    if(x == 0){
        return 0;
    }
    else if (x == 1){
        return 1;
    }else
    {
        let lbeta = lgamma(a+b) - lgamma(a) - lgamma(b) + a * Math.log(x) + b * Math.log(1-x);
        if (x < (a+1) / (a+b+2)){
            return Math.exp(lbeta) * contfractbeta(a, b, x) / a;
        }
        else {
            return 1 - Math.exp(lbeta) * contfractbeta(b, a, 1-x) / b;
        }
    }
}

/**
 * contfractbeta() evaluates the continued fraction form of the incomplete Beta function; incompbeta().  
 *   (Code translated from: Numerical Recipes in C.)
 * 
 * @param {*} a 
 * @param {*} b 
 * @param {*} x 
 * @param {*} ITMAX 
 * @returns 
 */
function contfractbeta(a,b,x, ITMAX = 1000){
     
    let EPS = 3.0e-7;
    let az = 1.0;
    let am = 1.0;
    let bm = 1.0;
    let qab = a + b;
    let qap = a + 1.0;
    let qam = a-1.0;
    let bz = 1.0 - qab*x/qap;
     
    //for i in range(ITMAX):
    for(let i =0; i<= ITMAX; i++){
        let em = parseFloat(i+1);
        let tem = em + em;
        let d = em*(b-em)*x/((qam+tem)*(a+tem));
        let ap = az + d*am;
        let bp = bz+d*bm;
        d = -(a+em)*(qab+em)*x/((qap+tem)*(a+tem));
        let app = ap+d*az;
        let bpp = bp+d*bz;
        let aold = az;
        am = ap/bpp;
        bm = bp/bpp;
        az = app/bpp;
        bz = 1.0;
        if (Math.abs(az-aold)<(EPS * Math.abs(az))){
            return az
        }
    }
}

/**
 * Evaluates factorial of a number
 * 
 * @param {Number} xf - Integer number 
 * @returns factorial of the number
 */
function factorial(xf) {
    if ((xf == 0) || (xf == 1)) return 1;
    else {
        let result = (xf * factorial(xf - 1));
        return result
    }
}

/**
 * Evalues factorial for an integer or fraction using either either factorial or gamma function
 * 
 * @param {Number} a - integar or fraction number
 * @returns value of a gamma function
 */
function gamma(a){
    let gamma;
    
    var qc = [75122.6331530, 80916.6278952, 36308.2951477, 8687.24529705, 1168.92649479, 83.8676043424, 2.50662827511];
    
    var sum1 = 0;
    var prod1 = 1;
    if (a == 0) { 
        gamma = 1e99; 
    }else {
        if ((a % 1) == 0) {//if integer
            gamma = factorial(a - 1);
        }
        else { //not integer
            for (let j = 0; j < qc.length; j++) {
                sum1 = sum1 + qc[j] * Math.pow(a, j);
                prod1 = prod1 * (a + j);
            }
            gamma = (sum1 * Math.pow((a + 5.5), (a + 0.5))) * Math.exp(-(a + 5.5)) / prod1;
        }
    }
    
    return gamma
}

/**
 * 
 * @param {Number} xg  - integar or fraction number
 * @returns natural log of gamma function
 */

function lgamma(xg){
    return Math.log(gamma(xg))
}

function t_test_1_sample$1(mean, m, s, n) {
    if (s == 0) s = 1;
    var t = ((mean - m) / s) * Math.sqrt(n);
    var p = 1.0 - TdistributionCDF(Math.abs(t), (n - 1));
    return p
}

function t_test_2_samples$1(m1, s1, n1, m2, s2, n2) {
    if (s1 == 0) s1 = 1;
    if (s2 == 0) s2 = 1;
    var t = (m1 - m2) / Math.sqrt(s1 ** 2 / n1 + s2 ** 2 / n2);
    var df = ((s1 ** 2 / n1 + s2 ** 2 / n2) ** 2 * (n1 - 1) * (n2 - 1)) /
        ((s1 ** 4 * (n2 - 1)) / n1 ** 2 + (s2 ** 4 * (n1 - 1)) / n2 ** 2);

    var p = 1.0 - TdistributionCDF(Math.abs(t), parseInt(df + 0.5));

    return p
}

var t_dist = {TdistributionCDF, gamma, t_test_1_sample: t_test_1_sample$1, t_test_2_samples: t_test_2_samples$1};

// source: https://github.com/mljs/levenberg-marquardt/tree/rollup-web

const toString$4 = Object.prototype.toString;

function isAnyArray$5(object) {
  return toString$4.call(object).endsWith('Array]');
}

function checkOptions(data, parameterizedFunction, options) {
  let {
    timeout,
    minValues,
    maxValues,
    initialValues,
    weights = 1,
    damping = 1e-2,
    dampingStepUp = 11,
    dampingStepDown = 9,
    maxIterations = 100,
    errorTolerance = 1e-7,
    centralDifference = false,
    gradientDifference = 10e-2,
    improvementThreshold = 1e-3,
  } = options;

  if (damping <= 0) {
    throw new Error('The damping option must be a positive number');
  } else if (!data.x || !data.y) {
    throw new Error('The data parameter must have x and y elements');
  } else if (
    !isAnyArray$5(data.x) ||
    data.x.length < 2 ||
    !isAnyArray$5(data.y) ||
    data.y.length < 2
  ) {
    throw new Error(
      'The data parameter elements must be an array with more than 2 points',
    );
  } else if (data.x.length !== data.y.length) {
    throw new Error('The data parameter elements must have the same size');
  }

  let parameters =
    initialValues || new Array(parameterizedFunction.length).fill(1);

  let nbPoints = data.y.length;
  let parLen = parameters.length;
  maxValues = maxValues || new Array(parLen).fill(Number.MAX_SAFE_INTEGER);
  minValues = minValues || new Array(parLen).fill(Number.MIN_SAFE_INTEGER);

  if (maxValues.length !== minValues.length) {
    throw new Error('minValues and maxValues must be the same size');
  }

  if (!isAnyArray$5(parameters)) {
    throw new Error('initialValues must be an array');
  }

  if (typeof gradientDifference === 'number') {
    gradientDifference = new Array(parameters.length).fill(gradientDifference);
  } else if (isAnyArray$5(gradientDifference)) {
    if (gradientDifference.length !== parLen) {
      gradientDifference = new Array(parLen).fill(gradientDifference[0]);
    }
  } else {
    throw new Error(
      'gradientDifference should be a number or array with length equal to the number of parameters',
    );
  }

  let filler;
  if (typeof weights === 'number') {
    let value = 1 / weights ** 2;
    filler = () => value;
  } else if (isAnyArray$5(weights)) {
    if (weights.length < data.x.length) {
      let value = 1 / weights[0] ** 2;
      filler = () => value;
    } else {
      filler = (i) => 1 / weights[i] ** 2;
    }
  } else {
    throw new Error(
      'weights should be a number or array with length equal to the number of data points',
    );
  }

  let checkTimeout;
  if (timeout !== undefined) {
    if (typeof timeout !== 'number') {
      throw new Error('timeout should be a number');
    }
    let endTime = Date.now() + timeout * 1000;
    checkTimeout = () => Date.now() > endTime;
  } else {
    checkTimeout = () => false;
  }

  let weightSquare = new Array(data.x.length);
  for (let i = 0; i < nbPoints; i++) {
    weightSquare[i] = filler(i);
  }

  return {
    checkTimeout,
    minValues,
    maxValues,
    parameters,
    weightSquare,
    damping,
    dampingStepUp,
    dampingStepDown,
    maxIterations,
    errorTolerance,
    centralDifference,
    gradientDifference,
    improvementThreshold,
  };
}

/**
 * the sum of the weighted squares of the errors (or weighted residuals) between the data.y
 * and the curve-fit function.
 * @ignore
 * @param {{x:Array<number>, y:Array<number>}} data - Array of points to fit in the format [x1, x2, ... ], [y1, y2, ... ]
 * @param {Array<number>} parameters - Array of current parameter values
 * @param {function} parameterizedFunction - The parameters and returns a function with the independent variable as a parameter
 * @param {Array} weightSquare - Square of weights
 * @return {number}
 */
function errorCalculation(
  data,
  parameters,
  parameterizedFunction,
  weightSquare,
) {
  let error = 0;
  const func = parameterizedFunction(parameters);
  for (let i = 0; i < data.x.length; i++) {
    error += Math.pow(data.y[i] - func(data.x[i]), 2) / weightSquare[i];
  }

  return error;
}

function getAugmentedNamespace(n) {
	if (n.__esModule) return n;
	var a = Object.defineProperty({}, '__esModule', {value: true});
	Object.keys(n).forEach(function (k) {
		var d = Object.getOwnPropertyDescriptor(n, k);
		Object.defineProperty(a, k, d.get ? d : {
			enumerable: true,
			get: function () {
				return n[k];
			}
		});
	});
	return a;
}

var matrix = {};

// eslint-disable-next-line @typescript-eslint/unbound-method
const toString$3 = Object.prototype.toString;
/**
 * Checks if an object is an instance of an Array (array or typed array, except those that contain bigint values).
 *
 * @param value - Object to check.
 * @returns True if the object is an array or a typed array.
 */
function isAnyArray$4(value) {
    const tag = toString$3.call(value);
    return tag.endsWith('Array]') && !tag.includes('Big');
}

var libEsm = /*#__PURE__*/Object.freeze({
  __proto__: null,
  isAnyArray: isAnyArray$4
});

var require$$0 = /*@__PURE__*/getAugmentedNamespace(libEsm);

// eslint-disable-next-line @typescript-eslint/unbound-method
const toString$2 = Object.prototype.toString;
/**
 * Checks if an object is an instance of an Array (array or typed array, except those that contain bigint values).
 *
 * @param value - Object to check.
 * @returns True if the object is an array or a typed array.
 */
function isAnyArray$3(value) {
    const tag = toString$2.call(value);
    return tag.endsWith('Array]') && !tag.includes('Big');
}

// eslint-disable-next-line @typescript-eslint/unbound-method
const toString$1$2 = Object.prototype.toString;
/**
 * Checks if an object is an instance of an Array (array or typed array, except those that contain bigint values).
 *
 * @param value - Object to check.
 * @returns True if the object is an array or a typed array.
 */
function isAnyArray$2(value) {
    const tag = toString$1$2.call(value);
    return tag.endsWith('Array]') && !tag.includes('Big');
}

function max(input) {
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

  if (!isAnyArray$2(input)) {
    throw new TypeError('input must be an array');
  }

  if (input.length === 0) {
    throw new TypeError('input must not be empty');
  }

  var _options$fromIndex = options.fromIndex,
      fromIndex = _options$fromIndex === void 0 ? 0 : _options$fromIndex,
      _options$toIndex = options.toIndex,
      toIndex = _options$toIndex === void 0 ? input.length : _options$toIndex;

  if (fromIndex < 0 || fromIndex >= input.length || !Number.isInteger(fromIndex)) {
    throw new Error('fromIndex must be a positive integer smaller than length');
  }

  if (toIndex <= fromIndex || toIndex > input.length || !Number.isInteger(toIndex)) {
    throw new Error('toIndex must be an integer greater than fromIndex and at most equal to length');
  }

  var maxValue = input[fromIndex];

  for (var i = fromIndex + 1; i < toIndex; i++) {
    if (input[i] > maxValue) maxValue = input[i];
  }

  return maxValue;
}

// eslint-disable-next-line @typescript-eslint/unbound-method
const toString$7 = Object.prototype.toString;
/**
 * Checks if an object is an instance of an Array (array or typed array, except those that contain bigint values).
 *
 * @param value - Object to check.
 * @returns True if the object is an array or a typed array.
 */
function isAnyArray$1(value) {
    const tag = toString$7.call(value);
    return tag.endsWith('Array]') && !tag.includes('Big');
}

function min(input) {
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

  if (!isAnyArray$1(input)) {
    throw new TypeError('input must be an array');
  }

  if (input.length === 0) {
    throw new TypeError('input must not be empty');
  }

  var _options$fromIndex = options.fromIndex,
      fromIndex = _options$fromIndex === void 0 ? 0 : _options$fromIndex,
      _options$toIndex = options.toIndex,
      toIndex = _options$toIndex === void 0 ? input.length : _options$toIndex;

  if (fromIndex < 0 || fromIndex >= input.length || !Number.isInteger(fromIndex)) {
    throw new Error('fromIndex must be a positive integer smaller than length');
  }

  if (toIndex <= fromIndex || toIndex > input.length || !Number.isInteger(toIndex)) {
    throw new Error('toIndex must be an integer greater than fromIndex and at most equal to length');
  }

  var minValue = input[fromIndex];

  for (var i = fromIndex + 1; i < toIndex; i++) {
    if (input[i] < minValue) minValue = input[i];
  }

  return minValue;
}

function rescale$1(input) {
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

  if (!isAnyArray$3(input)) {
    throw new TypeError('input must be an array');
  } else if (input.length === 0) {
    throw new TypeError('input must not be empty');
  }

  var output;

  if (options.output !== undefined) {
    if (!isAnyArray$3(options.output)) {
      throw new TypeError('output option must be an array if specified');
    }

    output = options.output;
  } else {
    output = new Array(input.length);
  }

  var currentMin = min(input);
  var currentMax = max(input);

  if (currentMin === currentMax) {
    throw new RangeError('minimum and maximum input values are equal. Cannot rescale a constant array');
  }

  var _options$min = options.min,
      minValue = _options$min === void 0 ? options.autoMinMax ? currentMin : 0 : _options$min,
      _options$max = options.max,
      maxValue = _options$max === void 0 ? options.autoMinMax ? currentMax : 1 : _options$max;

  if (minValue >= maxValue) {
    throw new RangeError('min option must be smaller than max option');
  }

  var factor = (maxValue - minValue) / (currentMax - currentMin);

  for (var i = 0; i < input.length; i++) {
    output[i] = (input[i] - currentMin) * factor + minValue;
  }

  return output;
}

var libEs6 = /*#__PURE__*/Object.freeze({
  __proto__: null,
  'default': rescale$1
});

var require$$1 = /*@__PURE__*/getAugmentedNamespace(libEs6);

Object.defineProperty(matrix, '__esModule', { value: true });

var isAnyArray = require$$0;
var rescale = require$$1;

const indent = ' '.repeat(2);
const indentData = ' '.repeat(4);

/**
 * @this {Matrix}
 * @returns {string}
 */
function inspectMatrix() {
  return inspectMatrixWithOptions(this);
}

function inspectMatrixWithOptions(matrix, options = {}) {
  const {
    maxRows = 15,
    maxColumns = 10,
    maxNumSize = 8,
    padMinus = 'auto',
  } = options;
  return `${matrix.constructor.name} {
${indent}[
${indentData}${inspectData(matrix, maxRows, maxColumns, maxNumSize, padMinus)}
${indent}]
${indent}rows: ${matrix.rows}
${indent}columns: ${matrix.columns}
}`;
}

function inspectData(matrix, maxRows, maxColumns, maxNumSize, padMinus) {
  const { rows, columns } = matrix;
  const maxI = Math.min(rows, maxRows);
  const maxJ = Math.min(columns, maxColumns);
  const result = [];

  if (padMinus === 'auto') {
    padMinus = false;
    loop: for (let i = 0; i < maxI; i++) {
      for (let j = 0; j < maxJ; j++) {
        if (matrix.get(i, j) < 0) {
          padMinus = true;
          break loop;
        }
      }
    }
  }

  for (let i = 0; i < maxI; i++) {
    let line = [];
    for (let j = 0; j < maxJ; j++) {
      line.push(formatNumber(matrix.get(i, j), maxNumSize, padMinus));
    }
    result.push(`${line.join(' ')}`);
  }
  if (maxJ !== columns) {
    result[result.length - 1] += ` ... ${columns - maxColumns} more columns`;
  }
  if (maxI !== rows) {
    result.push(`... ${rows - maxRows} more rows`);
  }
  return result.join(`\n${indentData}`);
}

function formatNumber(num, maxNumSize, padMinus) {
  return (
    num >= 0 && padMinus
      ? ` ${formatNumber2(num, maxNumSize - 1)}`
      : formatNumber2(num, maxNumSize)
  ).padEnd(maxNumSize);
}

function formatNumber2(num, len) {
  // small.length numbers should be as is
  let str = num.toString();
  if (str.length <= len) return str;

  // (7)'0.00123' is better then (7)'1.23e-2'
  // (8)'0.000123' is worse then (7)'1.23e-3',
  let fix = num.toFixed(len);
  if (fix.length > len) {
    fix = num.toFixed(Math.max(0, len - (fix.length - len)));
  }
  if (
    fix.length <= len &&
    !fix.startsWith('0.000') &&
    !fix.startsWith('-0.000')
  ) {
    return fix;
  }

  // well, if it's still too long the user should've used longer numbers
  let exp = num.toExponential(len);
  if (exp.length > len) {
    exp = num.toExponential(Math.max(0, len - (exp.length - len)));
  }
  return exp.slice(0);
}

function installMathOperations(AbstractMatrix, Matrix) {
  AbstractMatrix.prototype.add = function add(value) {
    if (typeof value === 'number') return this.addS(value);
    return this.addM(value);
  };

  AbstractMatrix.prototype.addS = function addS(value) {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) + value);
      }
    }
    return this;
  };

  AbstractMatrix.prototype.addM = function addM(matrix) {
    matrix = Matrix.checkMatrix(matrix);
    if (this.rows !== matrix.rows ||
      this.columns !== matrix.columns) {
      throw new RangeError('Matrices dimensions must be equal');
    }
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) + matrix.get(i, j));
      }
    }
    return this;
  };

  AbstractMatrix.add = function add(matrix, value) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.add(value);
  };

  AbstractMatrix.prototype.sub = function sub(value) {
    if (typeof value === 'number') return this.subS(value);
    return this.subM(value);
  };

  AbstractMatrix.prototype.subS = function subS(value) {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) - value);
      }
    }
    return this;
  };

  AbstractMatrix.prototype.subM = function subM(matrix) {
    matrix = Matrix.checkMatrix(matrix);
    if (this.rows !== matrix.rows ||
      this.columns !== matrix.columns) {
      throw new RangeError('Matrices dimensions must be equal');
    }
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) - matrix.get(i, j));
      }
    }
    return this;
  };

  AbstractMatrix.sub = function sub(matrix, value) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.sub(value);
  };
  AbstractMatrix.prototype.subtract = AbstractMatrix.prototype.sub;
  AbstractMatrix.prototype.subtractS = AbstractMatrix.prototype.subS;
  AbstractMatrix.prototype.subtractM = AbstractMatrix.prototype.subM;
  AbstractMatrix.subtract = AbstractMatrix.sub;

  AbstractMatrix.prototype.mul = function mul(value) {
    if (typeof value === 'number') return this.mulS(value);
    return this.mulM(value);
  };

  AbstractMatrix.prototype.mulS = function mulS(value) {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) * value);
      }
    }
    return this;
  };

  AbstractMatrix.prototype.mulM = function mulM(matrix) {
    matrix = Matrix.checkMatrix(matrix);
    if (this.rows !== matrix.rows ||
      this.columns !== matrix.columns) {
      throw new RangeError('Matrices dimensions must be equal');
    }
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) * matrix.get(i, j));
      }
    }
    return this;
  };

  AbstractMatrix.mul = function mul(matrix, value) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.mul(value);
  };
  AbstractMatrix.prototype.multiply = AbstractMatrix.prototype.mul;
  AbstractMatrix.prototype.multiplyS = AbstractMatrix.prototype.mulS;
  AbstractMatrix.prototype.multiplyM = AbstractMatrix.prototype.mulM;
  AbstractMatrix.multiply = AbstractMatrix.mul;

  AbstractMatrix.prototype.div = function div(value) {
    if (typeof value === 'number') return this.divS(value);
    return this.divM(value);
  };

  AbstractMatrix.prototype.divS = function divS(value) {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) / value);
      }
    }
    return this;
  };

  AbstractMatrix.prototype.divM = function divM(matrix) {
    matrix = Matrix.checkMatrix(matrix);
    if (this.rows !== matrix.rows ||
      this.columns !== matrix.columns) {
      throw new RangeError('Matrices dimensions must be equal');
    }
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) / matrix.get(i, j));
      }
    }
    return this;
  };

  AbstractMatrix.div = function div(matrix, value) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.div(value);
  };
  AbstractMatrix.prototype.divide = AbstractMatrix.prototype.div;
  AbstractMatrix.prototype.divideS = AbstractMatrix.prototype.divS;
  AbstractMatrix.prototype.divideM = AbstractMatrix.prototype.divM;
  AbstractMatrix.divide = AbstractMatrix.div;

  AbstractMatrix.prototype.mod = function mod(value) {
    if (typeof value === 'number') return this.modS(value);
    return this.modM(value);
  };

  AbstractMatrix.prototype.modS = function modS(value) {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) % value);
      }
    }
    return this;
  };

  AbstractMatrix.prototype.modM = function modM(matrix) {
    matrix = Matrix.checkMatrix(matrix);
    if (this.rows !== matrix.rows ||
      this.columns !== matrix.columns) {
      throw new RangeError('Matrices dimensions must be equal');
    }
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) % matrix.get(i, j));
      }
    }
    return this;
  };

  AbstractMatrix.mod = function mod(matrix, value) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.mod(value);
  };
  AbstractMatrix.prototype.modulus = AbstractMatrix.prototype.mod;
  AbstractMatrix.prototype.modulusS = AbstractMatrix.prototype.modS;
  AbstractMatrix.prototype.modulusM = AbstractMatrix.prototype.modM;
  AbstractMatrix.modulus = AbstractMatrix.mod;

  AbstractMatrix.prototype.and = function and(value) {
    if (typeof value === 'number') return this.andS(value);
    return this.andM(value);
  };

  AbstractMatrix.prototype.andS = function andS(value) {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) & value);
      }
    }
    return this;
  };

  AbstractMatrix.prototype.andM = function andM(matrix) {
    matrix = Matrix.checkMatrix(matrix);
    if (this.rows !== matrix.rows ||
      this.columns !== matrix.columns) {
      throw new RangeError('Matrices dimensions must be equal');
    }
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) & matrix.get(i, j));
      }
    }
    return this;
  };

  AbstractMatrix.and = function and(matrix, value) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.and(value);
  };

  AbstractMatrix.prototype.or = function or(value) {
    if (typeof value === 'number') return this.orS(value);
    return this.orM(value);
  };

  AbstractMatrix.prototype.orS = function orS(value) {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) | value);
      }
    }
    return this;
  };

  AbstractMatrix.prototype.orM = function orM(matrix) {
    matrix = Matrix.checkMatrix(matrix);
    if (this.rows !== matrix.rows ||
      this.columns !== matrix.columns) {
      throw new RangeError('Matrices dimensions must be equal');
    }
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) | matrix.get(i, j));
      }
    }
    return this;
  };

  AbstractMatrix.or = function or(matrix, value) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.or(value);
  };

  AbstractMatrix.prototype.xor = function xor(value) {
    if (typeof value === 'number') return this.xorS(value);
    return this.xorM(value);
  };

  AbstractMatrix.prototype.xorS = function xorS(value) {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) ^ value);
      }
    }
    return this;
  };

  AbstractMatrix.prototype.xorM = function xorM(matrix) {
    matrix = Matrix.checkMatrix(matrix);
    if (this.rows !== matrix.rows ||
      this.columns !== matrix.columns) {
      throw new RangeError('Matrices dimensions must be equal');
    }
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) ^ matrix.get(i, j));
      }
    }
    return this;
  };

  AbstractMatrix.xor = function xor(matrix, value) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.xor(value);
  };

  AbstractMatrix.prototype.leftShift = function leftShift(value) {
    if (typeof value === 'number') return this.leftShiftS(value);
    return this.leftShiftM(value);
  };

  AbstractMatrix.prototype.leftShiftS = function leftShiftS(value) {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) << value);
      }
    }
    return this;
  };

  AbstractMatrix.prototype.leftShiftM = function leftShiftM(matrix) {
    matrix = Matrix.checkMatrix(matrix);
    if (this.rows !== matrix.rows ||
      this.columns !== matrix.columns) {
      throw new RangeError('Matrices dimensions must be equal');
    }
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) << matrix.get(i, j));
      }
    }
    return this;
  };

  AbstractMatrix.leftShift = function leftShift(matrix, value) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.leftShift(value);
  };

  AbstractMatrix.prototype.signPropagatingRightShift = function signPropagatingRightShift(value) {
    if (typeof value === 'number') return this.signPropagatingRightShiftS(value);
    return this.signPropagatingRightShiftM(value);
  };

  AbstractMatrix.prototype.signPropagatingRightShiftS = function signPropagatingRightShiftS(value) {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) >> value);
      }
    }
    return this;
  };

  AbstractMatrix.prototype.signPropagatingRightShiftM = function signPropagatingRightShiftM(matrix) {
    matrix = Matrix.checkMatrix(matrix);
    if (this.rows !== matrix.rows ||
      this.columns !== matrix.columns) {
      throw new RangeError('Matrices dimensions must be equal');
    }
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) >> matrix.get(i, j));
      }
    }
    return this;
  };

  AbstractMatrix.signPropagatingRightShift = function signPropagatingRightShift(matrix, value) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.signPropagatingRightShift(value);
  };

  AbstractMatrix.prototype.rightShift = function rightShift(value) {
    if (typeof value === 'number') return this.rightShiftS(value);
    return this.rightShiftM(value);
  };

  AbstractMatrix.prototype.rightShiftS = function rightShiftS(value) {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) >>> value);
      }
    }
    return this;
  };

  AbstractMatrix.prototype.rightShiftM = function rightShiftM(matrix) {
    matrix = Matrix.checkMatrix(matrix);
    if (this.rows !== matrix.rows ||
      this.columns !== matrix.columns) {
      throw new RangeError('Matrices dimensions must be equal');
    }
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) >>> matrix.get(i, j));
      }
    }
    return this;
  };

  AbstractMatrix.rightShift = function rightShift(matrix, value) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.rightShift(value);
  };
  AbstractMatrix.prototype.zeroFillRightShift = AbstractMatrix.prototype.rightShift;
  AbstractMatrix.prototype.zeroFillRightShiftS = AbstractMatrix.prototype.rightShiftS;
  AbstractMatrix.prototype.zeroFillRightShiftM = AbstractMatrix.prototype.rightShiftM;
  AbstractMatrix.zeroFillRightShift = AbstractMatrix.rightShift;

  AbstractMatrix.prototype.not = function not() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, ~(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.not = function not(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.not();
  };

  AbstractMatrix.prototype.abs = function abs() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.abs(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.abs = function abs(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.abs();
  };

  AbstractMatrix.prototype.acos = function acos() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.acos(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.acos = function acos(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.acos();
  };

  AbstractMatrix.prototype.acosh = function acosh() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.acosh(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.acosh = function acosh(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.acosh();
  };

  AbstractMatrix.prototype.asin = function asin() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.asin(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.asin = function asin(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.asin();
  };

  AbstractMatrix.prototype.asinh = function asinh() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.asinh(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.asinh = function asinh(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.asinh();
  };

  AbstractMatrix.prototype.atan = function atan() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.atan(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.atan = function atan(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.atan();
  };

  AbstractMatrix.prototype.atanh = function atanh() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.atanh(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.atanh = function atanh(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.atanh();
  };

  AbstractMatrix.prototype.cbrt = function cbrt() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.cbrt(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.cbrt = function cbrt(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.cbrt();
  };

  AbstractMatrix.prototype.ceil = function ceil() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.ceil(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.ceil = function ceil(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.ceil();
  };

  AbstractMatrix.prototype.clz32 = function clz32() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.clz32(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.clz32 = function clz32(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.clz32();
  };

  AbstractMatrix.prototype.cos = function cos() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.cos(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.cos = function cos(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.cos();
  };

  AbstractMatrix.prototype.cosh = function cosh() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.cosh(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.cosh = function cosh(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.cosh();
  };

  AbstractMatrix.prototype.exp = function exp() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.exp(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.exp = function exp(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.exp();
  };

  AbstractMatrix.prototype.expm1 = function expm1() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.expm1(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.expm1 = function expm1(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.expm1();
  };

  AbstractMatrix.prototype.floor = function floor() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.floor(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.floor = function floor(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.floor();
  };

  AbstractMatrix.prototype.fround = function fround() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.fround(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.fround = function fround(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.fround();
  };

  AbstractMatrix.prototype.log = function log() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.log(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.log = function log(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.log();
  };

  AbstractMatrix.prototype.log1p = function log1p() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.log1p(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.log1p = function log1p(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.log1p();
  };

  AbstractMatrix.prototype.log10 = function log10() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.log10(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.log10 = function log10(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.log10();
  };

  AbstractMatrix.prototype.log2 = function log2() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.log2(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.log2 = function log2(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.log2();
  };

  AbstractMatrix.prototype.round = function round() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.round(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.round = function round(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.round();
  };

  AbstractMatrix.prototype.sign = function sign() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.sign(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.sign = function sign(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.sign();
  };

  AbstractMatrix.prototype.sin = function sin() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.sin(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.sin = function sin(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.sin();
  };

  AbstractMatrix.prototype.sinh = function sinh() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.sinh(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.sinh = function sinh(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.sinh();
  };

  AbstractMatrix.prototype.sqrt = function sqrt() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.sqrt(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.sqrt = function sqrt(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.sqrt();
  };

  AbstractMatrix.prototype.tan = function tan() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.tan(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.tan = function tan(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.tan();
  };

  AbstractMatrix.prototype.tanh = function tanh() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.tanh(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.tanh = function tanh(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.tanh();
  };

  AbstractMatrix.prototype.trunc = function trunc() {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, Math.trunc(this.get(i, j)));
      }
    }
    return this;
  };

  AbstractMatrix.trunc = function trunc(matrix) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.trunc();
  };

  AbstractMatrix.pow = function pow(matrix, arg0) {
    const newMatrix = new Matrix(matrix);
    return newMatrix.pow(arg0);
  };

  AbstractMatrix.prototype.pow = function pow(value) {
    if (typeof value === 'number') return this.powS(value);
    return this.powM(value);
  };

  AbstractMatrix.prototype.powS = function powS(value) {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) ** value);
      }
    }
    return this;
  };

  AbstractMatrix.prototype.powM = function powM(matrix) {
    matrix = Matrix.checkMatrix(matrix);
    if (this.rows !== matrix.rows ||
      this.columns !== matrix.columns) {
      throw new RangeError('Matrices dimensions must be equal');
    }
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) ** matrix.get(i, j));
      }
    }
    return this;
  };
}

/**
 * @private
 * Check that a row index is not out of bounds
 * @param {Matrix} matrix
 * @param {number} index
 * @param {boolean} [outer]
 */
function checkRowIndex(matrix, index, outer) {
  let max = outer ? matrix.rows : matrix.rows - 1;
  if (index < 0 || index > max) {
    throw new RangeError('Row index out of range');
  }
}

/**
 * @private
 * Check that a column index is not out of bounds
 * @param {Matrix} matrix
 * @param {number} index
 * @param {boolean} [outer]
 */
function checkColumnIndex(matrix, index, outer) {
  let max = outer ? matrix.columns : matrix.columns - 1;
  if (index < 0 || index > max) {
    throw new RangeError('Column index out of range');
  }
}

/**
 * @private
 * Check that the provided vector is an array with the right length
 * @param {Matrix} matrix
 * @param {Array|Matrix} vector
 * @return {Array}
 * @throws {RangeError}
 */
function checkRowVector(matrix, vector) {
  if (vector.to1DArray) {
    vector = vector.to1DArray();
  }
  if (vector.length !== matrix.columns) {
    throw new RangeError(
      'vector size must be the same as the number of columns',
    );
  }
  return vector;
}

/**
 * @private
 * Check that the provided vector is an array with the right length
 * @param {Matrix} matrix
 * @param {Array|Matrix} vector
 * @return {Array}
 * @throws {RangeError}
 */
function checkColumnVector(matrix, vector) {
  if (vector.to1DArray) {
    vector = vector.to1DArray();
  }
  if (vector.length !== matrix.rows) {
    throw new RangeError('vector size must be the same as the number of rows');
  }
  return vector;
}

function checkRowIndices(matrix, rowIndices) {
  if (!isAnyArray.isAnyArray(rowIndices)) {
    throw new TypeError('row indices must be an array');
  }

  for (let i = 0; i < rowIndices.length; i++) {
    if (rowIndices[i] < 0 || rowIndices[i] >= matrix.rows) {
      throw new RangeError('row indices are out of range');
    }
  }
}

function checkColumnIndices(matrix, columnIndices) {
  if (!isAnyArray.isAnyArray(columnIndices)) {
    throw new TypeError('column indices must be an array');
  }

  for (let i = 0; i < columnIndices.length; i++) {
    if (columnIndices[i] < 0 || columnIndices[i] >= matrix.columns) {
      throw new RangeError('column indices are out of range');
    }
  }
}

function checkRange(matrix, startRow, endRow, startColumn, endColumn) {
  if (arguments.length !== 5) {
    throw new RangeError('expected 4 arguments');
  }
  checkNumber('startRow', startRow);
  checkNumber('endRow', endRow);
  checkNumber('startColumn', startColumn);
  checkNumber('endColumn', endColumn);
  if (
    startRow > endRow ||
    startColumn > endColumn ||
    startRow < 0 ||
    startRow >= matrix.rows ||
    endRow < 0 ||
    endRow >= matrix.rows ||
    startColumn < 0 ||
    startColumn >= matrix.columns ||
    endColumn < 0 ||
    endColumn >= matrix.columns
  ) {
    throw new RangeError('Submatrix indices are out of range');
  }
}

function newArray(length, value = 0) {
  let array = [];
  for (let i = 0; i < length; i++) {
    array.push(value);
  }
  return array;
}

function checkNumber(name, value) {
  if (typeof value !== 'number') {
    throw new TypeError(`${name} must be a number`);
  }
}

function checkNonEmpty(matrix) {
  if (matrix.isEmpty()) {
    throw new Error('Empty matrix has no elements to index');
  }
}

function sumByRow(matrix) {
  let sum = newArray(matrix.rows);
  for (let i = 0; i < matrix.rows; ++i) {
    for (let j = 0; j < matrix.columns; ++j) {
      sum[i] += matrix.get(i, j);
    }
  }
  return sum;
}

function sumByColumn(matrix) {
  let sum = newArray(matrix.columns);
  for (let i = 0; i < matrix.rows; ++i) {
    for (let j = 0; j < matrix.columns; ++j) {
      sum[j] += matrix.get(i, j);
    }
  }
  return sum;
}

function sumAll(matrix) {
  let v = 0;
  for (let i = 0; i < matrix.rows; i++) {
    for (let j = 0; j < matrix.columns; j++) {
      v += matrix.get(i, j);
    }
  }
  return v;
}

function productByRow(matrix) {
  let sum = newArray(matrix.rows, 1);
  for (let i = 0; i < matrix.rows; ++i) {
    for (let j = 0; j < matrix.columns; ++j) {
      sum[i] *= matrix.get(i, j);
    }
  }
  return sum;
}

function productByColumn(matrix) {
  let sum = newArray(matrix.columns, 1);
  for (let i = 0; i < matrix.rows; ++i) {
    for (let j = 0; j < matrix.columns; ++j) {
      sum[j] *= matrix.get(i, j);
    }
  }
  return sum;
}

function productAll(matrix) {
  let v = 1;
  for (let i = 0; i < matrix.rows; i++) {
    for (let j = 0; j < matrix.columns; j++) {
      v *= matrix.get(i, j);
    }
  }
  return v;
}

function varianceByRow(matrix, unbiased, mean) {
  const rows = matrix.rows;
  const cols = matrix.columns;
  const variance = [];

  for (let i = 0; i < rows; i++) {
    let sum1 = 0;
    let sum2 = 0;
    let x = 0;
    for (let j = 0; j < cols; j++) {
      x = matrix.get(i, j) - mean[i];
      sum1 += x;
      sum2 += x * x;
    }
    if (unbiased) {
      variance.push((sum2 - (sum1 * sum1) / cols) / (cols - 1));
    } else {
      variance.push((sum2 - (sum1 * sum1) / cols) / cols);
    }
  }
  return variance;
}

function varianceByColumn(matrix, unbiased, mean) {
  const rows = matrix.rows;
  const cols = matrix.columns;
  const variance = [];

  for (let j = 0; j < cols; j++) {
    let sum1 = 0;
    let sum2 = 0;
    let x = 0;
    for (let i = 0; i < rows; i++) {
      x = matrix.get(i, j) - mean[j];
      sum1 += x;
      sum2 += x * x;
    }
    if (unbiased) {
      variance.push((sum2 - (sum1 * sum1) / rows) / (rows - 1));
    } else {
      variance.push((sum2 - (sum1 * sum1) / rows) / rows);
    }
  }
  return variance;
}

function varianceAll(matrix, unbiased, mean) {
  const rows = matrix.rows;
  const cols = matrix.columns;
  const size = rows * cols;

  let sum1 = 0;
  let sum2 = 0;
  let x = 0;
  for (let i = 0; i < rows; i++) {
    for (let j = 0; j < cols; j++) {
      x = matrix.get(i, j) - mean;
      sum1 += x;
      sum2 += x * x;
    }
  }
  if (unbiased) {
    return (sum2 - (sum1 * sum1) / size) / (size - 1);
  } else {
    return (sum2 - (sum1 * sum1) / size) / size;
  }
}

function centerByRow(matrix, mean) {
  for (let i = 0; i < matrix.rows; i++) {
    for (let j = 0; j < matrix.columns; j++) {
      matrix.set(i, j, matrix.get(i, j) - mean[i]);
    }
  }
}

function centerByColumn(matrix, mean) {
  for (let i = 0; i < matrix.rows; i++) {
    for (let j = 0; j < matrix.columns; j++) {
      matrix.set(i, j, matrix.get(i, j) - mean[j]);
    }
  }
}

function centerAll(matrix, mean) {
  for (let i = 0; i < matrix.rows; i++) {
    for (let j = 0; j < matrix.columns; j++) {
      matrix.set(i, j, matrix.get(i, j) - mean);
    }
  }
}

function getScaleByRow(matrix) {
  const scale = [];
  for (let i = 0; i < matrix.rows; i++) {
    let sum = 0;
    for (let j = 0; j < matrix.columns; j++) {
      sum += matrix.get(i, j) ** 2 / (matrix.columns - 1);
    }
    scale.push(Math.sqrt(sum));
  }
  return scale;
}

function scaleByRow(matrix, scale) {
  for (let i = 0; i < matrix.rows; i++) {
    for (let j = 0; j < matrix.columns; j++) {
      matrix.set(i, j, matrix.get(i, j) / scale[i]);
    }
  }
}

function getScaleByColumn(matrix) {
  const scale = [];
  for (let j = 0; j < matrix.columns; j++) {
    let sum = 0;
    for (let i = 0; i < matrix.rows; i++) {
      sum += matrix.get(i, j) ** 2 / (matrix.rows - 1);
    }
    scale.push(Math.sqrt(sum));
  }
  return scale;
}

function scaleByColumn(matrix, scale) {
  for (let i = 0; i < matrix.rows; i++) {
    for (let j = 0; j < matrix.columns; j++) {
      matrix.set(i, j, matrix.get(i, j) / scale[j]);
    }
  }
}

function getScaleAll(matrix) {
  const divider = matrix.size - 1;
  let sum = 0;
  for (let j = 0; j < matrix.columns; j++) {
    for (let i = 0; i < matrix.rows; i++) {
      sum += matrix.get(i, j) ** 2 / divider;
    }
  }
  return Math.sqrt(sum);
}

function scaleAll(matrix, scale) {
  for (let i = 0; i < matrix.rows; i++) {
    for (let j = 0; j < matrix.columns; j++) {
      matrix.set(i, j, matrix.get(i, j) / scale);
    }
  }
}

class AbstractMatrix {
  static from1DArray(newRows, newColumns, newData) {
    let length = newRows * newColumns;
    if (length !== newData.length) {
      throw new RangeError('data length does not match given dimensions');
    }
    let newMatrix = new Matrix$1(newRows, newColumns);
    for (let row = 0; row < newRows; row++) {
      for (let column = 0; column < newColumns; column++) {
        newMatrix.set(row, column, newData[row * newColumns + column]);
      }
    }
    return newMatrix;
  }

  static rowVector(newData) {
    let vector = new Matrix$1(1, newData.length);
    for (let i = 0; i < newData.length; i++) {
      vector.set(0, i, newData[i]);
    }
    return vector;
  }

  static columnVector(newData) {
    let vector = new Matrix$1(newData.length, 1);
    for (let i = 0; i < newData.length; i++) {
      vector.set(i, 0, newData[i]);
    }
    return vector;
  }

  static zeros(rows, columns) {
    return new Matrix$1(rows, columns);
  }

  static ones(rows, columns) {
    return new Matrix$1(rows, columns).fill(1);
  }

  static rand(rows, columns, options = {}) {
    if (typeof options !== 'object') {
      throw new TypeError('options must be an object');
    }
    const { random = Math.random } = options;
    let matrix = new Matrix$1(rows, columns);
    for (let i = 0; i < rows; i++) {
      for (let j = 0; j < columns; j++) {
        matrix.set(i, j, random());
      }
    }
    return matrix;
  }

  static randInt(rows, columns, options = {}) {
    if (typeof options !== 'object') {
      throw new TypeError('options must be an object');
    }
    const { min = 0, max = 1000, random = Math.random } = options;
    if (!Number.isInteger(min)) throw new TypeError('min must be an integer');
    if (!Number.isInteger(max)) throw new TypeError('max must be an integer');
    if (min >= max) throw new RangeError('min must be smaller than max');
    let interval = max - min;
    let matrix = new Matrix$1(rows, columns);
    for (let i = 0; i < rows; i++) {
      for (let j = 0; j < columns; j++) {
        let value = min + Math.round(random() * interval);
        matrix.set(i, j, value);
      }
    }
    return matrix;
  }

  static eye(rows, columns, value) {
    if (columns === undefined) columns = rows;
    if (value === undefined) value = 1;
    let min = Math.min(rows, columns);
    let matrix = this.zeros(rows, columns);
    for (let i = 0; i < min; i++) {
      matrix.set(i, i, value);
    }
    return matrix;
  }

  static diag(data, rows, columns) {
    let l = data.length;
    if (rows === undefined) rows = l;
    if (columns === undefined) columns = rows;
    let min = Math.min(l, rows, columns);
    let matrix = this.zeros(rows, columns);
    for (let i = 0; i < min; i++) {
      matrix.set(i, i, data[i]);
    }
    return matrix;
  }

  static min(matrix1, matrix2) {
    matrix1 = this.checkMatrix(matrix1);
    matrix2 = this.checkMatrix(matrix2);
    let rows = matrix1.rows;
    let columns = matrix1.columns;
    let result = new Matrix$1(rows, columns);
    for (let i = 0; i < rows; i++) {
      for (let j = 0; j < columns; j++) {
        result.set(i, j, Math.min(matrix1.get(i, j), matrix2.get(i, j)));
      }
    }
    return result;
  }

  static max(matrix1, matrix2) {
    matrix1 = this.checkMatrix(matrix1);
    matrix2 = this.checkMatrix(matrix2);
    let rows = matrix1.rows;
    let columns = matrix1.columns;
    let result = new this(rows, columns);
    for (let i = 0; i < rows; i++) {
      for (let j = 0; j < columns; j++) {
        result.set(i, j, Math.max(matrix1.get(i, j), matrix2.get(i, j)));
      }
    }
    return result;
  }

  static checkMatrix(value) {
    return AbstractMatrix.isMatrix(value) ? value : new Matrix$1(value);
  }

  static isMatrix(value) {
    return value != null && value.klass === 'Matrix';
  }

  get size() {
    return this.rows * this.columns;
  }

  apply(callback) {
    if (typeof callback !== 'function') {
      throw new TypeError('callback must be a function');
    }
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        callback.call(this, i, j);
      }
    }
    return this;
  }

  to1DArray() {
    let array = [];
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        array.push(this.get(i, j));
      }
    }
    return array;
  }

  to2DArray() {
    let copy = [];
    for (let i = 0; i < this.rows; i++) {
      copy.push([]);
      for (let j = 0; j < this.columns; j++) {
        copy[i].push(this.get(i, j));
      }
    }
    return copy;
  }

  toJSON() {
    return this.to2DArray();
  }

  isRowVector() {
    return this.rows === 1;
  }

  isColumnVector() {
    return this.columns === 1;
  }

  isVector() {
    return this.rows === 1 || this.columns === 1;
  }

  isSquare() {
    return this.rows === this.columns;
  }

  isEmpty() {
    return this.rows === 0 || this.columns === 0;
  }

  isSymmetric() {
    if (this.isSquare()) {
      for (let i = 0; i < this.rows; i++) {
        for (let j = 0; j <= i; j++) {
          if (this.get(i, j) !== this.get(j, i)) {
            return false;
          }
        }
      }
      return true;
    }
    return false;
  }

  isDistance() {
    if (!this.isSymmetric()) return false;

    for (let i = 0; i < this.rows; i++) {
      if (this.get(i, i) !== 0) return false;
    }

    return true;
  }

  isEchelonForm() {
    let i = 0;
    let j = 0;
    let previousColumn = -1;
    let isEchelonForm = true;
    let checked = false;
    while (i < this.rows && isEchelonForm) {
      j = 0;
      checked = false;
      while (j < this.columns && checked === false) {
        if (this.get(i, j) === 0) {
          j++;
        } else if (this.get(i, j) === 1 && j > previousColumn) {
          checked = true;
          previousColumn = j;
        } else {
          isEchelonForm = false;
          checked = true;
        }
      }
      i++;
    }
    return isEchelonForm;
  }

  isReducedEchelonForm() {
    let i = 0;
    let j = 0;
    let previousColumn = -1;
    let isReducedEchelonForm = true;
    let checked = false;
    while (i < this.rows && isReducedEchelonForm) {
      j = 0;
      checked = false;
      while (j < this.columns && checked === false) {
        if (this.get(i, j) === 0) {
          j++;
        } else if (this.get(i, j) === 1 && j > previousColumn) {
          checked = true;
          previousColumn = j;
        } else {
          isReducedEchelonForm = false;
          checked = true;
        }
      }
      for (let k = j + 1; k < this.rows; k++) {
        if (this.get(i, k) !== 0) {
          isReducedEchelonForm = false;
        }
      }
      i++;
    }
    return isReducedEchelonForm;
  }

  echelonForm() {
    let result = this.clone();
    let h = 0;
    let k = 0;
    while (h < result.rows && k < result.columns) {
      let iMax = h;
      for (let i = h; i < result.rows; i++) {
        if (result.get(i, k) > result.get(iMax, k)) {
          iMax = i;
        }
      }
      if (result.get(iMax, k) === 0) {
        k++;
      } else {
        result.swapRows(h, iMax);
        let tmp = result.get(h, k);
        for (let j = k; j < result.columns; j++) {
          result.set(h, j, result.get(h, j) / tmp);
        }
        for (let i = h + 1; i < result.rows; i++) {
          let factor = result.get(i, k) / result.get(h, k);
          result.set(i, k, 0);
          for (let j = k + 1; j < result.columns; j++) {
            result.set(i, j, result.get(i, j) - result.get(h, j) * factor);
          }
        }
        h++;
        k++;
      }
    }
    return result;
  }

  reducedEchelonForm() {
    let result = this.echelonForm();
    let m = result.columns;
    let n = result.rows;
    let h = n - 1;
    while (h >= 0) {
      if (result.maxRow(h) === 0) {
        h--;
      } else {
        let p = 0;
        let pivot = false;
        while (p < n && pivot === false) {
          if (result.get(h, p) === 1) {
            pivot = true;
          } else {
            p++;
          }
        }
        for (let i = 0; i < h; i++) {
          let factor = result.get(i, p);
          for (let j = p; j < m; j++) {
            let tmp = result.get(i, j) - factor * result.get(h, j);
            result.set(i, j, tmp);
          }
        }
        h--;
      }
    }
    return result;
  }

  set() {
    throw new Error('set method is unimplemented');
  }

  get() {
    throw new Error('get method is unimplemented');
  }

  repeat(options = {}) {
    if (typeof options !== 'object') {
      throw new TypeError('options must be an object');
    }
    const { rows = 1, columns = 1 } = options;
    if (!Number.isInteger(rows) || rows <= 0) {
      throw new TypeError('rows must be a positive integer');
    }
    if (!Number.isInteger(columns) || columns <= 0) {
      throw new TypeError('columns must be a positive integer');
    }
    let matrix = new Matrix$1(this.rows * rows, this.columns * columns);
    for (let i = 0; i < rows; i++) {
      for (let j = 0; j < columns; j++) {
        matrix.setSubMatrix(this, this.rows * i, this.columns * j);
      }
    }
    return matrix;
  }

  fill(value) {
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, value);
      }
    }
    return this;
  }

  neg() {
    return this.mulS(-1);
  }

  getRow(index) {
    checkRowIndex(this, index);
    let row = [];
    for (let i = 0; i < this.columns; i++) {
      row.push(this.get(index, i));
    }
    return row;
  }

  getRowVector(index) {
    return Matrix$1.rowVector(this.getRow(index));
  }

  setRow(index, array) {
    checkRowIndex(this, index);
    array = checkRowVector(this, array);
    for (let i = 0; i < this.columns; i++) {
      this.set(index, i, array[i]);
    }
    return this;
  }

  swapRows(row1, row2) {
    checkRowIndex(this, row1);
    checkRowIndex(this, row2);
    for (let i = 0; i < this.columns; i++) {
      let temp = this.get(row1, i);
      this.set(row1, i, this.get(row2, i));
      this.set(row2, i, temp);
    }
    return this;
  }

  getColumn(index) {
    checkColumnIndex(this, index);
    let column = [];
    for (let i = 0; i < this.rows; i++) {
      column.push(this.get(i, index));
    }
    return column;
  }

  getColumnVector(index) {
    return Matrix$1.columnVector(this.getColumn(index));
  }

  setColumn(index, array) {
    checkColumnIndex(this, index);
    array = checkColumnVector(this, array);
    for (let i = 0; i < this.rows; i++) {
      this.set(i, index, array[i]);
    }
    return this;
  }

  swapColumns(column1, column2) {
    checkColumnIndex(this, column1);
    checkColumnIndex(this, column2);
    for (let i = 0; i < this.rows; i++) {
      let temp = this.get(i, column1);
      this.set(i, column1, this.get(i, column2));
      this.set(i, column2, temp);
    }
    return this;
  }

  addRowVector(vector) {
    vector = checkRowVector(this, vector);
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) + vector[j]);
      }
    }
    return this;
  }

  subRowVector(vector) {
    vector = checkRowVector(this, vector);
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) - vector[j]);
      }
    }
    return this;
  }

  mulRowVector(vector) {
    vector = checkRowVector(this, vector);
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) * vector[j]);
      }
    }
    return this;
  }

  divRowVector(vector) {
    vector = checkRowVector(this, vector);
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) / vector[j]);
      }
    }
    return this;
  }

  addColumnVector(vector) {
    vector = checkColumnVector(this, vector);
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) + vector[i]);
      }
    }
    return this;
  }

  subColumnVector(vector) {
    vector = checkColumnVector(this, vector);
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) - vector[i]);
      }
    }
    return this;
  }

  mulColumnVector(vector) {
    vector = checkColumnVector(this, vector);
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) * vector[i]);
      }
    }
    return this;
  }

  divColumnVector(vector) {
    vector = checkColumnVector(this, vector);
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        this.set(i, j, this.get(i, j) / vector[i]);
      }
    }
    return this;
  }

  mulRow(index, value) {
    checkRowIndex(this, index);
    for (let i = 0; i < this.columns; i++) {
      this.set(index, i, this.get(index, i) * value);
    }
    return this;
  }

  mulColumn(index, value) {
    checkColumnIndex(this, index);
    for (let i = 0; i < this.rows; i++) {
      this.set(i, index, this.get(i, index) * value);
    }
    return this;
  }

  max(by) {
    if (this.isEmpty()) {
      return NaN;
    }
    switch (by) {
      case 'row': {
        const max = new Array(this.rows).fill(Number.NEGATIVE_INFINITY);
        for (let row = 0; row < this.rows; row++) {
          for (let column = 0; column < this.columns; column++) {
            if (this.get(row, column) > max[row]) {
              max[row] = this.get(row, column);
            }
          }
        }
        return max;
      }
      case 'column': {
        const max = new Array(this.columns).fill(Number.NEGATIVE_INFINITY);
        for (let row = 0; row < this.rows; row++) {
          for (let column = 0; column < this.columns; column++) {
            if (this.get(row, column) > max[column]) {
              max[column] = this.get(row, column);
            }
          }
        }
        return max;
      }
      case undefined: {
        let max = this.get(0, 0);
        for (let row = 0; row < this.rows; row++) {
          for (let column = 0; column < this.columns; column++) {
            if (this.get(row, column) > max) {
              max = this.get(row, column);
            }
          }
        }
        return max;
      }
      default:
        throw new Error(`invalid option: ${by}`);
    }
  }

  maxIndex() {
    checkNonEmpty(this);
    let v = this.get(0, 0);
    let idx = [0, 0];
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        if (this.get(i, j) > v) {
          v = this.get(i, j);
          idx[0] = i;
          idx[1] = j;
        }
      }
    }
    return idx;
  }

  min(by) {
    if (this.isEmpty()) {
      return NaN;
    }

    switch (by) {
      case 'row': {
        const min = new Array(this.rows).fill(Number.POSITIVE_INFINITY);
        for (let row = 0; row < this.rows; row++) {
          for (let column = 0; column < this.columns; column++) {
            if (this.get(row, column) < min[row]) {
              min[row] = this.get(row, column);
            }
          }
        }
        return min;
      }
      case 'column': {
        const min = new Array(this.columns).fill(Number.POSITIVE_INFINITY);
        for (let row = 0; row < this.rows; row++) {
          for (let column = 0; column < this.columns; column++) {
            if (this.get(row, column) < min[column]) {
              min[column] = this.get(row, column);
            }
          }
        }
        return min;
      }
      case undefined: {
        let min = this.get(0, 0);
        for (let row = 0; row < this.rows; row++) {
          for (let column = 0; column < this.columns; column++) {
            if (this.get(row, column) < min) {
              min = this.get(row, column);
            }
          }
        }
        return min;
      }
      default:
        throw new Error(`invalid option: ${by}`);
    }
  }

  minIndex() {
    checkNonEmpty(this);
    let v = this.get(0, 0);
    let idx = [0, 0];
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        if (this.get(i, j) < v) {
          v = this.get(i, j);
          idx[0] = i;
          idx[1] = j;
        }
      }
    }
    return idx;
  }

  maxRow(row) {
    checkRowIndex(this, row);
    if (this.isEmpty()) {
      return NaN;
    }
    let v = this.get(row, 0);
    for (let i = 1; i < this.columns; i++) {
      if (this.get(row, i) > v) {
        v = this.get(row, i);
      }
    }
    return v;
  }

  maxRowIndex(row) {
    checkRowIndex(this, row);
    checkNonEmpty(this);
    let v = this.get(row, 0);
    let idx = [row, 0];
    for (let i = 1; i < this.columns; i++) {
      if (this.get(row, i) > v) {
        v = this.get(row, i);
        idx[1] = i;
      }
    }
    return idx;
  }

  minRow(row) {
    checkRowIndex(this, row);
    if (this.isEmpty()) {
      return NaN;
    }
    let v = this.get(row, 0);
    for (let i = 1; i < this.columns; i++) {
      if (this.get(row, i) < v) {
        v = this.get(row, i);
      }
    }
    return v;
  }

  minRowIndex(row) {
    checkRowIndex(this, row);
    checkNonEmpty(this);
    let v = this.get(row, 0);
    let idx = [row, 0];
    for (let i = 1; i < this.columns; i++) {
      if (this.get(row, i) < v) {
        v = this.get(row, i);
        idx[1] = i;
      }
    }
    return idx;
  }

  maxColumn(column) {
    checkColumnIndex(this, column);
    if (this.isEmpty()) {
      return NaN;
    }
    let v = this.get(0, column);
    for (let i = 1; i < this.rows; i++) {
      if (this.get(i, column) > v) {
        v = this.get(i, column);
      }
    }
    return v;
  }

  maxColumnIndex(column) {
    checkColumnIndex(this, column);
    checkNonEmpty(this);
    let v = this.get(0, column);
    let idx = [0, column];
    for (let i = 1; i < this.rows; i++) {
      if (this.get(i, column) > v) {
        v = this.get(i, column);
        idx[0] = i;
      }
    }
    return idx;
  }

  minColumn(column) {
    checkColumnIndex(this, column);
    if (this.isEmpty()) {
      return NaN;
    }
    let v = this.get(0, column);
    for (let i = 1; i < this.rows; i++) {
      if (this.get(i, column) < v) {
        v = this.get(i, column);
      }
    }
    return v;
  }

  minColumnIndex(column) {
    checkColumnIndex(this, column);
    checkNonEmpty(this);
    let v = this.get(0, column);
    let idx = [0, column];
    for (let i = 1; i < this.rows; i++) {
      if (this.get(i, column) < v) {
        v = this.get(i, column);
        idx[0] = i;
      }
    }
    return idx;
  }

  diag() {
    let min = Math.min(this.rows, this.columns);
    let diag = [];
    for (let i = 0; i < min; i++) {
      diag.push(this.get(i, i));
    }
    return diag;
  }

  norm(type = 'frobenius') {
    switch (type) {
      case 'max':
        return this.max();
      case 'frobenius':
        return Math.sqrt(this.dot(this));
      default:
        throw new RangeError(`unknown norm type: ${type}`);
    }
  }

  cumulativeSum() {
    let sum = 0;
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        sum += this.get(i, j);
        this.set(i, j, sum);
      }
    }
    return this;
  }

  dot(vector2) {
    if (AbstractMatrix.isMatrix(vector2)) vector2 = vector2.to1DArray();
    let vector1 = this.to1DArray();
    if (vector1.length !== vector2.length) {
      throw new RangeError('vectors do not have the same size');
    }
    let dot = 0;
    for (let i = 0; i < vector1.length; i++) {
      dot += vector1[i] * vector2[i];
    }
    return dot;
  }

  mmul(other) {
    other = Matrix$1.checkMatrix(other);

    let m = this.rows;
    let n = this.columns;
    let p = other.columns;

    let result = new Matrix$1(m, p);

    let Bcolj = new Float64Array(n);
    for (let j = 0; j < p; j++) {
      for (let k = 0; k < n; k++) {
        Bcolj[k] = other.get(k, j);
      }

      for (let i = 0; i < m; i++) {
        let s = 0;
        for (let k = 0; k < n; k++) {
          s += this.get(i, k) * Bcolj[k];
        }

        result.set(i, j, s);
      }
    }
    return result;
  }

  mpow(scalar) {
    if (!this.isSquare()) {
      throw new RangeError('Matrix must be square');
    }
    if (!Number.isInteger(scalar) || scalar < 0) {
      throw new RangeError('Exponent must be a non-negative integer');
    }
    // Russian Peasant exponentiation, i.e. exponentiation by squaring
    let result = Matrix$1.eye(this.rows);
    let bb = this;
    // Note: Don't bit shift. In JS, that would truncate at 32 bits
    for (let e = scalar; e > 1; e /= 2) {
      if ((e & 1) !== 0) {
        result = result.mmul(bb);
      }
      bb = bb.mmul(bb);
    }
    return result;
  }

  strassen2x2(other) {
    other = Matrix$1.checkMatrix(other);
    let result = new Matrix$1(2, 2);
    const a11 = this.get(0, 0);
    const b11 = other.get(0, 0);
    const a12 = this.get(0, 1);
    const b12 = other.get(0, 1);
    const a21 = this.get(1, 0);
    const b21 = other.get(1, 0);
    const a22 = this.get(1, 1);
    const b22 = other.get(1, 1);

    // Compute intermediate values.
    const m1 = (a11 + a22) * (b11 + b22);
    const m2 = (a21 + a22) * b11;
    const m3 = a11 * (b12 - b22);
    const m4 = a22 * (b21 - b11);
    const m5 = (a11 + a12) * b22;
    const m6 = (a21 - a11) * (b11 + b12);
    const m7 = (a12 - a22) * (b21 + b22);

    // Combine intermediate values into the output.
    const c00 = m1 + m4 - m5 + m7;
    const c01 = m3 + m5;
    const c10 = m2 + m4;
    const c11 = m1 - m2 + m3 + m6;

    result.set(0, 0, c00);
    result.set(0, 1, c01);
    result.set(1, 0, c10);
    result.set(1, 1, c11);
    return result;
  }

  strassen3x3(other) {
    other = Matrix$1.checkMatrix(other);
    let result = new Matrix$1(3, 3);

    const a00 = this.get(0, 0);
    const a01 = this.get(0, 1);
    const a02 = this.get(0, 2);
    const a10 = this.get(1, 0);
    const a11 = this.get(1, 1);
    const a12 = this.get(1, 2);
    const a20 = this.get(2, 0);
    const a21 = this.get(2, 1);
    const a22 = this.get(2, 2);

    const b00 = other.get(0, 0);
    const b01 = other.get(0, 1);
    const b02 = other.get(0, 2);
    const b10 = other.get(1, 0);
    const b11 = other.get(1, 1);
    const b12 = other.get(1, 2);
    const b20 = other.get(2, 0);
    const b21 = other.get(2, 1);
    const b22 = other.get(2, 2);

    const m1 = (a00 + a01 + a02 - a10 - a11 - a21 - a22) * b11;
    const m2 = (a00 - a10) * (-b01 + b11);
    const m3 = a11 * (-b00 + b01 + b10 - b11 - b12 - b20 + b22);
    const m4 = (-a00 + a10 + a11) * (b00 - b01 + b11);
    const m5 = (a10 + a11) * (-b00 + b01);
    const m6 = a00 * b00;
    const m7 = (-a00 + a20 + a21) * (b00 - b02 + b12);
    const m8 = (-a00 + a20) * (b02 - b12);
    const m9 = (a20 + a21) * (-b00 + b02);
    const m10 = (a00 + a01 + a02 - a11 - a12 - a20 - a21) * b12;
    const m11 = a21 * (-b00 + b02 + b10 - b11 - b12 - b20 + b21);
    const m12 = (-a02 + a21 + a22) * (b11 + b20 - b21);
    const m13 = (a02 - a22) * (b11 - b21);
    const m14 = a02 * b20;
    const m15 = (a21 + a22) * (-b20 + b21);
    const m16 = (-a02 + a11 + a12) * (b12 + b20 - b22);
    const m17 = (a02 - a12) * (b12 - b22);
    const m18 = (a11 + a12) * (-b20 + b22);
    const m19 = a01 * b10;
    const m20 = a12 * b21;
    const m21 = a10 * b02;
    const m22 = a20 * b01;
    const m23 = a22 * b22;

    const c00 = m6 + m14 + m19;
    const c01 = m1 + m4 + m5 + m6 + m12 + m14 + m15;
    const c02 = m6 + m7 + m9 + m10 + m14 + m16 + m18;
    const c10 = m2 + m3 + m4 + m6 + m14 + m16 + m17;
    const c11 = m2 + m4 + m5 + m6 + m20;
    const c12 = m14 + m16 + m17 + m18 + m21;
    const c20 = m6 + m7 + m8 + m11 + m12 + m13 + m14;
    const c21 = m12 + m13 + m14 + m15 + m22;
    const c22 = m6 + m7 + m8 + m9 + m23;

    result.set(0, 0, c00);
    result.set(0, 1, c01);
    result.set(0, 2, c02);
    result.set(1, 0, c10);
    result.set(1, 1, c11);
    result.set(1, 2, c12);
    result.set(2, 0, c20);
    result.set(2, 1, c21);
    result.set(2, 2, c22);
    return result;
  }

  mmulStrassen(y) {
    y = Matrix$1.checkMatrix(y);
    let x = this.clone();
    let r1 = x.rows;
    let c1 = x.columns;
    let r2 = y.rows;
    let c2 = y.columns;
    if (c1 !== r2) {
      // eslint-disable-next-line no-console
      console.warn(
        `Multiplying ${r1} x ${c1} and ${r2} x ${c2} matrix: dimensions do not match.`,
      );
    }

    // Put a matrix into the top left of a matrix of zeros.
    // `rows` and `cols` are the dimensions of the output matrix.
    function embed(mat, rows, cols) {
      let r = mat.rows;
      let c = mat.columns;
      if (r === rows && c === cols) {
        return mat;
      } else {
        let resultat = AbstractMatrix.zeros(rows, cols);
        resultat = resultat.setSubMatrix(mat, 0, 0);
        return resultat;
      }
    }

    // Make sure both matrices are the same size.
    // This is exclusively for simplicity:
    // this algorithm can be implemented with matrices of different sizes.

    let r = Math.max(r1, r2);
    let c = Math.max(c1, c2);
    x = embed(x, r, c);
    y = embed(y, r, c);

    // Our recursive multiplication function.
    function blockMult(a, b, rows, cols) {
      // For small matrices, resort to naive multiplication.
      if (rows <= 512 || cols <= 512) {
        return a.mmul(b); // a is equivalent to this
      }

      // Apply dynamic padding.
      if (rows % 2 === 1 && cols % 2 === 1) {
        a = embed(a, rows + 1, cols + 1);
        b = embed(b, rows + 1, cols + 1);
      } else if (rows % 2 === 1) {
        a = embed(a, rows + 1, cols);
        b = embed(b, rows + 1, cols);
      } else if (cols % 2 === 1) {
        a = embed(a, rows, cols + 1);
        b = embed(b, rows, cols + 1);
      }

      let halfRows = parseInt(a.rows / 2, 10);
      let halfCols = parseInt(a.columns / 2, 10);
      // Subdivide input matrices.
      let a11 = a.subMatrix(0, halfRows - 1, 0, halfCols - 1);
      let b11 = b.subMatrix(0, halfRows - 1, 0, halfCols - 1);

      let a12 = a.subMatrix(0, halfRows - 1, halfCols, a.columns - 1);
      let b12 = b.subMatrix(0, halfRows - 1, halfCols, b.columns - 1);

      let a21 = a.subMatrix(halfRows, a.rows - 1, 0, halfCols - 1);
      let b21 = b.subMatrix(halfRows, b.rows - 1, 0, halfCols - 1);

      let a22 = a.subMatrix(halfRows, a.rows - 1, halfCols, a.columns - 1);
      let b22 = b.subMatrix(halfRows, b.rows - 1, halfCols, b.columns - 1);

      // Compute intermediate values.
      let m1 = blockMult(
        AbstractMatrix.add(a11, a22),
        AbstractMatrix.add(b11, b22),
        halfRows,
        halfCols,
      );
      let m2 = blockMult(AbstractMatrix.add(a21, a22), b11, halfRows, halfCols);
      let m3 = blockMult(a11, AbstractMatrix.sub(b12, b22), halfRows, halfCols);
      let m4 = blockMult(a22, AbstractMatrix.sub(b21, b11), halfRows, halfCols);
      let m5 = blockMult(AbstractMatrix.add(a11, a12), b22, halfRows, halfCols);
      let m6 = blockMult(
        AbstractMatrix.sub(a21, a11),
        AbstractMatrix.add(b11, b12),
        halfRows,
        halfCols,
      );
      let m7 = blockMult(
        AbstractMatrix.sub(a12, a22),
        AbstractMatrix.add(b21, b22),
        halfRows,
        halfCols,
      );

      // Combine intermediate values into the output.
      let c11 = AbstractMatrix.add(m1, m4);
      c11.sub(m5);
      c11.add(m7);
      let c12 = AbstractMatrix.add(m3, m5);
      let c21 = AbstractMatrix.add(m2, m4);
      let c22 = AbstractMatrix.sub(m1, m2);
      c22.add(m3);
      c22.add(m6);

      // Crop output to the desired size (undo dynamic padding).
      let result = AbstractMatrix.zeros(2 * c11.rows, 2 * c11.columns);
      result = result.setSubMatrix(c11, 0, 0);
      result = result.setSubMatrix(c12, c11.rows, 0);
      result = result.setSubMatrix(c21, 0, c11.columns);
      result = result.setSubMatrix(c22, c11.rows, c11.columns);
      return result.subMatrix(0, rows - 1, 0, cols - 1);
    }

    return blockMult(x, y, r, c);
  }

  scaleRows(options = {}) {
    if (typeof options !== 'object') {
      throw new TypeError('options must be an object');
    }
    const { min = 0, max = 1 } = options;
    if (!Number.isFinite(min)) throw new TypeError('min must be a number');
    if (!Number.isFinite(max)) throw new TypeError('max must be a number');
    if (min >= max) throw new RangeError('min must be smaller than max');
    let newMatrix = new Matrix$1(this.rows, this.columns);
    for (let i = 0; i < this.rows; i++) {
      const row = this.getRow(i);
      if (row.length > 0) {
        rescale(row, { min, max, output: row });
      }
      newMatrix.setRow(i, row);
    }
    return newMatrix;
  }

  scaleColumns(options = {}) {
    if (typeof options !== 'object') {
      throw new TypeError('options must be an object');
    }
    const { min = 0, max = 1 } = options;
    if (!Number.isFinite(min)) throw new TypeError('min must be a number');
    if (!Number.isFinite(max)) throw new TypeError('max must be a number');
    if (min >= max) throw new RangeError('min must be smaller than max');
    let newMatrix = new Matrix$1(this.rows, this.columns);
    for (let i = 0; i < this.columns; i++) {
      const column = this.getColumn(i);
      if (column.length) {
        rescale(column, {
          min,
          max,
          output: column,
        });
      }
      newMatrix.setColumn(i, column);
    }
    return newMatrix;
  }

  flipRows() {
    const middle = Math.ceil(this.columns / 2);
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < middle; j++) {
        let first = this.get(i, j);
        let last = this.get(i, this.columns - 1 - j);
        this.set(i, j, last);
        this.set(i, this.columns - 1 - j, first);
      }
    }
    return this;
  }

  flipColumns() {
    const middle = Math.ceil(this.rows / 2);
    for (let j = 0; j < this.columns; j++) {
      for (let i = 0; i < middle; i++) {
        let first = this.get(i, j);
        let last = this.get(this.rows - 1 - i, j);
        this.set(i, j, last);
        this.set(this.rows - 1 - i, j, first);
      }
    }
    return this;
  }

  kroneckerProduct(other) {
    other = Matrix$1.checkMatrix(other);

    let m = this.rows;
    let n = this.columns;
    let p = other.rows;
    let q = other.columns;

    let result = new Matrix$1(m * p, n * q);
    for (let i = 0; i < m; i++) {
      for (let j = 0; j < n; j++) {
        for (let k = 0; k < p; k++) {
          for (let l = 0; l < q; l++) {
            result.set(p * i + k, q * j + l, this.get(i, j) * other.get(k, l));
          }
        }
      }
    }
    return result;
  }

  kroneckerSum(other) {
    other = Matrix$1.checkMatrix(other);
    if (!this.isSquare() || !other.isSquare()) {
      throw new Error('Kronecker Sum needs two Square Matrices');
    }
    let m = this.rows;
    let n = other.rows;
    let AxI = this.kroneckerProduct(Matrix$1.eye(n, n));
    let IxB = Matrix$1.eye(m, m).kroneckerProduct(other);
    return AxI.add(IxB);
  }

  transpose() {
    let result = new Matrix$1(this.columns, this.rows);
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.columns; j++) {
        result.set(j, i, this.get(i, j));
      }
    }
    return result;
  }

  sortRows(compareFunction = compareNumbers) {
    for (let i = 0; i < this.rows; i++) {
      this.setRow(i, this.getRow(i).sort(compareFunction));
    }
    return this;
  }

  sortColumns(compareFunction = compareNumbers) {
    for (let i = 0; i < this.columns; i++) {
      this.setColumn(i, this.getColumn(i).sort(compareFunction));
    }
    return this;
  }

  subMatrix(startRow, endRow, startColumn, endColumn) {
    checkRange(this, startRow, endRow, startColumn, endColumn);
    let newMatrix = new Matrix$1(
      endRow - startRow + 1,
      endColumn - startColumn + 1,
    );
    for (let i = startRow; i <= endRow; i++) {
      for (let j = startColumn; j <= endColumn; j++) {
        newMatrix.set(i - startRow, j - startColumn, this.get(i, j));
      }
    }
    return newMatrix;
  }

  subMatrixRow(indices, startColumn, endColumn) {
    if (startColumn === undefined) startColumn = 0;
    if (endColumn === undefined) endColumn = this.columns - 1;
    if (
      startColumn > endColumn ||
      startColumn < 0 ||
      startColumn >= this.columns ||
      endColumn < 0 ||
      endColumn >= this.columns
    ) {
      throw new RangeError('Argument out of range');
    }

    let newMatrix = new Matrix$1(indices.length, endColumn - startColumn + 1);
    for (let i = 0; i < indices.length; i++) {
      for (let j = startColumn; j <= endColumn; j++) {
        if (indices[i] < 0 || indices[i] >= this.rows) {
          throw new RangeError(`Row index out of range: ${indices[i]}`);
        }
        newMatrix.set(i, j - startColumn, this.get(indices[i], j));
      }
    }
    return newMatrix;
  }

  subMatrixColumn(indices, startRow, endRow) {
    if (startRow === undefined) startRow = 0;
    if (endRow === undefined) endRow = this.rows - 1;
    if (
      startRow > endRow ||
      startRow < 0 ||
      startRow >= this.rows ||
      endRow < 0 ||
      endRow >= this.rows
    ) {
      throw new RangeError('Argument out of range');
    }

    let newMatrix = new Matrix$1(endRow - startRow + 1, indices.length);
    for (let i = 0; i < indices.length; i++) {
      for (let j = startRow; j <= endRow; j++) {
        if (indices[i] < 0 || indices[i] >= this.columns) {
          throw new RangeError(`Column index out of range: ${indices[i]}`);
        }
        newMatrix.set(j - startRow, i, this.get(j, indices[i]));
      }
    }
    return newMatrix;
  }

  setSubMatrix(matrix, startRow, startColumn) {
    matrix = Matrix$1.checkMatrix(matrix);
    if (matrix.isEmpty()) {
      return this;
    }
    let endRow = startRow + matrix.rows - 1;
    let endColumn = startColumn + matrix.columns - 1;
    checkRange(this, startRow, endRow, startColumn, endColumn);
    for (let i = 0; i < matrix.rows; i++) {
      for (let j = 0; j < matrix.columns; j++) {
        this.set(startRow + i, startColumn + j, matrix.get(i, j));
      }
    }
    return this;
  }

  selection(rowIndices, columnIndices) {
    checkRowIndices(this, rowIndices);
    checkColumnIndices(this, columnIndices);
    let newMatrix = new Matrix$1(rowIndices.length, columnIndices.length);
    for (let i = 0; i < rowIndices.length; i++) {
      let rowIndex = rowIndices[i];
      for (let j = 0; j < columnIndices.length; j++) {
        let columnIndex = columnIndices[j];
        newMatrix.set(i, j, this.get(rowIndex, columnIndex));
      }
    }
    return newMatrix;
  }

  trace() {
    let min = Math.min(this.rows, this.columns);
    let trace = 0;
    for (let i = 0; i < min; i++) {
      trace += this.get(i, i);
    }
    return trace;
  }

  clone() {
    return this.constructor.copy(this, new Matrix$1(this.rows, this.columns));
  }

  /**
   * @template {AbstractMatrix} M
   * @param {AbstractMatrix} from
   * @param {M} to
   * @return {M}
   */
  static copy(from, to) {
    for (const [row, column, value] of from.entries()) {
      to.set(row, column, value);
    }

    return to;
  }

  sum(by) {
    switch (by) {
      case 'row':
        return sumByRow(this);
      case 'column':
        return sumByColumn(this);
      case undefined:
        return sumAll(this);
      default:
        throw new Error(`invalid option: ${by}`);
    }
  }

  product(by) {
    switch (by) {
      case 'row':
        return productByRow(this);
      case 'column':
        return productByColumn(this);
      case undefined:
        return productAll(this);
      default:
        throw new Error(`invalid option: ${by}`);
    }
  }

  mean(by) {
    const sum = this.sum(by);
    switch (by) {
      case 'row': {
        for (let i = 0; i < this.rows; i++) {
          sum[i] /= this.columns;
        }
        return sum;
      }
      case 'column': {
        for (let i = 0; i < this.columns; i++) {
          sum[i] /= this.rows;
        }
        return sum;
      }
      case undefined:
        return sum / this.size;
      default:
        throw new Error(`invalid option: ${by}`);
    }
  }

  variance(by, options = {}) {
    if (typeof by === 'object') {
      options = by;
      by = undefined;
    }
    if (typeof options !== 'object') {
      throw new TypeError('options must be an object');
    }
    const { unbiased = true, mean = this.mean(by) } = options;
    if (typeof unbiased !== 'boolean') {
      throw new TypeError('unbiased must be a boolean');
    }
    switch (by) {
      case 'row': {
        if (!isAnyArray.isAnyArray(mean)) {
          throw new TypeError('mean must be an array');
        }
        return varianceByRow(this, unbiased, mean);
      }
      case 'column': {
        if (!isAnyArray.isAnyArray(mean)) {
          throw new TypeError('mean must be an array');
        }
        return varianceByColumn(this, unbiased, mean);
      }
      case undefined: {
        if (typeof mean !== 'number') {
          throw new TypeError('mean must be a number');
        }
        return varianceAll(this, unbiased, mean);
      }
      default:
        throw new Error(`invalid option: ${by}`);
    }
  }

  standardDeviation(by, options) {
    if (typeof by === 'object') {
      options = by;
      by = undefined;
    }
    const variance = this.variance(by, options);
    if (by === undefined) {
      return Math.sqrt(variance);
    } else {
      for (let i = 0; i < variance.length; i++) {
        variance[i] = Math.sqrt(variance[i]);
      }
      return variance;
    }
  }

  center(by, options = {}) {
    if (typeof by === 'object') {
      options = by;
      by = undefined;
    }
    if (typeof options !== 'object') {
      throw new TypeError('options must be an object');
    }
    const { center = this.mean(by) } = options;
    switch (by) {
      case 'row': {
        if (!isAnyArray.isAnyArray(center)) {
          throw new TypeError('center must be an array');
        }
        centerByRow(this, center);
        return this;
      }
      case 'column': {
        if (!isAnyArray.isAnyArray(center)) {
          throw new TypeError('center must be an array');
        }
        centerByColumn(this, center);
        return this;
      }
      case undefined: {
        if (typeof center !== 'number') {
          throw new TypeError('center must be a number');
        }
        centerAll(this, center);
        return this;
      }
      default:
        throw new Error(`invalid option: ${by}`);
    }
  }

  scale(by, options = {}) {
    if (typeof by === 'object') {
      options = by;
      by = undefined;
    }
    if (typeof options !== 'object') {
      throw new TypeError('options must be an object');
    }
    let scale = options.scale;
    switch (by) {
      case 'row': {
        if (scale === undefined) {
          scale = getScaleByRow(this);
        } else if (!isAnyArray.isAnyArray(scale)) {
          throw new TypeError('scale must be an array');
        }
        scaleByRow(this, scale);
        return this;
      }
      case 'column': {
        if (scale === undefined) {
          scale = getScaleByColumn(this);
        } else if (!isAnyArray.isAnyArray(scale)) {
          throw new TypeError('scale must be an array');
        }
        scaleByColumn(this, scale);
        return this;
      }
      case undefined: {
        if (scale === undefined) {
          scale = getScaleAll(this);
        } else if (typeof scale !== 'number') {
          throw new TypeError('scale must be a number');
        }
        scaleAll(this, scale);
        return this;
      }
      default:
        throw new Error(`invalid option: ${by}`);
    }
  }

  toString(options) {
    return inspectMatrixWithOptions(this, options);
  }

  [Symbol.iterator]() {
    return this.entries();
  }

  /**
   * iterator from left to right, from top to bottom
   * yield [row, column, value]
   * @returns {Generator<[number, number, number], void, void>}
   */
  *entries() {
    for (let row = 0; row < this.rows; row++) {
      for (let col = 0; col < this.columns; col++) {
        yield [row, col, this.get(row, col)];
      }
    }
  }

  /**
   * iterator from left to right, from top to bottom
   * yield value
   * @returns {Generator<number, void, void>}
   */
  *values() {
    for (let row = 0; row < this.rows; row++) {
      for (let col = 0; col < this.columns; col++) {
        yield this.get(row, col);
      }
    }
  }
}

AbstractMatrix.prototype.klass = 'Matrix';
if (typeof Symbol !== 'undefined') {
  AbstractMatrix.prototype[Symbol.for('nodejs.util.inspect.custom')] =
    inspectMatrix;
}

function compareNumbers(a, b) {
  return a - b;
}

function isArrayOfNumbers(array) {
  return array.every((element) => {
    return typeof element === 'number';
  });
}

// Synonyms
AbstractMatrix.random = AbstractMatrix.rand;
AbstractMatrix.randomInt = AbstractMatrix.randInt;
AbstractMatrix.diagonal = AbstractMatrix.diag;
AbstractMatrix.prototype.diagonal = AbstractMatrix.prototype.diag;
AbstractMatrix.identity = AbstractMatrix.eye;
AbstractMatrix.prototype.negate = AbstractMatrix.prototype.neg;
AbstractMatrix.prototype.tensorProduct =
  AbstractMatrix.prototype.kroneckerProduct;

class Matrix$1 extends AbstractMatrix {
  /**
   * @type {Float64Array[]}
   */
  data;

  /**
   * Init an empty matrix
   * @param {number} nRows
   * @param {number} nColumns
   */
  #initData(nRows, nColumns) {
    this.data = [];

    if (Number.isInteger(nColumns) && nColumns >= 0) {
      for (let i = 0; i < nRows; i++) {
        this.data.push(new Float64Array(nColumns));
      }
    } else {
      throw new TypeError('nColumns must be a positive integer');
    }

    this.rows = nRows;
    this.columns = nColumns;
  }

  constructor(nRows, nColumns) {
    super();
    if (Matrix$1.isMatrix(nRows)) {
      this.#initData(nRows.rows, nRows.columns);
      Matrix$1.copy(nRows, this);
    } else if (Number.isInteger(nRows) && nRows >= 0) {
      this.#initData(nRows, nColumns);
    } else if (isAnyArray.isAnyArray(nRows)) {
      // Copy the values from the 2D array
      const arrayData = nRows;
      nRows = arrayData.length;
      nColumns = nRows ? arrayData[0].length : 0;
      if (typeof nColumns !== 'number') {
        throw new TypeError(
          'Data must be a 2D array with at least one element',
        );
      }
      this.data = [];

      for (let i = 0; i < nRows; i++) {
        if (arrayData[i].length !== nColumns) {
          throw new RangeError('Inconsistent array dimensions');
        }
        if (!isArrayOfNumbers(arrayData[i])) {
          throw new TypeError('Input data contains non-numeric values');
        }
        this.data.push(Float64Array.from(arrayData[i]));
      }

      this.rows = nRows;
      this.columns = nColumns;
    } else {
      throw new TypeError(
        'First argument must be a positive number or an array',
      );
    }
  }

  set(rowIndex, columnIndex, value) {
    this.data[rowIndex][columnIndex] = value;
    return this;
  }

  get(rowIndex, columnIndex) {
    return this.data[rowIndex][columnIndex];
  }

  removeRow(index) {
    checkRowIndex(this, index);
    this.data.splice(index, 1);
    this.rows -= 1;
    return this;
  }

  addRow(index, array) {
    if (array === undefined) {
      array = index;
      index = this.rows;
    }
    checkRowIndex(this, index, true);
    array = Float64Array.from(checkRowVector(this, array));
    this.data.splice(index, 0, array);
    this.rows += 1;
    return this;
  }

  removeColumn(index) {
    checkColumnIndex(this, index);
    for (let i = 0; i < this.rows; i++) {
      const newRow = new Float64Array(this.columns - 1);
      for (let j = 0; j < index; j++) {
        newRow[j] = this.data[i][j];
      }
      for (let j = index + 1; j < this.columns; j++) {
        newRow[j - 1] = this.data[i][j];
      }
      this.data[i] = newRow;
    }
    this.columns -= 1;
    return this;
  }

  addColumn(index, array) {
    if (typeof array === 'undefined') {
      array = index;
      index = this.columns;
    }
    checkColumnIndex(this, index, true);
    array = checkColumnVector(this, array);
    for (let i = 0; i < this.rows; i++) {
      const newRow = new Float64Array(this.columns + 1);
      let j = 0;
      for (; j < index; j++) {
        newRow[j] = this.data[i][j];
      }
      newRow[j++] = array[i];
      for (; j < this.columns + 1; j++) {
        newRow[j] = this.data[i][j - 1];
      }
      this.data[i] = newRow;
    }
    this.columns += 1;
    return this;
  }
}

installMathOperations(AbstractMatrix, Matrix$1);

/**
 * @typedef {0 | 1 | number | boolean} Mask
 */

class SymmetricMatrix extends AbstractMatrix {
  /** @type {Matrix} */
  #matrix;

  get size() {
    return this.#matrix.size;
  }

  get rows() {
    return this.#matrix.rows;
  }

  get columns() {
    return this.#matrix.columns;
  }

  get diagonalSize() {
    return this.rows;
  }

  /**
   * not the same as matrix.isSymmetric()
   * Here is to check if it's instanceof SymmetricMatrix without bundling issues
   *
   * @param value
   * @returns {boolean}
   */
  static isSymmetricMatrix(value) {
    return Matrix$1.isMatrix(value) && value.klassType === 'SymmetricMatrix';
  }

  /**
   * @param diagonalSize
   * @return {SymmetricMatrix}
   */
  static zeros(diagonalSize) {
    return new this(diagonalSize);
  }

  /**
   * @param diagonalSize
   * @return {SymmetricMatrix}
   */
  static ones(diagonalSize) {
    return new this(diagonalSize).fill(1);
  }

  /**
   * @param {number | AbstractMatrix | ArrayLike<ArrayLike<number>>} diagonalSize
   * @return {this}
   */
  constructor(diagonalSize) {
    super();

    if (Matrix$1.isMatrix(diagonalSize)) {
      if (!diagonalSize.isSymmetric()) {
        throw new TypeError('not symmetric data');
      }

      this.#matrix = Matrix$1.copy(
        diagonalSize,
        new Matrix$1(diagonalSize.rows, diagonalSize.rows),
      );
    } else if (Number.isInteger(diagonalSize) && diagonalSize >= 0) {
      this.#matrix = new Matrix$1(diagonalSize, diagonalSize);
    } else {
      this.#matrix = new Matrix$1(diagonalSize);

      if (!this.isSymmetric()) {
        throw new TypeError('not symmetric data');
      }
    }
  }

  clone() {
    const matrix = new SymmetricMatrix(this.diagonalSize);

    for (const [row, col, value] of this.upperRightEntries()) {
      matrix.set(row, col, value);
    }

    return matrix;
  }

  toMatrix() {
    return new Matrix$1(this);
  }

  get(rowIndex, columnIndex) {
    return this.#matrix.get(rowIndex, columnIndex);
  }
  set(rowIndex, columnIndex, value) {
    // symmetric set
    this.#matrix.set(rowIndex, columnIndex, value);
    this.#matrix.set(columnIndex, rowIndex, value);

    return this;
  }

  removeCross(index) {
    // symmetric remove side
    this.#matrix.removeRow(index);
    this.#matrix.removeColumn(index);

    return this;
  }

  addCross(index, array) {
    if (array === undefined) {
      array = index;
      index = this.diagonalSize;
    }

    const row = array.slice();
    row.splice(index, 1);

    this.#matrix.addRow(index, row);
    this.#matrix.addColumn(index, array);

    return this;
  }

  /**
   * @param {Mask[]} mask
   */
  applyMask(mask) {
    if (mask.length !== this.diagonalSize) {
      throw new RangeError('Mask size do not match with matrix size');
    }

    // prepare sides to remove from matrix from mask
    /** @type {number[]} */
    const sidesToRemove = [];
    for (const [index, passthroughs] of mask.entries()) {
      if (passthroughs) continue;
      sidesToRemove.push(index);
    }
    // to remove from highest to lowest for no mutation shifting
    sidesToRemove.reverse();

    // remove sides
    for (const sideIndex of sidesToRemove) {
      this.removeCross(sideIndex);
    }

    return this;
  }

  /**
   * Compact format upper-right corner of matrix
   * iterate from left to right, from top to bottom.
   *
   * ```
   *   A B C D
   * A 1 2 3 4
   * B 2 5 6 7
   * C 3 6 8 9
   * D 4 7 9 10
   * ```
   *
   * will return compact 1D array `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]`
   *
   * length is S(i=0, n=sideSize) => 10 for a 4 sideSized matrix
   *
   * @returns {number[]}
   */
  toCompact() {
    const { diagonalSize } = this;

    /** @type {number[]} */
    const compact = new Array((diagonalSize * (diagonalSize + 1)) / 2);
    for (let col = 0, row = 0, index = 0; index < compact.length; index++) {
      compact[index] = this.get(row, col);

      if (++col >= diagonalSize) col = ++row;
    }

    return compact;
  }

  /**
   * @param {number[]} compact
   * @return {SymmetricMatrix}
   */
  static fromCompact(compact) {
    const compactSize = compact.length;
    // compactSize = (sideSize * (sideSize + 1)) / 2
    // https://mathsolver.microsoft.com/fr/solve-problem/y%20%3D%20%20x%20%60cdot%20%20%20%60frac%7B%20%20%60left(%20x%2B1%20%20%60right)%20%20%20%20%7D%7B%202%20%20%7D
    // sideSize = (Sqrt(8 × compactSize + 1) - 1) / 2
    const diagonalSize = (Math.sqrt(8 * compactSize + 1) - 1) / 2;

    if (!Number.isInteger(diagonalSize)) {
      throw new TypeError(
        `This array is not a compact representation of a Symmetric Matrix, ${JSON.stringify(
          compact,
        )}`,
      );
    }

    const matrix = new SymmetricMatrix(diagonalSize);
    for (let col = 0, row = 0, index = 0; index < compactSize; index++) {
      matrix.set(col, row, compact[index]);
      if (++col >= diagonalSize) col = ++row;
    }

    return matrix;
  }

  /**
   * half iterator upper-right-corner from left to right, from top to bottom
   * yield [row, column, value]
   *
   * @returns {Generator<[number, number, number], void, void>}
   */
  *upperRightEntries() {
    for (let row = 0, col = 0; row < this.diagonalSize; void 0) {
      const value = this.get(row, col);

      yield [row, col, value];

      // at the end of row, move cursor to next row at diagonal position
      if (++col >= this.diagonalSize) col = ++row;
    }
  }

  /**
   * half iterator upper-right-corner from left to right, from top to bottom
   * yield value
   *
   * @returns {Generator<[number, number, number], void, void>}
   */
  *upperRightValues() {
    for (let row = 0, col = 0; row < this.diagonalSize; void 0) {
      const value = this.get(row, col);

      yield value;

      // at the end of row, move cursor to next row at diagonal position
      if (++col >= this.diagonalSize) col = ++row;
    }
  }
}
SymmetricMatrix.prototype.klassType = 'SymmetricMatrix';

class DistanceMatrix extends SymmetricMatrix {
  /**
   * not the same as matrix.isSymmetric()
   * Here is to check if it's instanceof SymmetricMatrix without bundling issues
   *
   * @param value
   * @returns {boolean}
   */
  static isDistanceMatrix(value) {
    return (
      SymmetricMatrix.isSymmetricMatrix(value) &&
      value.klassSubType === 'DistanceMatrix'
    );
  }

  constructor(sideSize) {
    super(sideSize);

    if (!this.isDistance()) {
      throw new TypeError('Provided arguments do no produce a distance matrix');
    }
  }

  set(rowIndex, columnIndex, value) {
    // distance matrix diagonal is 0
    if (rowIndex === columnIndex) value = 0;

    return super.set(rowIndex, columnIndex, value);
  }

  addCross(index, array) {
    if (array === undefined) {
      array = index;
      index = this.diagonalSize;
    }

    // ensure distance
    array = array.slice();
    array[index] = 0;

    return super.addCross(index, array);
  }

  toSymmetricMatrix() {
    return new SymmetricMatrix(this);
  }

  clone() {
    const matrix = new DistanceMatrix(this.diagonalSize);

    for (const [row, col, value] of this.upperRightEntries()) {
      if (row === col) continue;
      matrix.set(row, col, value);
    }

    return matrix;
  }

  /**
   * Compact format upper-right corner of matrix
   * no diagonal (only zeros)
   * iterable from left to right, from top to bottom.
   *
   * ```
   *   A B C D
   * A 0 1 2 3
   * B 1 0 4 5
   * C 2 4 0 6
   * D 3 5 6 0
   * ```
   *
   * will return compact 1D array `[1, 2, 3, 4, 5, 6]`
   *
   * length is S(i=0, n=sideSize-1) => 6 for a 4 side sized matrix
   *
   * @returns {number[]}
   */
  toCompact() {
    const { diagonalSize } = this;
    const compactLength = ((diagonalSize - 1) * diagonalSize) / 2;

    /** @type {number[]} */
    const compact = new Array(compactLength);
    for (let col = 1, row = 0, index = 0; index < compact.length; index++) {
      compact[index] = this.get(row, col);

      if (++col >= diagonalSize) col = ++row + 1;
    }

    return compact;
  }

  /**
   * @param {number[]} compact
   */
  static fromCompact(compact) {
    const compactSize = compact.length;

    if (compactSize === 0) {
      return new this(0);
    }

    // compactSize in Natural integer range ]0;∞]
    // compactSize = (sideSize * (sideSize - 1)) / 2
    // sideSize = (Sqrt(8 × compactSize + 1) + 1) / 2
    const diagonalSize = (Math.sqrt(8 * compactSize + 1) + 1) / 2;

    if (!Number.isInteger(diagonalSize)) {
      throw new TypeError(
        `This array is not a compact representation of a DistanceMatrix, ${JSON.stringify(
          compact,
        )}`,
      );
    }

    const matrix = new this(diagonalSize);
    for (let col = 1, row = 0, index = 0; index < compactSize; index++) {
      matrix.set(col, row, compact[index]);
      if (++col >= diagonalSize) col = ++row + 1;
    }

    return matrix;
  }
}
DistanceMatrix.prototype.klassSubType = 'DistanceMatrix';

class BaseView extends AbstractMatrix {
  constructor(matrix, rows, columns) {
    super();
    this.matrix = matrix;
    this.rows = rows;
    this.columns = columns;
  }
}

class MatrixColumnView extends BaseView {
  constructor(matrix, column) {
    checkColumnIndex(matrix, column);
    super(matrix, matrix.rows, 1);
    this.column = column;
  }

  set(rowIndex, columnIndex, value) {
    this.matrix.set(rowIndex, this.column, value);
    return this;
  }

  get(rowIndex) {
    return this.matrix.get(rowIndex, this.column);
  }
}

class MatrixColumnSelectionView extends BaseView {
  constructor(matrix, columnIndices) {
    checkColumnIndices(matrix, columnIndices);
    super(matrix, matrix.rows, columnIndices.length);
    this.columnIndices = columnIndices;
  }

  set(rowIndex, columnIndex, value) {
    this.matrix.set(rowIndex, this.columnIndices[columnIndex], value);
    return this;
  }

  get(rowIndex, columnIndex) {
    return this.matrix.get(rowIndex, this.columnIndices[columnIndex]);
  }
}

class MatrixFlipColumnView extends BaseView {
  constructor(matrix) {
    super(matrix, matrix.rows, matrix.columns);
  }

  set(rowIndex, columnIndex, value) {
    this.matrix.set(rowIndex, this.columns - columnIndex - 1, value);
    return this;
  }

  get(rowIndex, columnIndex) {
    return this.matrix.get(rowIndex, this.columns - columnIndex - 1);
  }
}

class MatrixFlipRowView extends BaseView {
  constructor(matrix) {
    super(matrix, matrix.rows, matrix.columns);
  }

  set(rowIndex, columnIndex, value) {
    this.matrix.set(this.rows - rowIndex - 1, columnIndex, value);
    return this;
  }

  get(rowIndex, columnIndex) {
    return this.matrix.get(this.rows - rowIndex - 1, columnIndex);
  }
}

class MatrixRowView extends BaseView {
  constructor(matrix, row) {
    checkRowIndex(matrix, row);
    super(matrix, 1, matrix.columns);
    this.row = row;
  }

  set(rowIndex, columnIndex, value) {
    this.matrix.set(this.row, columnIndex, value);
    return this;
  }

  get(rowIndex, columnIndex) {
    return this.matrix.get(this.row, columnIndex);
  }
}

class MatrixRowSelectionView extends BaseView {
  constructor(matrix, rowIndices) {
    checkRowIndices(matrix, rowIndices);
    super(matrix, rowIndices.length, matrix.columns);
    this.rowIndices = rowIndices;
  }

  set(rowIndex, columnIndex, value) {
    this.matrix.set(this.rowIndices[rowIndex], columnIndex, value);
    return this;
  }

  get(rowIndex, columnIndex) {
    return this.matrix.get(this.rowIndices[rowIndex], columnIndex);
  }
}

class MatrixSelectionView extends BaseView {
  constructor(matrix, rowIndices, columnIndices) {
    checkRowIndices(matrix, rowIndices);
    checkColumnIndices(matrix, columnIndices);
    super(matrix, rowIndices.length, columnIndices.length);
    this.rowIndices = rowIndices;
    this.columnIndices = columnIndices;
  }

  set(rowIndex, columnIndex, value) {
    this.matrix.set(
      this.rowIndices[rowIndex],
      this.columnIndices[columnIndex],
      value,
    );
    return this;
  }

  get(rowIndex, columnIndex) {
    return this.matrix.get(
      this.rowIndices[rowIndex],
      this.columnIndices[columnIndex],
    );
  }
}

class MatrixSubView extends BaseView {
  constructor(matrix, startRow, endRow, startColumn, endColumn) {
    checkRange(matrix, startRow, endRow, startColumn, endColumn);
    super(matrix, endRow - startRow + 1, endColumn - startColumn + 1);
    this.startRow = startRow;
    this.startColumn = startColumn;
  }

  set(rowIndex, columnIndex, value) {
    this.matrix.set(
      this.startRow + rowIndex,
      this.startColumn + columnIndex,
      value,
    );
    return this;
  }

  get(rowIndex, columnIndex) {
    return this.matrix.get(
      this.startRow + rowIndex,
      this.startColumn + columnIndex,
    );
  }
}

class MatrixTransposeView extends BaseView {
  constructor(matrix) {
    super(matrix, matrix.columns, matrix.rows);
  }

  set(rowIndex, columnIndex, value) {
    this.matrix.set(columnIndex, rowIndex, value);
    return this;
  }

  get(rowIndex, columnIndex) {
    return this.matrix.get(columnIndex, rowIndex);
  }
}

class WrapperMatrix1D extends AbstractMatrix {
  constructor(data, options = {}) {
    const { rows = 1 } = options;

    if (data.length % rows !== 0) {
      throw new Error('the data length is not divisible by the number of rows');
    }
    super();
    this.rows = rows;
    this.columns = data.length / rows;
    this.data = data;
  }

  set(rowIndex, columnIndex, value) {
    let index = this._calculateIndex(rowIndex, columnIndex);
    this.data[index] = value;
    return this;
  }

  get(rowIndex, columnIndex) {
    let index = this._calculateIndex(rowIndex, columnIndex);
    return this.data[index];
  }

  _calculateIndex(row, column) {
    return row * this.columns + column;
  }
}

class WrapperMatrix2D extends AbstractMatrix {
  constructor(data) {
    super();
    this.data = data;
    this.rows = data.length;
    this.columns = data[0].length;
  }

  set(rowIndex, columnIndex, value) {
    this.data[rowIndex][columnIndex] = value;
    return this;
  }

  get(rowIndex, columnIndex) {
    return this.data[rowIndex][columnIndex];
  }
}

function wrap(array, options) {
  if (isAnyArray.isAnyArray(array)) {
    if (array[0] && isAnyArray.isAnyArray(array[0])) {
      return new WrapperMatrix2D(array);
    } else {
      return new WrapperMatrix1D(array, options);
    }
  } else {
    throw new Error('the argument is not an array');
  }
}

class LuDecomposition {
  constructor(matrix) {
    matrix = WrapperMatrix2D.checkMatrix(matrix);

    let lu = matrix.clone();
    let rows = lu.rows;
    let columns = lu.columns;
    let pivotVector = new Float64Array(rows);
    let pivotSign = 1;
    let i, j, k, p, s, t, v;
    let LUcolj, kmax;

    for (i = 0; i < rows; i++) {
      pivotVector[i] = i;
    }

    LUcolj = new Float64Array(rows);

    for (j = 0; j < columns; j++) {
      for (i = 0; i < rows; i++) {
        LUcolj[i] = lu.get(i, j);
      }

      for (i = 0; i < rows; i++) {
        kmax = Math.min(i, j);
        s = 0;
        for (k = 0; k < kmax; k++) {
          s += lu.get(i, k) * LUcolj[k];
        }
        LUcolj[i] -= s;
        lu.set(i, j, LUcolj[i]);
      }

      p = j;
      for (i = j + 1; i < rows; i++) {
        if (Math.abs(LUcolj[i]) > Math.abs(LUcolj[p])) {
          p = i;
        }
      }

      if (p !== j) {
        for (k = 0; k < columns; k++) {
          t = lu.get(p, k);
          lu.set(p, k, lu.get(j, k));
          lu.set(j, k, t);
        }

        v = pivotVector[p];
        pivotVector[p] = pivotVector[j];
        pivotVector[j] = v;

        pivotSign = -pivotSign;
      }

      if (j < rows && lu.get(j, j) !== 0) {
        for (i = j + 1; i < rows; i++) {
          lu.set(i, j, lu.get(i, j) / lu.get(j, j));
        }
      }
    }

    this.LU = lu;
    this.pivotVector = pivotVector;
    this.pivotSign = pivotSign;
  }

  isSingular() {
    let data = this.LU;
    let col = data.columns;
    for (let j = 0; j < col; j++) {
      if (data.get(j, j) === 0) {
        return true;
      }
    }
    return false;
  }

  solve(value) {
    value = Matrix$1.checkMatrix(value);

    let lu = this.LU;
    let rows = lu.rows;

    if (rows !== value.rows) {
      throw new Error('Invalid matrix dimensions');
    }
    if (this.isSingular()) {
      throw new Error('LU matrix is singular');
    }

    let count = value.columns;
    let X = value.subMatrixRow(this.pivotVector, 0, count - 1);
    let columns = lu.columns;
    let i, j, k;

    for (k = 0; k < columns; k++) {
      for (i = k + 1; i < columns; i++) {
        for (j = 0; j < count; j++) {
          X.set(i, j, X.get(i, j) - X.get(k, j) * lu.get(i, k));
        }
      }
    }
    for (k = columns - 1; k >= 0; k--) {
      for (j = 0; j < count; j++) {
        X.set(k, j, X.get(k, j) / lu.get(k, k));
      }
      for (i = 0; i < k; i++) {
        for (j = 0; j < count; j++) {
          X.set(i, j, X.get(i, j) - X.get(k, j) * lu.get(i, k));
        }
      }
    }
    return X;
  }

  get determinant() {
    let data = this.LU;
    if (!data.isSquare()) {
      throw new Error('Matrix must be square');
    }
    let determinant = this.pivotSign;
    let col = data.columns;
    for (let j = 0; j < col; j++) {
      determinant *= data.get(j, j);
    }
    return determinant;
  }

  get lowerTriangularMatrix() {
    let data = this.LU;
    let rows = data.rows;
    let columns = data.columns;
    let X = new Matrix$1(rows, columns);
    for (let i = 0; i < rows; i++) {
      for (let j = 0; j < columns; j++) {
        if (i > j) {
          X.set(i, j, data.get(i, j));
        } else if (i === j) {
          X.set(i, j, 1);
        } else {
          X.set(i, j, 0);
        }
      }
    }
    return X;
  }

  get upperTriangularMatrix() {
    let data = this.LU;
    let rows = data.rows;
    let columns = data.columns;
    let X = new Matrix$1(rows, columns);
    for (let i = 0; i < rows; i++) {
      for (let j = 0; j < columns; j++) {
        if (i <= j) {
          X.set(i, j, data.get(i, j));
        } else {
          X.set(i, j, 0);
        }
      }
    }
    return X;
  }

  get pivotPermutationVector() {
    return Array.from(this.pivotVector);
  }
}

function hypotenuse(a, b) {
  let r = 0;
  if (Math.abs(a) > Math.abs(b)) {
    r = b / a;
    return Math.abs(a) * Math.sqrt(1 + r * r);
  }
  if (b !== 0) {
    r = a / b;
    return Math.abs(b) * Math.sqrt(1 + r * r);
  }
  return 0;
}

class QrDecomposition {
  constructor(value) {
    value = WrapperMatrix2D.checkMatrix(value);

    let qr = value.clone();
    let m = value.rows;
    let n = value.columns;
    let rdiag = new Float64Array(n);
    let i, j, k, s;

    for (k = 0; k < n; k++) {
      let nrm = 0;
      for (i = k; i < m; i++) {
        nrm = hypotenuse(nrm, qr.get(i, k));
      }
      if (nrm !== 0) {
        if (qr.get(k, k) < 0) {
          nrm = -nrm;
        }
        for (i = k; i < m; i++) {
          qr.set(i, k, qr.get(i, k) / nrm);
        }
        qr.set(k, k, qr.get(k, k) + 1);
        for (j = k + 1; j < n; j++) {
          s = 0;
          for (i = k; i < m; i++) {
            s += qr.get(i, k) * qr.get(i, j);
          }
          s = -s / qr.get(k, k);
          for (i = k; i < m; i++) {
            qr.set(i, j, qr.get(i, j) + s * qr.get(i, k));
          }
        }
      }
      rdiag[k] = -nrm;
    }

    this.QR = qr;
    this.Rdiag = rdiag;
  }

  solve(value) {
    value = Matrix$1.checkMatrix(value);

    let qr = this.QR;
    let m = qr.rows;

    if (value.rows !== m) {
      throw new Error('Matrix row dimensions must agree');
    }
    if (!this.isFullRank()) {
      throw new Error('Matrix is rank deficient');
    }

    let count = value.columns;
    let X = value.clone();
    let n = qr.columns;
    let i, j, k, s;

    for (k = 0; k < n; k++) {
      for (j = 0; j < count; j++) {
        s = 0;
        for (i = k; i < m; i++) {
          s += qr.get(i, k) * X.get(i, j);
        }
        s = -s / qr.get(k, k);
        for (i = k; i < m; i++) {
          X.set(i, j, X.get(i, j) + s * qr.get(i, k));
        }
      }
    }
    for (k = n - 1; k >= 0; k--) {
      for (j = 0; j < count; j++) {
        X.set(k, j, X.get(k, j) / this.Rdiag[k]);
      }
      for (i = 0; i < k; i++) {
        for (j = 0; j < count; j++) {
          X.set(i, j, X.get(i, j) - X.get(k, j) * qr.get(i, k));
        }
      }
    }

    return X.subMatrix(0, n - 1, 0, count - 1);
  }

  isFullRank() {
    let columns = this.QR.columns;
    for (let i = 0; i < columns; i++) {
      if (this.Rdiag[i] === 0) {
        return false;
      }
    }
    return true;
  }

  get upperTriangularMatrix() {
    let qr = this.QR;
    let n = qr.columns;
    let X = new Matrix$1(n, n);
    let i, j;
    for (i = 0; i < n; i++) {
      for (j = 0; j < n; j++) {
        if (i < j) {
          X.set(i, j, qr.get(i, j));
        } else if (i === j) {
          X.set(i, j, this.Rdiag[i]);
        } else {
          X.set(i, j, 0);
        }
      }
    }
    return X;
  }

  get orthogonalMatrix() {
    let qr = this.QR;
    let rows = qr.rows;
    let columns = qr.columns;
    let X = new Matrix$1(rows, columns);
    let i, j, k, s;

    for (k = columns - 1; k >= 0; k--) {
      for (i = 0; i < rows; i++) {
        X.set(i, k, 0);
      }
      X.set(k, k, 1);
      for (j = k; j < columns; j++) {
        if (qr.get(k, k) !== 0) {
          s = 0;
          for (i = k; i < rows; i++) {
            s += qr.get(i, k) * X.get(i, j);
          }

          s = -s / qr.get(k, k);

          for (i = k; i < rows; i++) {
            X.set(i, j, X.get(i, j) + s * qr.get(i, k));
          }
        }
      }
    }
    return X;
  }
}

class SingularValueDecomposition {
  constructor(value, options = {}) {
    value = WrapperMatrix2D.checkMatrix(value);

    if (value.isEmpty()) {
      throw new Error('Matrix must be non-empty');
    }

    let m = value.rows;
    let n = value.columns;

    const {
      computeLeftSingularVectors = true,
      computeRightSingularVectors = true,
      autoTranspose = false,
    } = options;

    let wantu = Boolean(computeLeftSingularVectors);
    let wantv = Boolean(computeRightSingularVectors);

    let swapped = false;
    let a;
    if (m < n) {
      if (!autoTranspose) {
        a = value.clone();
        // eslint-disable-next-line no-console
        console.warn(
          'Computing SVD on a matrix with more columns than rows. Consider enabling autoTranspose',
        );
      } else {
        a = value.transpose();
        m = a.rows;
        n = a.columns;
        swapped = true;
        let aux = wantu;
        wantu = wantv;
        wantv = aux;
      }
    } else {
      a = value.clone();
    }

    let nu = Math.min(m, n);
    let ni = Math.min(m + 1, n);
    let s = new Float64Array(ni);
    let U = new Matrix$1(m, nu);
    let V = new Matrix$1(n, n);

    let e = new Float64Array(n);
    let work = new Float64Array(m);

    let si = new Float64Array(ni);
    for (let i = 0; i < ni; i++) si[i] = i;

    let nct = Math.min(m - 1, n);
    let nrt = Math.max(0, Math.min(n - 2, m));
    let mrc = Math.max(nct, nrt);

    for (let k = 0; k < mrc; k++) {
      if (k < nct) {
        s[k] = 0;
        for (let i = k; i < m; i++) {
          s[k] = hypotenuse(s[k], a.get(i, k));
        }
        if (s[k] !== 0) {
          if (a.get(k, k) < 0) {
            s[k] = -s[k];
          }
          for (let i = k; i < m; i++) {
            a.set(i, k, a.get(i, k) / s[k]);
          }
          a.set(k, k, a.get(k, k) + 1);
        }
        s[k] = -s[k];
      }

      for (let j = k + 1; j < n; j++) {
        if (k < nct && s[k] !== 0) {
          let t = 0;
          for (let i = k; i < m; i++) {
            t += a.get(i, k) * a.get(i, j);
          }
          t = -t / a.get(k, k);
          for (let i = k; i < m; i++) {
            a.set(i, j, a.get(i, j) + t * a.get(i, k));
          }
        }
        e[j] = a.get(k, j);
      }

      if (wantu && k < nct) {
        for (let i = k; i < m; i++) {
          U.set(i, k, a.get(i, k));
        }
      }

      if (k < nrt) {
        e[k] = 0;
        for (let i = k + 1; i < n; i++) {
          e[k] = hypotenuse(e[k], e[i]);
        }
        if (e[k] !== 0) {
          if (e[k + 1] < 0) {
            e[k] = 0 - e[k];
          }
          for (let i = k + 1; i < n; i++) {
            e[i] /= e[k];
          }
          e[k + 1] += 1;
        }
        e[k] = -e[k];
        if (k + 1 < m && e[k] !== 0) {
          for (let i = k + 1; i < m; i++) {
            work[i] = 0;
          }
          for (let i = k + 1; i < m; i++) {
            for (let j = k + 1; j < n; j++) {
              work[i] += e[j] * a.get(i, j);
            }
          }
          for (let j = k + 1; j < n; j++) {
            let t = -e[j] / e[k + 1];
            for (let i = k + 1; i < m; i++) {
              a.set(i, j, a.get(i, j) + t * work[i]);
            }
          }
        }
        if (wantv) {
          for (let i = k + 1; i < n; i++) {
            V.set(i, k, e[i]);
          }
        }
      }
    }

    let p = Math.min(n, m + 1);
    if (nct < n) {
      s[nct] = a.get(nct, nct);
    }
    if (m < p) {
      s[p - 1] = 0;
    }
    if (nrt + 1 < p) {
      e[nrt] = a.get(nrt, p - 1);
    }
    e[p - 1] = 0;

    if (wantu) {
      for (let j = nct; j < nu; j++) {
        for (let i = 0; i < m; i++) {
          U.set(i, j, 0);
        }
        U.set(j, j, 1);
      }
      for (let k = nct - 1; k >= 0; k--) {
        if (s[k] !== 0) {
          for (let j = k + 1; j < nu; j++) {
            let t = 0;
            for (let i = k; i < m; i++) {
              t += U.get(i, k) * U.get(i, j);
            }
            t = -t / U.get(k, k);
            for (let i = k; i < m; i++) {
              U.set(i, j, U.get(i, j) + t * U.get(i, k));
            }
          }
          for (let i = k; i < m; i++) {
            U.set(i, k, -U.get(i, k));
          }
          U.set(k, k, 1 + U.get(k, k));
          for (let i = 0; i < k - 1; i++) {
            U.set(i, k, 0);
          }
        } else {
          for (let i = 0; i < m; i++) {
            U.set(i, k, 0);
          }
          U.set(k, k, 1);
        }
      }
    }

    if (wantv) {
      for (let k = n - 1; k >= 0; k--) {
        if (k < nrt && e[k] !== 0) {
          for (let j = k + 1; j < n; j++) {
            let t = 0;
            for (let i = k + 1; i < n; i++) {
              t += V.get(i, k) * V.get(i, j);
            }
            t = -t / V.get(k + 1, k);
            for (let i = k + 1; i < n; i++) {
              V.set(i, j, V.get(i, j) + t * V.get(i, k));
            }
          }
        }
        for (let i = 0; i < n; i++) {
          V.set(i, k, 0);
        }
        V.set(k, k, 1);
      }
    }

    let pp = p - 1;
    let eps = Number.EPSILON;
    while (p > 0) {
      let k, kase;
      for (k = p - 2; k >= -1; k--) {
        if (k === -1) {
          break;
        }
        const alpha =
          Number.MIN_VALUE + eps * Math.abs(s[k] + Math.abs(s[k + 1]));
        if (Math.abs(e[k]) <= alpha || Number.isNaN(e[k])) {
          e[k] = 0;
          break;
        }
      }
      if (k === p - 2) {
        kase = 4;
      } else {
        let ks;
        for (ks = p - 1; ks >= k; ks--) {
          if (ks === k) {
            break;
          }
          let t =
            (ks !== p ? Math.abs(e[ks]) : 0) +
            (ks !== k + 1 ? Math.abs(e[ks - 1]) : 0);
          if (Math.abs(s[ks]) <= eps * t) {
            s[ks] = 0;
            break;
          }
        }
        if (ks === k) {
          kase = 3;
        } else if (ks === p - 1) {
          kase = 1;
        } else {
          kase = 2;
          k = ks;
        }
      }

      k++;

      switch (kase) {
        case 1: {
          let f = e[p - 2];
          e[p - 2] = 0;
          for (let j = p - 2; j >= k; j--) {
            let t = hypotenuse(s[j], f);
            let cs = s[j] / t;
            let sn = f / t;
            s[j] = t;
            if (j !== k) {
              f = -sn * e[j - 1];
              e[j - 1] = cs * e[j - 1];
            }
            if (wantv) {
              for (let i = 0; i < n; i++) {
                t = cs * V.get(i, j) + sn * V.get(i, p - 1);
                V.set(i, p - 1, -sn * V.get(i, j) + cs * V.get(i, p - 1));
                V.set(i, j, t);
              }
            }
          }
          break;
        }
        case 2: {
          let f = e[k - 1];
          e[k - 1] = 0;
          for (let j = k; j < p; j++) {
            let t = hypotenuse(s[j], f);
            let cs = s[j] / t;
            let sn = f / t;
            s[j] = t;
            f = -sn * e[j];
            e[j] = cs * e[j];
            if (wantu) {
              for (let i = 0; i < m; i++) {
                t = cs * U.get(i, j) + sn * U.get(i, k - 1);
                U.set(i, k - 1, -sn * U.get(i, j) + cs * U.get(i, k - 1));
                U.set(i, j, t);
              }
            }
          }
          break;
        }
        case 3: {
          const scale = Math.max(
            Math.abs(s[p - 1]),
            Math.abs(s[p - 2]),
            Math.abs(e[p - 2]),
            Math.abs(s[k]),
            Math.abs(e[k]),
          );
          const sp = s[p - 1] / scale;
          const spm1 = s[p - 2] / scale;
          const epm1 = e[p - 2] / scale;
          const sk = s[k] / scale;
          const ek = e[k] / scale;
          const b = ((spm1 + sp) * (spm1 - sp) + epm1 * epm1) / 2;
          const c = sp * epm1 * (sp * epm1);
          let shift = 0;
          if (b !== 0 || c !== 0) {
            if (b < 0) {
              shift = 0 - Math.sqrt(b * b + c);
            } else {
              shift = Math.sqrt(b * b + c);
            }
            shift = c / (b + shift);
          }
          let f = (sk + sp) * (sk - sp) + shift;
          let g = sk * ek;
          for (let j = k; j < p - 1; j++) {
            let t = hypotenuse(f, g);
            if (t === 0) t = Number.MIN_VALUE;
            let cs = f / t;
            let sn = g / t;
            if (j !== k) {
              e[j - 1] = t;
            }
            f = cs * s[j] + sn * e[j];
            e[j] = cs * e[j] - sn * s[j];
            g = sn * s[j + 1];
            s[j + 1] = cs * s[j + 1];
            if (wantv) {
              for (let i = 0; i < n; i++) {
                t = cs * V.get(i, j) + sn * V.get(i, j + 1);
                V.set(i, j + 1, -sn * V.get(i, j) + cs * V.get(i, j + 1));
                V.set(i, j, t);
              }
            }
            t = hypotenuse(f, g);
            if (t === 0) t = Number.MIN_VALUE;
            cs = f / t;
            sn = g / t;
            s[j] = t;
            f = cs * e[j] + sn * s[j + 1];
            s[j + 1] = -sn * e[j] + cs * s[j + 1];
            g = sn * e[j + 1];
            e[j + 1] = cs * e[j + 1];
            if (wantu && j < m - 1) {
              for (let i = 0; i < m; i++) {
                t = cs * U.get(i, j) + sn * U.get(i, j + 1);
                U.set(i, j + 1, -sn * U.get(i, j) + cs * U.get(i, j + 1));
                U.set(i, j, t);
              }
            }
          }
          e[p - 2] = f;
          break;
        }
        case 4: {
          if (s[k] <= 0) {
            s[k] = s[k] < 0 ? -s[k] : 0;
            if (wantv) {
              for (let i = 0; i <= pp; i++) {
                V.set(i, k, -V.get(i, k));
              }
            }
          }
          while (k < pp) {
            if (s[k] >= s[k + 1]) {
              break;
            }
            let t = s[k];
            s[k] = s[k + 1];
            s[k + 1] = t;
            if (wantv && k < n - 1) {
              for (let i = 0; i < n; i++) {
                t = V.get(i, k + 1);
                V.set(i, k + 1, V.get(i, k));
                V.set(i, k, t);
              }
            }
            if (wantu && k < m - 1) {
              for (let i = 0; i < m; i++) {
                t = U.get(i, k + 1);
                U.set(i, k + 1, U.get(i, k));
                U.set(i, k, t);
              }
            }
            k++;
          }
          p--;
          break;
        }
        // no default
      }
    }

    if (swapped) {
      let tmp = V;
      V = U;
      U = tmp;
    }

    this.m = m;
    this.n = n;
    this.s = s;
    this.U = U;
    this.V = V;
  }

  solve(value) {
    let Y = value;
    let e = this.threshold;
    let scols = this.s.length;
    let Ls = Matrix$1.zeros(scols, scols);

    for (let i = 0; i < scols; i++) {
      if (Math.abs(this.s[i]) <= e) {
        Ls.set(i, i, 0);
      } else {
        Ls.set(i, i, 1 / this.s[i]);
      }
    }

    let U = this.U;
    let V = this.rightSingularVectors;

    let VL = V.mmul(Ls);
    let vrows = V.rows;
    let urows = U.rows;
    let VLU = Matrix$1.zeros(vrows, urows);

    for (let i = 0; i < vrows; i++) {
      for (let j = 0; j < urows; j++) {
        let sum = 0;
        for (let k = 0; k < scols; k++) {
          sum += VL.get(i, k) * U.get(j, k);
        }
        VLU.set(i, j, sum);
      }
    }

    return VLU.mmul(Y);
  }

  solveForDiagonal(value) {
    return this.solve(Matrix$1.diag(value));
  }

  inverse() {
    let V = this.V;
    let e = this.threshold;
    let vrows = V.rows;
    let vcols = V.columns;
    let X = new Matrix$1(vrows, this.s.length);

    for (let i = 0; i < vrows; i++) {
      for (let j = 0; j < vcols; j++) {
        if (Math.abs(this.s[j]) > e) {
          X.set(i, j, V.get(i, j) / this.s[j]);
        }
      }
    }

    let U = this.U;

    let urows = U.rows;
    let ucols = U.columns;
    let Y = new Matrix$1(vrows, urows);

    for (let i = 0; i < vrows; i++) {
      for (let j = 0; j < urows; j++) {
        let sum = 0;
        for (let k = 0; k < ucols; k++) {
          sum += X.get(i, k) * U.get(j, k);
        }
        Y.set(i, j, sum);
      }
    }

    return Y;
  }

  get condition() {
    return this.s[0] / this.s[Math.min(this.m, this.n) - 1];
  }

  get norm2() {
    return this.s[0];
  }

  get rank() {
    let tol = Math.max(this.m, this.n) * this.s[0] * Number.EPSILON;
    let r = 0;
    let s = this.s;
    for (let i = 0, ii = s.length; i < ii; i++) {
      if (s[i] > tol) {
        r++;
      }
    }
    return r;
  }

  get diagonal() {
    return Array.from(this.s);
  }

  get threshold() {
    return (Number.EPSILON / 2) * Math.max(this.m, this.n) * this.s[0];
  }

  get leftSingularVectors() {
    return this.U;
  }

  get rightSingularVectors() {
    return this.V;
  }

  get diagonalMatrix() {
    return Matrix$1.diag(this.s);
  }
}

function inverse$1(matrix, useSVD = false) {
  matrix = WrapperMatrix2D.checkMatrix(matrix);
  if (useSVD) {
    return new SingularValueDecomposition(matrix).inverse();
  } else {
    return solve(matrix, Matrix$1.eye(matrix.rows));
  }
}

function solve(leftHandSide, rightHandSide, useSVD = false) {
  leftHandSide = WrapperMatrix2D.checkMatrix(leftHandSide);
  rightHandSide = WrapperMatrix2D.checkMatrix(rightHandSide);
  if (useSVD) {
    return new SingularValueDecomposition(leftHandSide).solve(rightHandSide);
  } else {
    return leftHandSide.isSquare()
      ? new LuDecomposition(leftHandSide).solve(rightHandSide)
      : new QrDecomposition(leftHandSide).solve(rightHandSide);
  }
}

function determinant(matrix) {
  matrix = Matrix$1.checkMatrix(matrix);
  if (matrix.isSquare()) {
    if (matrix.columns === 0) {
      return 1;
    }

    let a, b, c, d;
    if (matrix.columns === 2) {
      // 2 x 2 matrix
      a = matrix.get(0, 0);
      b = matrix.get(0, 1);
      c = matrix.get(1, 0);
      d = matrix.get(1, 1);

      return a * d - b * c;
    } else if (matrix.columns === 3) {
      // 3 x 3 matrix
      let subMatrix0, subMatrix1, subMatrix2;
      subMatrix0 = new MatrixSelectionView(matrix, [1, 2], [1, 2]);
      subMatrix1 = new MatrixSelectionView(matrix, [1, 2], [0, 2]);
      subMatrix2 = new MatrixSelectionView(matrix, [1, 2], [0, 1]);
      a = matrix.get(0, 0);
      b = matrix.get(0, 1);
      c = matrix.get(0, 2);

      return (
        a * determinant(subMatrix0) -
        b * determinant(subMatrix1) +
        c * determinant(subMatrix2)
      );
    } else {
      // general purpose determinant using the LU decomposition
      return new LuDecomposition(matrix).determinant;
    }
  } else {
    throw Error('determinant can only be calculated for a square matrix');
  }
}

function xrange(n, exception) {
  let range = [];
  for (let i = 0; i < n; i++) {
    if (i !== exception) {
      range.push(i);
    }
  }
  return range;
}

function dependenciesOneRow(
  error,
  matrix,
  index,
  thresholdValue = 10e-10,
  thresholdError = 10e-10,
) {
  if (error > thresholdError) {
    return new Array(matrix.rows + 1).fill(0);
  } else {
    let returnArray = matrix.addRow(index, [0]);
    for (let i = 0; i < returnArray.rows; i++) {
      if (Math.abs(returnArray.get(i, 0)) < thresholdValue) {
        returnArray.set(i, 0, 0);
      }
    }
    return returnArray.to1DArray();
  }
}

function linearDependencies(matrix, options = {}) {
  const { thresholdValue = 10e-10, thresholdError = 10e-10 } = options;
  matrix = Matrix$1.checkMatrix(matrix);

  let n = matrix.rows;
  let results = new Matrix$1(n, n);

  for (let i = 0; i < n; i++) {
    let b = Matrix$1.columnVector(matrix.getRow(i));
    let Abis = matrix.subMatrixRow(xrange(n, i)).transpose();
    let svd = new SingularValueDecomposition(Abis);
    let x = svd.solve(b);
    let error = Matrix$1.sub(b, Abis.mmul(x)).abs().max();
    results.setRow(
      i,
      dependenciesOneRow(error, x, i, thresholdValue, thresholdError),
    );
  }
  return results;
}

function pseudoInverse(matrix, threshold = Number.EPSILON) {
  matrix = Matrix$1.checkMatrix(matrix);
  if (matrix.isEmpty()) {
    // with a zero dimension, the pseudo-inverse is the transpose, since all 0xn and nx0 matrices are singular
    // (0xn)*(nx0)*(0xn) = 0xn
    // (nx0)*(0xn)*(nx0) = nx0
    return matrix.transpose();
  }
  let svdSolution = new SingularValueDecomposition(matrix, { autoTranspose: true });

  let U = svdSolution.leftSingularVectors;
  let V = svdSolution.rightSingularVectors;
  let s = svdSolution.diagonal;

  for (let i = 0; i < s.length; i++) {
    if (Math.abs(s[i]) > threshold) {
      s[i] = 1.0 / s[i];
    } else {
      s[i] = 0.0;
    }
  }

  return V.mmul(Matrix$1.diag(s).mmul(U.transpose()));
}

function covariance(xMatrix, yMatrix = xMatrix, options = {}) {
  xMatrix = new Matrix$1(xMatrix);
  let yIsSame = false;
  if (
    typeof yMatrix === 'object' &&
    !Matrix$1.isMatrix(yMatrix) &&
    !isAnyArray.isAnyArray(yMatrix)
  ) {
    options = yMatrix;
    yMatrix = xMatrix;
    yIsSame = true;
  } else {
    yMatrix = new Matrix$1(yMatrix);
  }
  if (xMatrix.rows !== yMatrix.rows) {
    throw new TypeError('Both matrices must have the same number of rows');
  }
  const { center = true } = options;
  if (center) {
    xMatrix = xMatrix.center('column');
    if (!yIsSame) {
      yMatrix = yMatrix.center('column');
    }
  }
  const cov = xMatrix.transpose().mmul(yMatrix);
  for (let i = 0; i < cov.rows; i++) {
    for (let j = 0; j < cov.columns; j++) {
      cov.set(i, j, cov.get(i, j) * (1 / (xMatrix.rows - 1)));
    }
  }
  return cov;
}

function correlation(xMatrix, yMatrix = xMatrix, options = {}) {
  xMatrix = new Matrix$1(xMatrix);
  let yIsSame = false;
  if (
    typeof yMatrix === 'object' &&
    !Matrix$1.isMatrix(yMatrix) &&
    !isAnyArray.isAnyArray(yMatrix)
  ) {
    options = yMatrix;
    yMatrix = xMatrix;
    yIsSame = true;
  } else {
    yMatrix = new Matrix$1(yMatrix);
  }
  if (xMatrix.rows !== yMatrix.rows) {
    throw new TypeError('Both matrices must have the same number of rows');
  }

  const { center = true, scale = true } = options;
  if (center) {
    xMatrix.center('column');
    if (!yIsSame) {
      yMatrix.center('column');
    }
  }
  if (scale) {
    xMatrix.scale('column');
    if (!yIsSame) {
      yMatrix.scale('column');
    }
  }

  const sdx = xMatrix.standardDeviation('column', { unbiased: true });
  const sdy = yIsSame
    ? sdx
    : yMatrix.standardDeviation('column', { unbiased: true });

  const corr = xMatrix.transpose().mmul(yMatrix);
  for (let i = 0; i < corr.rows; i++) {
    for (let j = 0; j < corr.columns; j++) {
      corr.set(
        i,
        j,
        corr.get(i, j) * (1 / (sdx[i] * sdy[j])) * (1 / (xMatrix.rows - 1)),
      );
    }
  }
  return corr;
}

class EigenvalueDecomposition {
  constructor(matrix, options = {}) {
    const { assumeSymmetric = false } = options;

    matrix = WrapperMatrix2D.checkMatrix(matrix);
    if (!matrix.isSquare()) {
      throw new Error('Matrix is not a square matrix');
    }

    if (matrix.isEmpty()) {
      throw new Error('Matrix must be non-empty');
    }

    let n = matrix.columns;
    let V = new Matrix$1(n, n);
    let d = new Float64Array(n);
    let e = new Float64Array(n);
    let value = matrix;
    let i, j;

    let isSymmetric = false;
    if (assumeSymmetric) {
      isSymmetric = true;
    } else {
      isSymmetric = matrix.isSymmetric();
    }

    if (isSymmetric) {
      for (i = 0; i < n; i++) {
        for (j = 0; j < n; j++) {
          V.set(i, j, value.get(i, j));
        }
      }
      tred2(n, e, d, V);
      tql2(n, e, d, V);
    } else {
      let H = new Matrix$1(n, n);
      let ort = new Float64Array(n);
      for (j = 0; j < n; j++) {
        for (i = 0; i < n; i++) {
          H.set(i, j, value.get(i, j));
        }
      }
      orthes(n, H, ort, V);
      hqr2(n, e, d, V, H);
    }

    this.n = n;
    this.e = e;
    this.d = d;
    this.V = V;
  }

  get realEigenvalues() {
    return Array.from(this.d);
  }

  get imaginaryEigenvalues() {
    return Array.from(this.e);
  }

  get eigenvectorMatrix() {
    return this.V;
  }

  get diagonalMatrix() {
    let n = this.n;
    let e = this.e;
    let d = this.d;
    let X = new Matrix$1(n, n);
    let i, j;
    for (i = 0; i < n; i++) {
      for (j = 0; j < n; j++) {
        X.set(i, j, 0);
      }
      X.set(i, i, d[i]);
      if (e[i] > 0) {
        X.set(i, i + 1, e[i]);
      } else if (e[i] < 0) {
        X.set(i, i - 1, e[i]);
      }
    }
    return X;
  }
}

function tred2(n, e, d, V) {
  let f, g, h, i, j, k, hh, scale;

  for (j = 0; j < n; j++) {
    d[j] = V.get(n - 1, j);
  }

  for (i = n - 1; i > 0; i--) {
    scale = 0;
    h = 0;
    for (k = 0; k < i; k++) {
      scale = scale + Math.abs(d[k]);
    }

    if (scale === 0) {
      e[i] = d[i - 1];
      for (j = 0; j < i; j++) {
        d[j] = V.get(i - 1, j);
        V.set(i, j, 0);
        V.set(j, i, 0);
      }
    } else {
      for (k = 0; k < i; k++) {
        d[k] /= scale;
        h += d[k] * d[k];
      }

      f = d[i - 1];
      g = Math.sqrt(h);
      if (f > 0) {
        g = -g;
      }

      e[i] = scale * g;
      h = h - f * g;
      d[i - 1] = f - g;
      for (j = 0; j < i; j++) {
        e[j] = 0;
      }

      for (j = 0; j < i; j++) {
        f = d[j];
        V.set(j, i, f);
        g = e[j] + V.get(j, j) * f;
        for (k = j + 1; k <= i - 1; k++) {
          g += V.get(k, j) * d[k];
          e[k] += V.get(k, j) * f;
        }
        e[j] = g;
      }

      f = 0;
      for (j = 0; j < i; j++) {
        e[j] /= h;
        f += e[j] * d[j];
      }

      hh = f / (h + h);
      for (j = 0; j < i; j++) {
        e[j] -= hh * d[j];
      }

      for (j = 0; j < i; j++) {
        f = d[j];
        g = e[j];
        for (k = j; k <= i - 1; k++) {
          V.set(k, j, V.get(k, j) - (f * e[k] + g * d[k]));
        }
        d[j] = V.get(i - 1, j);
        V.set(i, j, 0);
      }
    }
    d[i] = h;
  }

  for (i = 0; i < n - 1; i++) {
    V.set(n - 1, i, V.get(i, i));
    V.set(i, i, 1);
    h = d[i + 1];
    if (h !== 0) {
      for (k = 0; k <= i; k++) {
        d[k] = V.get(k, i + 1) / h;
      }

      for (j = 0; j <= i; j++) {
        g = 0;
        for (k = 0; k <= i; k++) {
          g += V.get(k, i + 1) * V.get(k, j);
        }
        for (k = 0; k <= i; k++) {
          V.set(k, j, V.get(k, j) - g * d[k]);
        }
      }
    }

    for (k = 0; k <= i; k++) {
      V.set(k, i + 1, 0);
    }
  }

  for (j = 0; j < n; j++) {
    d[j] = V.get(n - 1, j);
    V.set(n - 1, j, 0);
  }

  V.set(n - 1, n - 1, 1);
  e[0] = 0;
}

function tql2(n, e, d, V) {
  let g, h, i, j, k, l, m, p, r, dl1, c, c2, c3, el1, s, s2;

  for (i = 1; i < n; i++) {
    e[i - 1] = e[i];
  }

  e[n - 1] = 0;

  let f = 0;
  let tst1 = 0;
  let eps = Number.EPSILON;

  for (l = 0; l < n; l++) {
    tst1 = Math.max(tst1, Math.abs(d[l]) + Math.abs(e[l]));
    m = l;
    while (m < n) {
      if (Math.abs(e[m]) <= eps * tst1) {
        break;
      }
      m++;
    }

    if (m > l) {
      do {

        g = d[l];
        p = (d[l + 1] - g) / (2 * e[l]);
        r = hypotenuse(p, 1);
        if (p < 0) {
          r = -r;
        }

        d[l] = e[l] / (p + r);
        d[l + 1] = e[l] * (p + r);
        dl1 = d[l + 1];
        h = g - d[l];
        for (i = l + 2; i < n; i++) {
          d[i] -= h;
        }

        f = f + h;

        p = d[m];
        c = 1;
        c2 = c;
        c3 = c;
        el1 = e[l + 1];
        s = 0;
        s2 = 0;
        for (i = m - 1; i >= l; i--) {
          c3 = c2;
          c2 = c;
          s2 = s;
          g = c * e[i];
          h = c * p;
          r = hypotenuse(p, e[i]);
          e[i + 1] = s * r;
          s = e[i] / r;
          c = p / r;
          p = c * d[i] - s * g;
          d[i + 1] = h + s * (c * g + s * d[i]);

          for (k = 0; k < n; k++) {
            h = V.get(k, i + 1);
            V.set(k, i + 1, s * V.get(k, i) + c * h);
            V.set(k, i, c * V.get(k, i) - s * h);
          }
        }

        p = (-s * s2 * c3 * el1 * e[l]) / dl1;
        e[l] = s * p;
        d[l] = c * p;
      } while (Math.abs(e[l]) > eps * tst1);
    }
    d[l] = d[l] + f;
    e[l] = 0;
  }

  for (i = 0; i < n - 1; i++) {
    k = i;
    p = d[i];
    for (j = i + 1; j < n; j++) {
      if (d[j] < p) {
        k = j;
        p = d[j];
      }
    }

    if (k !== i) {
      d[k] = d[i];
      d[i] = p;
      for (j = 0; j < n; j++) {
        p = V.get(j, i);
        V.set(j, i, V.get(j, k));
        V.set(j, k, p);
      }
    }
  }
}

function orthes(n, H, ort, V) {
  let low = 0;
  let high = n - 1;
  let f, g, h, i, j, m;
  let scale;

  for (m = low + 1; m <= high - 1; m++) {
    scale = 0;
    for (i = m; i <= high; i++) {
      scale = scale + Math.abs(H.get(i, m - 1));
    }

    if (scale !== 0) {
      h = 0;
      for (i = high; i >= m; i--) {
        ort[i] = H.get(i, m - 1) / scale;
        h += ort[i] * ort[i];
      }

      g = Math.sqrt(h);
      if (ort[m] > 0) {
        g = -g;
      }

      h = h - ort[m] * g;
      ort[m] = ort[m] - g;

      for (j = m; j < n; j++) {
        f = 0;
        for (i = high; i >= m; i--) {
          f += ort[i] * H.get(i, j);
        }

        f = f / h;
        for (i = m; i <= high; i++) {
          H.set(i, j, H.get(i, j) - f * ort[i]);
        }
      }

      for (i = 0; i <= high; i++) {
        f = 0;
        for (j = high; j >= m; j--) {
          f += ort[j] * H.get(i, j);
        }

        f = f / h;
        for (j = m; j <= high; j++) {
          H.set(i, j, H.get(i, j) - f * ort[j]);
        }
      }

      ort[m] = scale * ort[m];
      H.set(m, m - 1, scale * g);
    }
  }

  for (i = 0; i < n; i++) {
    for (j = 0; j < n; j++) {
      V.set(i, j, i === j ? 1 : 0);
    }
  }

  for (m = high - 1; m >= low + 1; m--) {
    if (H.get(m, m - 1) !== 0) {
      for (i = m + 1; i <= high; i++) {
        ort[i] = H.get(i, m - 1);
      }

      for (j = m; j <= high; j++) {
        g = 0;
        for (i = m; i <= high; i++) {
          g += ort[i] * V.get(i, j);
        }

        g = g / ort[m] / H.get(m, m - 1);
        for (i = m; i <= high; i++) {
          V.set(i, j, V.get(i, j) + g * ort[i]);
        }
      }
    }
  }
}

function hqr2(nn, e, d, V, H) {
  let n = nn - 1;
  let low = 0;
  let high = nn - 1;
  let eps = Number.EPSILON;
  let exshift = 0;
  let norm = 0;
  let p = 0;
  let q = 0;
  let r = 0;
  let s = 0;
  let z = 0;
  let iter = 0;
  let i, j, k, l, m, t, w, x, y;
  let ra, sa, vr, vi;
  let notlast, cdivres;

  for (i = 0; i < nn; i++) {
    if (i < low || i > high) {
      d[i] = H.get(i, i);
      e[i] = 0;
    }

    for (j = Math.max(i - 1, 0); j < nn; j++) {
      norm = norm + Math.abs(H.get(i, j));
    }
  }

  while (n >= low) {
    l = n;
    while (l > low) {
      s = Math.abs(H.get(l - 1, l - 1)) + Math.abs(H.get(l, l));
      if (s === 0) {
        s = norm;
      }
      if (Math.abs(H.get(l, l - 1)) < eps * s) {
        break;
      }
      l--;
    }

    if (l === n) {
      H.set(n, n, H.get(n, n) + exshift);
      d[n] = H.get(n, n);
      e[n] = 0;
      n--;
      iter = 0;
    } else if (l === n - 1) {
      w = H.get(n, n - 1) * H.get(n - 1, n);
      p = (H.get(n - 1, n - 1) - H.get(n, n)) / 2;
      q = p * p + w;
      z = Math.sqrt(Math.abs(q));
      H.set(n, n, H.get(n, n) + exshift);
      H.set(n - 1, n - 1, H.get(n - 1, n - 1) + exshift);
      x = H.get(n, n);

      if (q >= 0) {
        z = p >= 0 ? p + z : p - z;
        d[n - 1] = x + z;
        d[n] = d[n - 1];
        if (z !== 0) {
          d[n] = x - w / z;
        }
        e[n - 1] = 0;
        e[n] = 0;
        x = H.get(n, n - 1);
        s = Math.abs(x) + Math.abs(z);
        p = x / s;
        q = z / s;
        r = Math.sqrt(p * p + q * q);
        p = p / r;
        q = q / r;

        for (j = n - 1; j < nn; j++) {
          z = H.get(n - 1, j);
          H.set(n - 1, j, q * z + p * H.get(n, j));
          H.set(n, j, q * H.get(n, j) - p * z);
        }

        for (i = 0; i <= n; i++) {
          z = H.get(i, n - 1);
          H.set(i, n - 1, q * z + p * H.get(i, n));
          H.set(i, n, q * H.get(i, n) - p * z);
        }

        for (i = low; i <= high; i++) {
          z = V.get(i, n - 1);
          V.set(i, n - 1, q * z + p * V.get(i, n));
          V.set(i, n, q * V.get(i, n) - p * z);
        }
      } else {
        d[n - 1] = x + p;
        d[n] = x + p;
        e[n - 1] = z;
        e[n] = -z;
      }

      n = n - 2;
      iter = 0;
    } else {
      x = H.get(n, n);
      y = 0;
      w = 0;
      if (l < n) {
        y = H.get(n - 1, n - 1);
        w = H.get(n, n - 1) * H.get(n - 1, n);
      }

      if (iter === 10) {
        exshift += x;
        for (i = low; i <= n; i++) {
          H.set(i, i, H.get(i, i) - x);
        }
        s = Math.abs(H.get(n, n - 1)) + Math.abs(H.get(n - 1, n - 2));
        // eslint-disable-next-line no-multi-assign
        x = y = 0.75 * s;
        w = -0.4375 * s * s;
      }

      if (iter === 30) {
        s = (y - x) / 2;
        s = s * s + w;
        if (s > 0) {
          s = Math.sqrt(s);
          if (y < x) {
            s = -s;
          }
          s = x - w / ((y - x) / 2 + s);
          for (i = low; i <= n; i++) {
            H.set(i, i, H.get(i, i) - s);
          }
          exshift += s;
          // eslint-disable-next-line no-multi-assign
          x = y = w = 0.964;
        }
      }

      iter = iter + 1;

      m = n - 2;
      while (m >= l) {
        z = H.get(m, m);
        r = x - z;
        s = y - z;
        p = (r * s - w) / H.get(m + 1, m) + H.get(m, m + 1);
        q = H.get(m + 1, m + 1) - z - r - s;
        r = H.get(m + 2, m + 1);
        s = Math.abs(p) + Math.abs(q) + Math.abs(r);
        p = p / s;
        q = q / s;
        r = r / s;
        if (m === l) {
          break;
        }
        if (
          Math.abs(H.get(m, m - 1)) * (Math.abs(q) + Math.abs(r)) <
          eps *
            (Math.abs(p) *
              (Math.abs(H.get(m - 1, m - 1)) +
                Math.abs(z) +
                Math.abs(H.get(m + 1, m + 1))))
        ) {
          break;
        }
        m--;
      }

      for (i = m + 2; i <= n; i++) {
        H.set(i, i - 2, 0);
        if (i > m + 2) {
          H.set(i, i - 3, 0);
        }
      }

      for (k = m; k <= n - 1; k++) {
        notlast = k !== n - 1;
        if (k !== m) {
          p = H.get(k, k - 1);
          q = H.get(k + 1, k - 1);
          r = notlast ? H.get(k + 2, k - 1) : 0;
          x = Math.abs(p) + Math.abs(q) + Math.abs(r);
          if (x !== 0) {
            p = p / x;
            q = q / x;
            r = r / x;
          }
        }

        if (x === 0) {
          break;
        }

        s = Math.sqrt(p * p + q * q + r * r);
        if (p < 0) {
          s = -s;
        }

        if (s !== 0) {
          if (k !== m) {
            H.set(k, k - 1, -s * x);
          } else if (l !== m) {
            H.set(k, k - 1, -H.get(k, k - 1));
          }

          p = p + s;
          x = p / s;
          y = q / s;
          z = r / s;
          q = q / p;
          r = r / p;

          for (j = k; j < nn; j++) {
            p = H.get(k, j) + q * H.get(k + 1, j);
            if (notlast) {
              p = p + r * H.get(k + 2, j);
              H.set(k + 2, j, H.get(k + 2, j) - p * z);
            }

            H.set(k, j, H.get(k, j) - p * x);
            H.set(k + 1, j, H.get(k + 1, j) - p * y);
          }

          for (i = 0; i <= Math.min(n, k + 3); i++) {
            p = x * H.get(i, k) + y * H.get(i, k + 1);
            if (notlast) {
              p = p + z * H.get(i, k + 2);
              H.set(i, k + 2, H.get(i, k + 2) - p * r);
            }

            H.set(i, k, H.get(i, k) - p);
            H.set(i, k + 1, H.get(i, k + 1) - p * q);
          }

          for (i = low; i <= high; i++) {
            p = x * V.get(i, k) + y * V.get(i, k + 1);
            if (notlast) {
              p = p + z * V.get(i, k + 2);
              V.set(i, k + 2, V.get(i, k + 2) - p * r);
            }

            V.set(i, k, V.get(i, k) - p);
            V.set(i, k + 1, V.get(i, k + 1) - p * q);
          }
        }
      }
    }
  }

  if (norm === 0) {
    return;
  }

  for (n = nn - 1; n >= 0; n--) {
    p = d[n];
    q = e[n];

    if (q === 0) {
      l = n;
      H.set(n, n, 1);
      for (i = n - 1; i >= 0; i--) {
        w = H.get(i, i) - p;
        r = 0;
        for (j = l; j <= n; j++) {
          r = r + H.get(i, j) * H.get(j, n);
        }

        if (e[i] < 0) {
          z = w;
          s = r;
        } else {
          l = i;
          if (e[i] === 0) {
            H.set(i, n, w !== 0 ? -r / w : -r / (eps * norm));
          } else {
            x = H.get(i, i + 1);
            y = H.get(i + 1, i);
            q = (d[i] - p) * (d[i] - p) + e[i] * e[i];
            t = (x * s - z * r) / q;
            H.set(i, n, t);
            H.set(
              i + 1,
              n,
              Math.abs(x) > Math.abs(z) ? (-r - w * t) / x : (-s - y * t) / z,
            );
          }

          t = Math.abs(H.get(i, n));
          if (eps * t * t > 1) {
            for (j = i; j <= n; j++) {
              H.set(j, n, H.get(j, n) / t);
            }
          }
        }
      }
    } else if (q < 0) {
      l = n - 1;

      if (Math.abs(H.get(n, n - 1)) > Math.abs(H.get(n - 1, n))) {
        H.set(n - 1, n - 1, q / H.get(n, n - 1));
        H.set(n - 1, n, -(H.get(n, n) - p) / H.get(n, n - 1));
      } else {
        cdivres = cdiv(0, -H.get(n - 1, n), H.get(n - 1, n - 1) - p, q);
        H.set(n - 1, n - 1, cdivres[0]);
        H.set(n - 1, n, cdivres[1]);
      }

      H.set(n, n - 1, 0);
      H.set(n, n, 1);
      for (i = n - 2; i >= 0; i--) {
        ra = 0;
        sa = 0;
        for (j = l; j <= n; j++) {
          ra = ra + H.get(i, j) * H.get(j, n - 1);
          sa = sa + H.get(i, j) * H.get(j, n);
        }

        w = H.get(i, i) - p;

        if (e[i] < 0) {
          z = w;
          r = ra;
          s = sa;
        } else {
          l = i;
          if (e[i] === 0) {
            cdivres = cdiv(-ra, -sa, w, q);
            H.set(i, n - 1, cdivres[0]);
            H.set(i, n, cdivres[1]);
          } else {
            x = H.get(i, i + 1);
            y = H.get(i + 1, i);
            vr = (d[i] - p) * (d[i] - p) + e[i] * e[i] - q * q;
            vi = (d[i] - p) * 2 * q;
            if (vr === 0 && vi === 0) {
              vr =
                eps *
                norm *
                (Math.abs(w) +
                  Math.abs(q) +
                  Math.abs(x) +
                  Math.abs(y) +
                  Math.abs(z));
            }
            cdivres = cdiv(
              x * r - z * ra + q * sa,
              x * s - z * sa - q * ra,
              vr,
              vi,
            );
            H.set(i, n - 1, cdivres[0]);
            H.set(i, n, cdivres[1]);
            if (Math.abs(x) > Math.abs(z) + Math.abs(q)) {
              H.set(
                i + 1,
                n - 1,
                (-ra - w * H.get(i, n - 1) + q * H.get(i, n)) / x,
              );
              H.set(
                i + 1,
                n,
                (-sa - w * H.get(i, n) - q * H.get(i, n - 1)) / x,
              );
            } else {
              cdivres = cdiv(
                -r - y * H.get(i, n - 1),
                -s - y * H.get(i, n),
                z,
                q,
              );
              H.set(i + 1, n - 1, cdivres[0]);
              H.set(i + 1, n, cdivres[1]);
            }
          }

          t = Math.max(Math.abs(H.get(i, n - 1)), Math.abs(H.get(i, n)));
          if (eps * t * t > 1) {
            for (j = i; j <= n; j++) {
              H.set(j, n - 1, H.get(j, n - 1) / t);
              H.set(j, n, H.get(j, n) / t);
            }
          }
        }
      }
    }
  }

  for (i = 0; i < nn; i++) {
    if (i < low || i > high) {
      for (j = i; j < nn; j++) {
        V.set(i, j, H.get(i, j));
      }
    }
  }

  for (j = nn - 1; j >= low; j--) {
    for (i = low; i <= high; i++) {
      z = 0;
      for (k = low; k <= Math.min(j, high); k++) {
        z = z + V.get(i, k) * H.get(k, j);
      }
      V.set(i, j, z);
    }
  }
}

function cdiv(xr, xi, yr, yi) {
  let r, d;
  if (Math.abs(yr) > Math.abs(yi)) {
    r = yi / yr;
    d = yr + r * yi;
    return [(xr + r * xi) / d, (xi - r * xr) / d];
  } else {
    r = yr / yi;
    d = yi + r * yr;
    return [(r * xr + xi) / d, (r * xi - xr) / d];
  }
}

class CholeskyDecomposition {
  constructor(value) {
    value = WrapperMatrix2D.checkMatrix(value);
    if (!value.isSymmetric()) {
      throw new Error('Matrix is not symmetric');
    }

    let a = value;
    let dimension = a.rows;
    let l = new Matrix$1(dimension, dimension);
    let positiveDefinite = true;
    let i, j, k;

    for (j = 0; j < dimension; j++) {
      let d = 0;
      for (k = 0; k < j; k++) {
        let s = 0;
        for (i = 0; i < k; i++) {
          s += l.get(k, i) * l.get(j, i);
        }
        s = (a.get(j, k) - s) / l.get(k, k);
        l.set(j, k, s);
        d = d + s * s;
      }

      d = a.get(j, j) - d;

      positiveDefinite &&= d > 0;
      l.set(j, j, Math.sqrt(Math.max(d, 0)));
      for (k = j + 1; k < dimension; k++) {
        l.set(j, k, 0);
      }
    }

    this.L = l;
    this.positiveDefinite = positiveDefinite;
  }

  isPositiveDefinite() {
    return this.positiveDefinite;
  }

  solve(value) {
    value = WrapperMatrix2D.checkMatrix(value);

    let l = this.L;
    let dimension = l.rows;

    if (value.rows !== dimension) {
      throw new Error('Matrix dimensions do not match');
    }
    if (this.isPositiveDefinite() === false) {
      throw new Error('Matrix is not positive definite');
    }

    let count = value.columns;
    let B = value.clone();
    let i, j, k;

    for (k = 0; k < dimension; k++) {
      for (j = 0; j < count; j++) {
        for (i = 0; i < k; i++) {
          B.set(k, j, B.get(k, j) - B.get(i, j) * l.get(k, i));
        }
        B.set(k, j, B.get(k, j) / l.get(k, k));
      }
    }

    for (k = dimension - 1; k >= 0; k--) {
      for (j = 0; j < count; j++) {
        for (i = k + 1; i < dimension; i++) {
          B.set(k, j, B.get(k, j) - B.get(i, j) * l.get(i, k));
        }
        B.set(k, j, B.get(k, j) / l.get(k, k));
      }
    }

    return B;
  }

  get lowerTriangularMatrix() {
    return this.L;
  }
}

class nipals {
  constructor(X, options = {}) {
    X = WrapperMatrix2D.checkMatrix(X);
    let { Y } = options;
    const {
      scaleScores = false,
      maxIterations = 1000,
      terminationCriteria = 1e-10,
    } = options;

    let u;
    if (Y) {
      if (isAnyArray.isAnyArray(Y) && typeof Y[0] === 'number') {
        Y = Matrix$1.columnVector(Y);
      } else {
        Y = WrapperMatrix2D.checkMatrix(Y);
      }
      if (Y.rows !== X.rows) {
        throw new Error('Y should have the same number of rows as X');
      }
      u = Y.getColumnVector(0);
    } else {
      u = X.getColumnVector(0);
    }

    let diff = 1;
    let t, q, w, tOld;

    for (
      let counter = 0;
      counter < maxIterations && diff > terminationCriteria;
      counter++
    ) {
      w = X.transpose().mmul(u).div(u.transpose().mmul(u).get(0, 0));
      w = w.div(w.norm());

      t = X.mmul(w).div(w.transpose().mmul(w).get(0, 0));

      if (counter > 0) {
        diff = t.clone().sub(tOld).pow(2).sum();
      }
      tOld = t.clone();

      if (Y) {
        q = Y.transpose().mmul(t).div(t.transpose().mmul(t).get(0, 0));
        q = q.div(q.norm());

        u = Y.mmul(q).div(q.transpose().mmul(q).get(0, 0));
      } else {
        u = t;
      }
    }

    if (Y) {
      let p = X.transpose().mmul(t).div(t.transpose().mmul(t).get(0, 0));
      p = p.div(p.norm());
      let xResidual = X.clone().sub(t.clone().mmul(p.transpose()));
      let residual = u.transpose().mmul(t).div(t.transpose().mmul(t).get(0, 0));
      let yResidual = Y.clone().sub(
        t.clone().mulS(residual.get(0, 0)).mmul(q.transpose()),
      );

      this.t = t;
      this.p = p.transpose();
      this.w = w.transpose();
      this.q = q;
      this.u = u;
      this.s = t.transpose().mmul(t);
      this.xResidual = xResidual;
      this.yResidual = yResidual;
      this.betas = residual;
    } else {
      this.w = w.transpose();
      this.s = t.transpose().mmul(t).sqrt();
      if (scaleScores) {
        this.t = t.clone().div(this.s.get(0, 0));
      } else {
        this.t = t;
      }
      this.xResidual = X.sub(t.mmul(w.transpose()));
    }
  }
}

matrix.AbstractMatrix = AbstractMatrix;
matrix.CHO = CholeskyDecomposition;
matrix.CholeskyDecomposition = CholeskyDecomposition;
matrix.DistanceMatrix = DistanceMatrix;
matrix.EVD = EigenvalueDecomposition;
matrix.EigenvalueDecomposition = EigenvalueDecomposition;
matrix.LU = LuDecomposition;
matrix.LuDecomposition = LuDecomposition;
var Matrix_1 = matrix.Matrix = Matrix$1;
matrix.MatrixColumnSelectionView = MatrixColumnSelectionView;
matrix.MatrixColumnView = MatrixColumnView;
matrix.MatrixFlipColumnView = MatrixFlipColumnView;
matrix.MatrixFlipRowView = MatrixFlipRowView;
matrix.MatrixRowSelectionView = MatrixRowSelectionView;
matrix.MatrixRowView = MatrixRowView;
matrix.MatrixSelectionView = MatrixSelectionView;
matrix.MatrixSubView = MatrixSubView;
matrix.MatrixTransposeView = MatrixTransposeView;
matrix.NIPALS = nipals;
matrix.Nipals = nipals;
matrix.QR = QrDecomposition;
matrix.QrDecomposition = QrDecomposition;
matrix.SVD = SingularValueDecomposition;
matrix.SingularValueDecomposition = SingularValueDecomposition;
matrix.SymmetricMatrix = SymmetricMatrix;
matrix.WrapperMatrix1D = WrapperMatrix1D;
matrix.WrapperMatrix2D = WrapperMatrix2D;
matrix.correlation = correlation;
matrix.covariance = covariance;
var _default = matrix.default = Matrix$1;
matrix.determinant = determinant;
var inverse_1 = matrix.inverse = inverse$1;
matrix.linearDependencies = linearDependencies;
matrix.pseudoInverse = pseudoInverse;
matrix.solve = solve;
matrix.wrap = wrap;

const Matrix$2 = Matrix_1;
_default.Matrix ? _default.Matrix : Matrix_1;
const inverse = inverse_1;

/**
 * Difference of the matrix function over the parameters
 * @ignore
 * @param {{x:Array<number>, y:Array<number>}} data - Array of points to fit in the format [x1, x2, ... ], [y1, y2, ... ]
 * @param {Array<number>} evaluatedData - Array of previous evaluated function values
 * @param {Array<number>} params - Array of previous parameter values
 * @param {number|array} gradientDifference - The step size to approximate the jacobian matrix
 * @param {boolean} centralDifference - If true the jacobian matrix is approximated by central differences otherwise by forward differences
 * @param {function} paramFunction - The parameters and returns a function with the independent variable as a parameter
 * @return {Matrix}
 */

function gradientFunction(
  data,
  evaluatedData,
  params,
  gradientDifference,
  paramFunction,
  centralDifference,
) {
  const nbParams = params.length;
  const nbPoints = data.x.length;
  let ans = Matrix$2.zeros(nbParams, nbPoints);

  let rowIndex = 0;
  for (let param = 0; param < nbParams; param++) {
    if (gradientDifference[param] === 0) continue;
    let delta = gradientDifference[param];
    let auxParams = params.slice();
    auxParams[param] += delta;
    let funcParam = paramFunction(auxParams);
    if (!centralDifference) {
      for (let point = 0; point < nbPoints; point++) {
        ans.set(
          rowIndex,
          point,
          (evaluatedData[point] - funcParam(data.x[point])) / delta,
        );
      }
    } else {
      auxParams = params.slice();
      auxParams[param] -= delta;
      delta *= 2;
      let funcParam2 = paramFunction(auxParams);
      for (let point = 0; point < nbPoints; point++) {
        ans.set(
          rowIndex,
          point,
          (funcParam2(data.x[point]) - funcParam(data.x[point])) / delta,
        );
      }
    }
    rowIndex++;
  }

  return ans;
}

/**
 * Matrix function over the samples
 * @ignore
 * @param {{x:Array<number>, y:Array<number>}} data - Array of points to fit in the format [x1, x2, ... ], [y1, y2, ... ]
 * @param {Array<number>} evaluatedData - Array of previous evaluated function values
 * @return {Matrix}
 */
function matrixFunction(data, evaluatedData) {
  const m = data.x.length;

  let ans = new Matrix$2(m, 1);

  for (let point = 0; point < m; point++) {
    ans.set(point, 0, data.y[point] - evaluatedData[point]);
  }
  return ans;
}

/**
 * Iteration for Levenberg-Marquardt
 * @ignore
 * @param {{x:Array<number>, y:Array<number>}} data - Array of points to fit in the format [x1, x2, ... ], [y1, y2, ... ]
 * @param {Array<number>} params - Array of previous parameter values
 * @param {number} damping - Levenberg-Marquardt parameter
 * @param {number|array} gradientDifference - The step size to approximate the jacobian matrix
 * @param {boolean} centralDifference - If true the jacobian matrix is approximated by central differences otherwise by forward differences
 * @param {function} parameterizedFunction - The parameters and returns a function with the independent variable as a parameter
 * @return {Array<number>}
 */
function step(
  data,
  params,
  damping,
  gradientDifference,
  parameterizedFunction,
  centralDifference,
  weights,
) {
  let value = damping;
  let identity = Matrix$2.eye(params.length, params.length, value);

  const func = parameterizedFunction(params);

  let evaluatedData = new Float64Array(data.x.length);
  for (let i = 0; i < data.x.length; i++) {
    evaluatedData[i] = func(data.x[i]);
  }

  let gradientFunc = gradientFunction(
    data,
    evaluatedData,
    params,
    gradientDifference,
    parameterizedFunction,
    centralDifference,
  );
  let residualError = matrixFunction(data, evaluatedData);

  let inverseMatrix = inverse(
    identity.add(
      gradientFunc.mmul(
        gradientFunc.transpose().scale('row', { scale: weights }),
      ),
    ),
  );

  let jacobianWeigthResidualError = gradientFunc.mmul(
    residualError.scale('row', { scale: weights }),
  );

  let perturbations = inverseMatrix.mmul(jacobianWeigthResidualError);

  return {
    perturbations,
    jacobianWeigthResidualError,
  };
}

/**
 * Curve fitting algorithm
 * @param {{x:Array<number>, y:Array<number>}} data - Array of points to fit in the format [x1, x2, ... ], [y1, y2, ... ]
 * @param {function} parameterizedFunction - The parameters and returns a function with the independent variable as a parameter
 * @param {object} [options] - Options object
 * @param {number|array} [options.weights = 1] - weighting vector, if the length does not match with the number of data points, the vector is reconstructed with first value.
 * @param {number} [options.damping = 1e-2] - Levenberg-Marquardt parameter, small values of the damping parameter λ result in a Gauss-Newton update and large
values of λ result in a gradient descent update
 * @param {number} [options.dampingStepDown = 9] - factor to reduce the damping (Levenberg-Marquardt parameter) when there is not an improvement when updating parameters.
 * @param {number} [options.dampingStepUp = 11] - factor to increase the damping (Levenberg-Marquardt parameter) when there is an improvement when updating parameters.
 * @param {number} [options.improvementThreshold = 1e-3] - the threshold to define an improvement through an update of parameters
 * @param {number|array} [options.gradientDifference = 10e-2] - The step size to approximate the jacobian matrix
 * @param {boolean} [options.centralDifference = false] - If true the jacobian matrix is approximated by central differences otherwise by forward differences
 * @param {Array<number>} [options.minValues] - Minimum allowed values for parameters
 * @param {Array<number>} [options.maxValues] - Maximum allowed values for parameters
 * @param {Array<number>} [options.initialValues] - Array of initial parameter values
 * @param {number} [options.maxIterations = 100] - Maximum of allowed iterations
 * @param {number} [options.errorTolerance = 10e-3] - Minimum uncertainty allowed for each point.
 * @param {number} [options.timeout] - maximum time running before throw in seconds.
 * @return {{parameterValues: Array<number>, parameterError: number, iterations: number}}
 */
function levenbergMarquardt(
  data,
  parameterizedFunction,
  options = {},
) {
  let {
    checkTimeout,
    minValues,
    maxValues,
    parameters,
    weightSquare,
    damping,
    dampingStepUp,
    dampingStepDown,
    maxIterations,
    errorTolerance,
    centralDifference,
    gradientDifference,
    improvementThreshold,
  } = checkOptions(data, parameterizedFunction, options);

  let error = errorCalculation(
    data,
    parameters,
    parameterizedFunction,
    weightSquare,
  );

  let converged = error <= errorTolerance;

  let iteration = 0;
  for (; iteration < maxIterations && !converged; iteration++) {
    let previousError = error;

    let { perturbations, jacobianWeigthResidualError } = step(
      data,
      parameters,
      damping,
      gradientDifference,
      parameterizedFunction,
      centralDifference,
      weightSquare,
    );

    for (let k = 0; k < parameters.length; k++) {
      parameters[k] = Math.min(
        Math.max(minValues[k], parameters[k] - perturbations.get(k, 0)),
        maxValues[k],
      );
    }

    error = errorCalculation(
      data,
      parameters,
      parameterizedFunction,
      weightSquare,
    );

    if (isNaN(error)) break;

    let improvementMetric =
      (previousError - error) /
      perturbations
        .transpose()
        .mmul(perturbations.mulS(damping).add(jacobianWeigthResidualError))
        .get(0, 0);

    if (improvementMetric > improvementThreshold) {
      damping = Math.max(damping / dampingStepDown, 1e-7);
    } else {
      error = previousError;
      damping = Math.min(damping * dampingStepUp, 1e7);
    }

    if (checkTimeout()) {
      throw new Error(
        `The execution time is over to ${options.timeout} seconds`,
      );
    }

    converged = error <= errorTolerance;
  }

  return {
    parameterValues: parameters,
    parameterError: error,
    iterations: iteration,
  };
}

class FitingMethod {
    /**
     * 
     * @param {*} binScores ; array containing list of rd values
     */
    constructor(binScores) {
        this.binScores = binScores;
    }

    get_histogram(){
        /**
         * Get the 2D histogram of the array.
        */

        let max_bin = Math.max(...this.binScores);
        let rd_bin_size = Math.floor(max_bin / 1000);
        let rd_bins = this.range(0, Math.floor(max_bin / rd_bin_size) * rd_bin_size + rd_bin_size, rd_bin_size);
        const { counts, bins } = this.histogram(this.binScores, rd_bins);
        return { counts, bins };
    }

    range(start, end, step) {
        const result = [];
        for (let i = start; i < end; i += step) {
            result.push(i);
        }
        return result;
    }
    
    histogram(data, bins) {
        const counts = Array(bins.length).fill(0);
      
        data.forEach(value => {
            for (let i = 0; i < bins.length - 1; i++) {
                if (value >= bins[i] && value < bins[i + 1]) {
                    counts[i]++;
                    break;
                }
            }
        });
      
        return { counts, bins };
    }

    normal_distribution([a, x0, sigma]) {
        /**
         * Normal distribution.
         *
         * @param {number} a - Area.
         * @param {number} x0 - Mean value.
         * @param {number} sigma - Sigma.
         * @returns {number} - Value of distribution at x.
         */
        // console.log('Model Parameters', a, x0, sigma)
        return (x) => a * Math.exp(-Math.pow(x - x0, 2) / (2 * Math.pow(sigma, 2))) / (Math.sqrt(2 * Math.PI) * sigma);
    }

    get_initial_model_values(x, y){
        /**
         * calculate the initial values for the normal distribution
         * x: The x values of the data.
         * y: The y values of the data.
         * return [area, mean, sigma]: The initial values for the normal distribution.
         */
        
        const sumY = y.slice(1, -1).reduce((acc, val) => acc + val, 0);
        if (sumY === 0) {
            console.debug("Problem with fit: all data points have zero value. Return zeros instead fit parameters!");
            return null;
        }
        // calculate area
        const bin_width = x[1] - x[0];
        const area =  bin_width * sumY;
    
        // Calculate the mean
        const weightedSum = x.reduce((acc, val, idx) => acc + val * y[idx], 0);
        const mean = weightedSum / sumY;
    
        // Calculate the sigma
        const weightedSquareSum = x.reduce((acc, val, idx) => acc + y[idx] * Math.pow(val - mean, 2), 0);
        const sigma = Math.sqrt(weightedSquareSum / sumY);
        // console.log('Initial Model Parameters', area, mean, sigma)
        return [area, mean, sigma];
    }

    normal_fit(x, y) {
        /**
         * Fit a normal distribution to the histogram using levenberg-marquardt algorithm.
         * x: The x values of the data.
         * y: The y values of the data.
         * return model: The model of the normal distribution.
         */

        // Initial values were calculated for arithmetic mean and standard deviation. 
        let initialValues = this.get_initial_model_values(x, y);
        
        // paramters for the levenberg-marquardt algorithm
        const options = {
          initialValues: initialValues,
          maxIterations: 100,
        };

        // Fit the model
        const model = levenbergMarquardt({x, y}, this.normal_distribution, options);
    
        return model;
    }

}

class FetchGCInfo {
    /**
     * Creates an instance of gcCorrection.
     * @param {number} binSize - binSize
     * @param {string} refGenome - Reference genome name.
     */
    constructor(binSize, refGenome){
        this.binSize = binSize;
        this.refGenome = refGenome;

        // Determine the appropriate GC bin size
        this.gcBin = this.getGCbinSize();
    }

    /**
     * Determines the appropriate GC bin size.
     * @returns {number|boolean} The GC bin size or false if not found.
     */
    getGCbinSize(){
        for (let gcBin of [100000, 10000]){
            // Check if the binSize is a multiple of the current gcBin
            if (this.binSize % gcBin == 0) return gcBin;
        }
        // Return false if no appropriate GC bin size is found
        return false
    }

    /**
     * Fetches GC values for a predefined gc bin sizes (i.e., 10k, 100k) from a remote JSON file.
     * @returns {Object} The GC values.
     */
    async getBinGC(){
        const gcData = {};

        // If no appropriate GC bin size is found, return an empty object
        if (!this.gcBin) {
            return gcData;
        }
       
        // URL for the GC info JSON file
        const gcInfoURL = 'https://storage.googleapis.com/cnvpytor_data/gcInfoData/GCinfo.json';
        
        try {
            // Load the GC info JSON file
            const gcInfoJson = await igvxhr$1.loadJson(gcInfoURL, {timeout: 5000});

            // Find the reference genome data within the JSON file
            const gcRef = gcInfoJson.find(item => item.id === this.refGenome);
            if (!gcRef){
                console.warn("GC data not found for ", this.refGenome);
                return gcData;
            }
            
            // Find the matching bin size data
            const matchingInfo = gcRef.Bins.find(bin => bin.BinSize === this.gcBin);
            if (!matchingInfo) {
                console.warn("GC data not found for ", this.refGenome, " Bin : ", this.gcBin);
                return gcData;
            }

            // Construct the URL to fetch the GC data file
            const parentDirectory = gcInfoURL.split('/').slice(0, -1).join('/');
            let gcURL = `${parentDirectory}${matchingInfo.fileURL}`;
            
            // Fetch the GC data file
            const data = await igvxhr$1.load(gcURL, {});

            // Parse the GC data file and populate the gcData object
            data.split("\n").forEach((row) => {
                if (row.trim() !== "") {
                const [refName, start, gcContent, gcCount, atCount] = row.split("\t");
                if (!gcData[refName]) {
                    gcData[refName] = [];
                }
                gcData[refName].push({
                    start: +start,
                    gcContent: +gcContent,
                    gcCount: +gcCount,
                    atCount: +atCount,
                });
                }
            });

        } catch(e){
            console.error(e);
            console.warn("Errors loading GC correction data.");
        }
        
        return gcData;

    }

}


class baseCNVpytorVCF extends FetchGCInfo {
    /**
     * Class for gcCorrection 
     * 
     * @param {*} wigFeatures 
     * @param {*} binSize 
     * @param {*} refGenome 
     */
    constructor(wigFeatures, binSize, refGenome){
        super(binSize, refGenome);
        this.wigFeatures = wigFeatures;
        this.binSize = binSize;
        this.refGenome = refGenome;
        
    }
    async apply_gcCorrection(){
        // applying fitting method and defineing the variables
        if (!this.wigFeatures) {
            console.error("BinScore data is not available.");
            return null;
        }
        
        // Extract all binScore values into a single array
        const binScores = Object.values(this.wigFeatures).reduce(
            (binResult, bin) => { return binResult.concat(bin.filter(a => a.binScore > 0).map(a => a.binScore)) }, []
        );

        let data_fitter = new FitingMethod(binScores);

        const { counts: dist_p, bins } = data_fitter.get_histogram();
        let model_parameters = data_fitter.normal_fit(bins, dist_p);
        // return { globalMean: model_parameters.parameterValues[1], globalStd: model_parameters.parameterValues[2]}
        this.globalMean = model_parameters.parameterValues[1];
        this.globalStd = model_parameters.parameterValues[2];
        // console.log("globalmean", this.globalMean)

        
        this.gcData = await this.getBinGC();
        this.gcFlag =  Object.keys(this.gcData).length > 0 ? true : false; // Flag indicating whether GC data is available
        this.binScoreField = this.gcFlag ? "gcCorrectedBinScore": "binScore" ;

        this.getGcCorrectionSignal(this.globalMean);
        
    }

    /**
     * Applies GC correction to the bin scores.
     * @param {number} rdGlobalMean - The global mean read depth.
     */
    getGcCorrectionSignal(rdGlobalMean){
        let gcRDMean = this.getGcCorrection(rdGlobalMean);
        Object.keys(this.wigFeatures).forEach(chr => {
            this.wigFeatures[chr].forEach(bin => {
                    if (bin.binScore){
                        bin.gcCorrectedBinScore = Math.round(gcRDMean[bin.gc] * bin.binScore);
                    }else {
                        bin.gcCorrectedBinScore = null;
                    }
                }
            );
        });
    }

    /**
     * Computes the GC correction values based on the global mean read depth.
     * @param {number} rdGlobalMean - The global mean for read depth.
     * @returns {Object} An object containing the GC correction values indexed by GC percentage.
     */
    getGcCorrection(rdGlobalMean){
        
        const gcRDMean = {};
        if(this.gcFlag){
            let gcBin = this.getGCbinSize();
            const gcRD = {};
            
            let gcBinFactor = parseInt(this.binSize/gcBin);
            for (let chr in this.wigFeatures){
                for (let k=0; k< this.wigFeatures[chr].length; k++){
                    
                    // collect GC related values  for a bin 
                    let baseInfo = {'AT': 0, 'GC': 0};
                    for (let j = k * gcBinFactor; j < k * gcBinFactor + gcBinFactor; j++){
                        if (this.gcData[chr][j]){
                            baseInfo['GC'] += this.gcData[chr][j].gcCount;
                            baseInfo['AT'] += this.gcData[chr][j].atCount;
                        }
                    }

                    let gcValue = Math.round((baseInfo['GC'] * 100)/(baseInfo['GC'] + baseInfo['AT']));

                    this.wigFeatures[chr][k].gc = gcValue;
                    if (!gcRD[gcValue]) {
                        gcRD[gcValue] = [];
                    }
                    if (this.wigFeatures[chr][k].binScore){
                        gcRD[gcValue].push(this.wigFeatures[chr][k].binScore);
                    }
                }
            }
            
            // get average rd for bins that has same gc content
            Object.keys(gcRD).forEach(gc_value => {
                
               // apply fitting method to get the gc average
                let gcRDBins = gcRD[gc_value];
                let gcMean; 
                if(gcRDBins.length < 4){
                    gcMean = gcRDBins.reduce((sum, val) => sum + val, 0) / gcRDBins.length;
                }else {
                    let data_fitter = new FitingMethod(gcRD[gc_value]);

                    const { counts: dist_p, bins } = data_fitter.get_histogram();
                    let model_parameters = data_fitter.normal_fit(bins, dist_p);
                    gcMean = model_parameters.parameterValues[1];
                }
               gcRDMean[gc_value] = rdGlobalMean/gcMean;

            });
        
        }
        return gcRDMean
    }

    /**
     * Formats the data structure for output.
     * @param {string} feature_column - The column name of the feature to be scaled.
     * @param {number} [scaling_factor=1] - The factor by which to scale the feature values.
     * @returns {Array} The formatted data structure.
     */
    formatDataStructure(feature_column, scaling_factor = 1) {
        const results = [];
        for (const [chr, wig] of Object.entries(this.wigFeatures)) {

            for(let sample of wig){
                var new_sample = { ...sample };
                if (scaling_factor != 1) {
                    new_sample.value = sample[feature_column] / scaling_factor * 2;
                }
                results.push(new_sample);
            }
        }

        return results
    }

    formatDataStructure_BAF(feature_column, scaling_factor = -1) {
        const baf1 = [];
        const baf2 = [];
        for (const [chr, wig] of Object.entries(this.wigFeatures)) {

            wig.forEach(sample => {
                
                var baf1_value = { ...sample };
                var baf2_value = { ...sample };
                
                let value = sample[feature_column];
                if (value != 0.5){
                    baf2_value.value = scaling_factor * (1 - value);
                    baf2.push(baf2_value);
                }
                baf1_value.value = scaling_factor * value;
                baf1.push(baf1_value);
                    
            });
        }
        
        return [baf1, baf2]
    }
    
    
      
}

class CombinedCaller extends baseCNVpytorVCF {
    /**
     * Creates an instance of CombinedCaller.
     * 
     * @param {Array} wigFeatures - An array of arrays containing wig formatted data for each chromosome and bin.
     * @param {number} binSize - The size of the bins used in the wig data.
     * @param {string} refGenome - GC content data indexed by chromosome and bin.
     */
    constructor(wigFeatures, binSize, refGenome) {
        super(wigFeatures, binSize, refGenome);
    }

    async call_2d(omin=null, mcount=null, event_type="both", max_distance=0.1, baf_threshold=0, max_copy_number=10, min_cell_fraction=0.0){
        
        // let fit_obj = this.get_fit_v2()
        // this.globalMean = fit_obj.globalMean
        // this.globalStd = fit_obj.globalStd
        
        // this.getGcCorrectionSignal(this.globalMean)

        // applying gc correction
        await this.apply_gcCorrection();

        let binScoreField = this.gcFlag ? "gcCorrectedBinScore": "binScore" ;

        let overlap_min = omin==null ?  0.05 * this.binSize / 3e9: omin ;
        let min_count = mcount == null ? parseInt(this.binSize / 10000) : mcount ;

        let gstat_rd0 = [];
        let gstat_rd_all = [];
        let gstat_rd = [];
        let gstat_error = [];
        let gstat_lh = [];
        let gstat_n = [];
        
        for (const [chr, wig] of Object.entries(this.wigFeatures)) {
            let segments = [];
            let levels = [];
            let likelihoods = [];
            
            wig.forEach((bin, bin_idx) => {
                if (bin.hets_count > 4 ){
                    
                    if( bin.dp_count > min_count ){
                        if(bin[binScoreField]){
                            segments.push([bin_idx]);
                            levels.push(bin[binScoreField]);
                            likelihoods.push(bin.likelihood_score);
                            delete bin.likelihood_score;

                        }

                    }
                }
            });

            let diff_level = [];
            for(let i=1; i<levels.length; i++){
                diff_level.push(Math.abs(levels[i] - levels[i-1]));
            }
            let min_flank = [0];
            for(let i=1; i<diff_level.length; i++){
                min_flank.push(Math.min(diff_level[i-1], diff_level[i]));    
            }
            min_flank.push(0);

            let error  = levels.map((x, x_idx) => {return Math.sqrt(Math.sqrt(x) ** 2 + this.globalStd ** 2 + Math.pow(min_flank[x_idx]/2, 2));});
            
            let overlaps = [];
            
            for(let i=0; i< segments.length-1; i++){
                
                let lh_overlap = 0;
                try{
                    lh_overlap = likelihood_overlap(likelihoods[i], likelihoods[i+1]);
                }catch{
                }
                
                let rd_overlap = normal_overlap_approx(levels[i], error[i], levels[i+1], error[i+1]);
                overlaps.push(rd_overlap * lh_overlap);
                
            }
            
            while(overlaps.length >0) {
                overlaps = overlaps.filter(num => typeof num === "number");
                
                let max_overlap = arrayMax(overlaps);
                if(max_overlap < overlap_min){
                    // console.log("maxoverlap ",max_overlap,  "is smaller than overlap min")
                    break
                }
                let i = overlaps.indexOf(max_overlap);
                
                let merge_level = normal_merge(levels[i], error[i], levels[i + 1], error[i + 1]);
                
                let nlh;
                let nlh_sum;
                try{
                    nlh = likelihoods[i].map((l_value, l_idx) => { return l_value * likelihoods[i+1][l_idx]});
                
                    nlh_sum = nlh.reduce((a, c_value) => {return a + c_value});
                    
                }catch{
                }
                // nlh_sum = nlh.reduce((a, c_value) => {return a + c_value});

                levels[i] = merge_level.nl;
                error[i] = merge_level.ne;
                
                likelihoods[i] = nlh.map(function(item) { return item/nlh_sum } );
                
                segments[i].push(...segments[i+1]);

                levels.splice(i + 1, 1);
                error.splice(i + 1, 1);
                segments.splice(i + 1, 1);
                likelihoods.splice(i + 1, 1);
                overlaps.splice(i, 1);
                
                if(i < overlaps.length){
                    
                    let rd_overlap = normal_overlap_approx(levels[i], error[i], levels[i+1], error[i+1]);
                    let new_overlap = rd_overlap * likelihood_overlap(likelihoods[i], likelihoods[i + 1]);
                    
                    overlaps[i] = new_overlap;
                }
                if(i > 0){
                    let new_overlap = normal_overlap_approx(levels[i - 1], error[i - 1], levels[i], error[i]) 
                    * likelihood_overlap(likelihoods[i - 1], likelihoods[i]);
                    overlaps[i - 1] = new_overlap;
                }

            }
            let ons = -1;
            while(true){
                overlaps = [];
                for(let i=0; i< levels.length; i++){
                    for(let j=i; j<levels.length; j++){
                        if(segments[j][0] - segments[i].at(-1) < max_distance * (segments[i].length + segments[j].length)){
                            overlaps.push(normal_overlap_approx(levels[i], error[i], levels[j], error[j]) * likelihood_overlap(likelihoods[i], likelihoods[j]));
                        }
                    }
                }
                
                if(overlaps.length == 0){
                    break
                }
                let max_overlap = arrayMax(overlaps);
                if(max_overlap < overlap_min){
                    break
                }
                let i = 0; 
                let j = 1;
                while (i < segments.length - 1){
                    let overlap_value = normal_overlap_approx(levels[i], error[i], levels[j], error[j]) * likelihood_overlap(likelihoods[i], likelihoods[j]);

                    if((segments[j][0] - segments[i].at(-1)) < max_distance * (segments[i].length + segments[j].length) && overlap_value == max_overlap){
                        let merge_level = normal_merge(levels[i], error[i], levels[i + 1], error[i + 1]);

                        levels[i] = merge_level.nl;
                        error[i] = merge_level.ne;
                        let nlh = likelihoods[i].map((l_value, l_idx) => { return l_value * likelihoods[i+1][l_idx]});
                        let nlh_sum = nlh.reduce((a, c_value) => {return a + c_value});
                        likelihoods[i] = nlh.map(function(item) { return item/nlh_sum } );

                        
                        segments[i].push(...segments[i+1]);
                        segments[i] = segments[i].sort((a,b) => a-b);

                        levels.splice(j, 1);
                        error.splice(j, 1);
                        segments.splice(j, 1);
                        likelihoods.splice(j, 1);
                        
                        if(j >= segments.length){
                            i += 1;
                            j = i + 1;
                        }

                    }else {
                        j += 1;
                        if(j >= segments.length){
                            i += 1;
                            j = i + 1;
                        }
                    }
                }
                if(ons == segments.length){
                    break
                }
                ons = segments.length;
            }
            // console.log('final segments', segments)
            
            segments.forEach((seg_value, seg_idx) => {
                let baf_info = likelihood_baf_pval(likelihoods[seg_idx]);
                if(seg_value.length > 1){

                    seg_value.forEach((bin, bin_idx) =>{
                        gstat_rd_all.push(wig[bin]);
                        if(baf_info.mean <= baf_threshold){
                            gstat_rd0.push(wig[bin]);
                        }
                        wig[bin].segment_score = levels[seg_idx];
                    });
                    gstat_rd.push(levels[seg_idx]);
                    gstat_error.push(error[seg_idx]);
                    gstat_lh.push(likelihoods[seg_idx]);

                }

            });

            continue
        }
        
        // Third stage for call
        
        // let data = gstat_rd0.lengthn == 0 ?  gstat_rd_all: gstat_rd0 ;
        
        let points = parseInt(1000 * (1 - min_cell_fraction));
        if(points == 0){
            points = 1;
        }
        let x = g_utils.linspace(min_cell_fraction, 1, points);
        let master_lh = {};
        let germline_lh = {};
        for(let cn=10; cn > -1; cn--){
            for(let h1=0; h1 < (cn/2+1); h1++){
                let h2 = cn - h1;
                let mrd = x.map((v, idx) => {return 1-v +v*cn/2});
                let g_mrd = cn / 2;
                let g_mbaf;
                let mbaf;
                if(cn > 0){
                    g_mbaf = 0.5 - (h1 / (h1 + h2));
                    mbaf = x.map((v, idx) => {return 0.5 - (1 - v + v * h1) / (2 - 2 * v + (h1 + h2) * v)});
                   
                }else {
                    g_mbaf = 0;
                    mbaf =  x.map((v, idx) => {return 0*v});
                }
                
                for( let ei=0; ei < gstat_rd.length; ei++){
                        
                    let g_lh = normal(g_mrd * this.globalMean, 1, gstat_rd[ei], gstat_error[ei]) * likelihood_of_baf(gstat_lh[ei], 0.5 + g_mbaf);
                    if(ei in germline_lh){
                        germline_lh[ei].push([cn, h1, h2, g_lh, 1.0]);
                    }else {
                        germline_lh[ei] = [cn, h1, h2, g_lh, 1.0];
                    }
                    let slh = 0;
                    let max_lh = 0;
                    let max_x = 0;
                    mrd.forEach((mi, idx) => {
                        if(!isNaN(mbaf[idx])){
                            let tmpl = normal(mi * this.globalMean, 1, gstat_rd[ei], gstat_error[ei]) * likelihood_of_baf(gstat_lh[ei], 0.5 + mbaf[idx]);
                            slh += tmpl;
                            if(tmpl > max_lh){
                                max_lh = tmpl;
                                max_x = x[idx];
                            }
                        }
                    });
                    if(ei in master_lh){
                        master_lh[ei].push([cn, h1, h2, slh / x.length, max_x]);
                    }else {
                        master_lh[ei] = [cn, h1, h2, slh / x.length, max_x];
                    }
                }
                
                for( let ei=0; ei < gstat_rd.length; ei++){
                    if(event_type == "germline"){
                        master_lh[ei].sort((a, b) => a[3] - b[3]);
                    }
                    else {
                        master_lh[ei].sort((a, b) => a[3] - b[3]);
                        if(event_type == "both"){
                            
                            germline_lh[ei].sort((a, b) => a[3] - b[3]);
                            if(germline_lh[ei][0][3] > master_lh[ei][0][3]){
                                //let tmp_list = list(filter( lambda x: x[0] != germline_lh[ei][0][0] and x[1] != germline_lh[ei][0][1], master_lh[ei]))
                                let tmp_list = master_lh[ei].filter((x) => (x[0] != germline_lh[ei][0][0]) && (x[1] <= germline_lh[ei][0][1]));
                                // console.log('tmp_list', tmp_list)
                                // master_lh[ei] = [germline_lh[ei][0]] + tmp_list
                                master_lh[ei] = [germline_lh[ei][0]].push(...tmp_list);
                            }
                        }
                    }
                }
                for( let ei=0; ei < gstat_rd.length; ei++){
                    if(master_lh[ei][0][0] > 2);
                    if(master_lh[ei][0][0] < 2);
                    gstat_rd[ei] / this.globalMean;
                    t_dist.t_test_1_sample(this.globalMean, gstat_rd[ei], gstat_error[ei], gstat_n[ei]);
                    // console.log(etype)

                }
                

                // break
            }
            
        }
        
        var rawbinScore = this.formatDataStructure(this.wigFeatures, 'binScore', this.globalMean);

        let gcCorrectedBinScore = [];
        if (this.gcFlag) {
            gcCorrectedBinScore = this.formatDataStructure(this.wigFeatures, 'gcCorrectedBinScore', this.globalMean);
        }
        var callScore = this.formatDataStructure(this.wigFeatures, 'segment_score', this.globalMean);
        
        return {binScore: rawbinScore, gcCorrectedBinScore: gcCorrectedBinScore, segmentScore: callScore}
        
    }
    
    formatDataStructure(wigFeatures, feature_column, scaling_factor = 1) {
        const results = [];
        for (const [chr, wig] of Object.entries(wigFeatures)) {

            wig.forEach(sample => {
                var new_sample = { ...sample };
                if (scaling_factor != 1) {
                    new_sample.value = sample[feature_column] / scaling_factor * 2;
                }
                results.push(new_sample);
            });
        }

        return results
    }
    /*
    formatDataStructure_BAF(feature_column, scaling_factor = -1) {
        const baf1 = []
        const baf2 = []
        for (const [chr, wig] of Object.entries(this.wigFeatures)) {

            wig.forEach(sample => {
                
                var baf1_value = { ...sample }
                var baf2_value = { ...sample }
                
                let value = sample[feature_column]
                if (value != 0.5){
                    baf2_value.value = scaling_factor * (1 - value)
                    baf2.push(baf2_value)
                }
                baf1_value.value = scaling_factor * value
                baf1.push(baf1_value)
                    
            })
        }
        

        return [baf1, baf2]
    }*/
}

function arrayMax(arr) {
    return arr.reduce(function (p, v) {
      return ( p > v ? p : v );
    });
  }

/**
 * Normal distribution.
 * 
 * @param {float} x - Variable.
 * @param {float} a - area
 * @param {float} x0 -  Mean value
 * @param {float} sigma - Sigma
 * @returns {float} - Value of distribution in x.
 */
function normal(x, a, x0, sigma){
  
  return a * Math.exp(-1* (x - x0) ** 2 / (2 * sigma ** 2)) / Math.sqrt(2 * Math.PI) / sigma

}

/**
 *  Calculates two normal distributions overlap area.
 * 
 * @param {float} m1 - Mean value of the first distribution
 * @param {float} s1 - Sigma of the first distribution
 * @param {float} m2 - Mean value for second distribution
 * @param {float} s2 - Sigma of the second distribution
 * 
 * @returns {float} area - Area of overlap
 */
function normal_overlap_approx(m1, s1, m2, s2){
   
    return Math.exp(-1* (m1-m2)**2/ (s1**2+s2**2))
}


/**
 * Returns overlap area of two likelihood functions.
 * 
 * @param {*} lk1 - First likelihood function.
 * @param {*} lk2 - Second likelihood function.
 * 
 * @returns {float}  - Overlap area.
 */
function likelihood_overlap(likelihood_1, likelihood_2){
    // console.log(likelihood_1, likelihood_2)
    let sum;
    try{
        sum = likelihood_1.reduce((accumulator, currentValue, currentIndex) => {return accumulator + Math.min(currentValue, likelihood_2[currentIndex])});
    }catch{
        return 0
    }

    return sum
}

/**
 * Calculates normal distribution that is product of two given normal distributions.
 * 
 * @param {float} m1 - Mean value of the first distribution
 * @param {float} s1 - Sigma of the first distribution
 * @param {float} m2 - Mean value for second distribution
 * @param {float} s2 - Sigma of the second distribution
 * @returns {Object} An object representing the first distribution
 * @property {float} nl - Mean value of the first distribution
 * @property {float} ne - Sigma of the first distribution
 */
function normal_merge(m1, s1, m2, s2){
   
    if((s1 == 0) && (s2 == 0)){
        return {nl: 0.5 * (m1 + m2), ne: 0}
    }
    else {
        return {nl: (m1 * s2 * s2 + m2 * s1 * s1) / (s1 * s1 + s2 * s2), ne: Math.sqrt(s1 * s1 * s2 * s2 / (s1 * s1 + s2 * s2))}
    }
}

/**
 * Calculates likelihood for given baf
 * @param {*} likelihood 
 * @param {*} baf 
 * @returns {float}  likelihood value
 */
function likelihood_of_baf(likelihood, baf){
   
    let bin = parseInt(baf * (likelihood.length - 1));
    let fr = baf * (likelihood.length - 1) - bin;
    if(bin < likelihood.length - 1){
        return likelihood[bin] * (1 - fr) + likelihood[bin + 1] * fr
    }
    else {
        return likelihood[bin]
    }
}

/**
 * 
 * Calculates baf level and p-value for given likelihood function.
 * 
 * @param {*} likelihood 
 * @returns {Object} An object representing BAF
 * @property {float} mean  BAF level (difference from 1/2)
 * @property {float} p  p-value for event different than 1/2
 */
function likelihood_baf_pval(likelihood) {
    const res = likelihood.length;
    const max_lh = Math.max(...likelihood);
    let ix = likelihood.indexOf(max_lh);
    if (ix > Math.floor(res / 2)) {
      ix = res - 1 - ix;
    }
    const b = (res / 2 - ix) / (res + 1);
  
    const ix1 = Math.floor((res / 2 + ix) / 2);
    const ix2 = res - 1 - ix1;
    let p = likelihood.slice(ix1, ix2 + 1).reduce((acc, val) => acc + val, 0) / likelihood.reduce((acc, val) => acc + val, 0);
    if (ix === Math.floor(res / 2)) {
      p = 1.0;
    }
    return {mean:b, p:p};
  }

  var combined_caller = {CombinedCaller};

// TODO -- remove this hardcoded value
const genome_size = 2871000000;


class MeanShiftCaller extends baseCNVpytorVCF{
    /**
     * Creates an instance of CombinedCaller.
     * 
     * @param {Array} wigFeatures - An array of arrays containing wig formatted data for each chromosome and bin.
     * @param {number} binSize - The size of the bins used in the wig data.
     * @param {string} refGenome - reference genome name
     */
    constructor(wigFeatures, binSize, refGenome) {
        super(wigFeatures, binSize, refGenome);
        this.binBands = [2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, 64, 80, 96, 112, 128];
    }

    async callMeanshift(repeats = 3){
        // applying gc correction
        await this.apply_gcCorrection();

        let partitionLevels = this.partition();
        // console.log("Partition: ", partitionLevels)

        // console.log("WigFeatures: ", this.wigFeatures)

        // let cnvs = this.cnvCalling(partitionLevels)
        // console.log("cnvs: ", cnvs)
        
        Object.entries(this.wigFeatures).forEach(([chr, chrRD]) => {
            chrRD.forEach((bin, index) => {
                if (partitionLevels[chr]){ 
                    bin.partitionLevel = parseInt(partitionLevels[chr][index]);
                }
                // bin.cnvCall = parseInt(caller_array[0][chr][index])
            });
        });

        var rawbinScore = this.formatDataStructure('binScore', this.globalMean);
        var gcCorrectedBinScore = this.formatDataStructure('gcCorrectedBinScore', this.globalMean);
        var partitionBinScore = this.formatDataStructure('partitionLevel', this.globalMean);
        // var cnvLevels = this.formatDataStructure('cnvCall', this.globalMean)

        const fetchedData = {binScore: rawbinScore, gcCorrectedBinScore: gcCorrectedBinScore, segmentsCNV: partitionBinScore};
        return fetchedData

    }
    
    getRDSignalBandWidth(data_array) {
        const threshold = this.globalMean / 4;

        // The values are reversed and squared as they will be used to calculate a gradient function.
        // This optimization is done to speed up the calculations.

        const constantValue = 4 / this.globalStd ** 2;
        return data_array.map(value => {
            return value > threshold ? this.globalMean / (this.globalStd ** 2 * value) : constantValue;
        });
    }

    partition(repeats = 3){
        
        // sort the dictionary based on chromosome names;
        let sortedDictionary = {};
        Object.keys(this.wigFeatures).sort((a, b) => a.localeCompare(b, undefined, {numeric: true})).forEach(key => {
            sortedDictionary[key] = this.wigFeatures[key];
        });

        let binScoreField = this.gcFlag ? "gcCorrectedBinScore": "binScore" ;
        
        var chrLevels = {};
        // Object.entries(this.wigFeatures).forEach(([chr, chr_rd]) => {
        for (const [chr, chrWig] of Object.entries(sortedDictionary)) {

            // console.log("chr: ", chr, chrWig.length, chrWig)

            // boolean array; Initiate with all false values
            var masked = new Array(chrWig.length).fill(false);

            // set the level; score from either RAW or GC corrected bin score 
            var levels = chrWig.map((item, index) => !masked[index] ? item[binScoreField] : undefined);
            // console.log("Levels: ", chr, levels)
            // var levels = chrWig.map((item, index) => !masked[index] ? item : undefined);
            
            
            this.binBands.forEach((bin_band, bin_band_index) => {
                
                // console.log("BinBand: ", bin_band)

                // not masked levels at current bin
                // get boolean values
                var not_masked = masked.map((value, index) => { return !value; });
                // console.log(not_masked)

                // not masked level at current bin
                // Object.entries(chrWig).forEach(([k, v]) => { nm_levels.push(v.binScore) })
                // var nm_levels = Object.keys(chrWig).map(k => chrWig[k].binScore);
                // var nm_levels = levels

                // not mask level
                var nm_levels = levels.filter((_, index) => !masked[index]);
                // console.log("nm_levels : ( Bin Band: " , bin_band, " )", nm_levels)
                // const nm_levels = Object.keys(chrWig).map(k => chrWig[k].binScore).filter(score => !isNaN(score));

                // console.log(nm_levels)

                // set the mask border
                var mask_borders = [0];
                var count = 0;
                
                // the masked array was declared previously
                masked.forEach(item => {
                    if (item) {
                        if (count > 0) {
                            mask_borders.push(mask_borders[mask_borders.length - 1] + count - 1);
                            count = 0;
                        }
                    } else if (!item) { count++; }
                });
                // console.log(mask_borders)
                // console.log("Mask Borders: ", mask_borders)
                mask_borders.shift();

                // repeating steps
                for (let step = 0; step < repeats; step++) {
                    var isig = this.getRDSignalBandWidth(nm_levels);
                    
                    // get the direction of meanshift vector for a bin
                    // it compares near by bins to get the direction of the vector; bin band defines the range of comparison

                    var grad = new Array(nm_levels.length).fill(0);

                    for (let i = 0; i < nm_levels.length; i++) {
                        const start_bin = Math.max(0, i - 3 * bin_band);
                        const end_bin = Math.min(nm_levels.length - 1, i + 3 * bin_band + 1);
                        // let bin_length = end_bin - start_bin 
                        
                        for (let j = start_bin ; j <= end_bin; j++) {
                        // for (let j = i - 3 * bin_band; j <= i + 3 * bin_band + 1; j++) {
                            // if (j < 0 || j >= nm_levels.length) continue;
                            // if (Math.abs(i - j) >= nm_levels.length) continue;

                            var g_value = (j - i) * Math.exp((-0.5 * (j - i) ** 2) / bin_band ** 2) *  Math.exp(-0.5 * (nm_levels[i] - nm_levels[j]) ** 2 * isig[i]);

                            grad[i] += g_value;
                        }
                    }
                    // console.log("grad: ", grad)
                    // get the border; if there is a change of gradient, it is a border
                    var border = new Array();
                    for (var i = 0; i < grad.length - 1; i++) {
                        if ((grad[i] < 0) & (grad[i + 1] >= 0)) border.push(i);
                    }

                    border.push(grad.length - 1);
                    border = border.concat(mask_borders).sort((a, b) => a - b);
                    border = Array.from(new Set(border));

                    var pb = 0;
                    for (var i = 0; i < border.length; i++) {
                        var range_array = nm_levels.slice(pb, border[i] + 1);
                        var range_mean = range_array.reduce((acc, n) => acc + n) / range_array.length;

                        nm_levels.fill(range_mean, pb, border[i] + 1);
                        pb = border[i] + 1;
                    }
                }

                for (var i = 0, j = 0; i < levels.length; i++) {
                    if (not_masked[i]) {
                        levels[i] = nm_levels[j];
                        j++;
                    }
                }

                //get the border
                var border = new Array();
                for (var i = 0; i < levels.length - 1; i++) {
                    //if(i == levels.length -1) continue;
                    var diff = Math.abs(levels[i + 1] - levels[i]);

                    if (diff > 0.01) border.push(i + 1);
                }

                border.unshift(0);
                border.push(levels.length);

                // reset the mask
                masked = new Array(this.wigFeatures.length).fill(false);

                // check the borders
                for (var i = 1; i < border.length; i++) {
                    var seg = [border[i - 1], border[i]];
                    var seg_left = [border[i - 1], border[i - 1]];
                    if (i > 1) { seg_left[0] = border[i - 2]; } else continue;

                    var seg_right = [border[i], border[i]];
                    if (i < border.length - 1) { seg_right[1] = border[i + 1]; } else continue;

                    var n = seg[1] - seg[0];
                    var n_left = seg_left[1] - seg_left[0];
                    var n_right = seg_right[1] - seg_right[0];
                    if (n <= 1) continue;
                    var seg_array = new DataStat(levels.slice(seg[0], seg[1]));

                    if (n_right <= 15 || n_left <= 15 || n <= 15) {
                        var ns = 1.8 * Math.sqrt(levels[seg_left[0]] / this.globalMean) * this.globalStd;
                        if (Math.abs(levels[seg_left[0]] - levels[seg[0]]) < ns) { continue }

                        ns = 1.8 * Math.sqrt(levels[seg_right[0]] / this.globalMean) * this.globalStd;
                        if (Math.abs(levels[seg_right[0]] - levels[seg[0]]) < ns) { continue }
                    } else {
                        var seg_left_array = levels.slice(seg_left[0], seg_left[1]);
                        var seg_left_1 = new DataStat(seg_left_array);

                        var seg_right_array = levels.slice(seg_right[0], seg_right[1]);
                        var seg_right_1 = new DataStat(seg_right_array);

                        var ttest_2sample_1 = t_test_2_samples(seg_array.mean, seg_array.std, seg_array.data.length,
                            seg_left_1.mean, seg_left_1.std, seg_left_1.data.length);
                        if (ttest_2sample_1 > (0.01 / genome_size) * this.binSize * (n + n_left)) { continue }

                        var ttest_2sample_2 = t_test_2_samples(seg_array.mean, seg_array.std, seg_array.data.length,
                            seg_right_1.mean, seg_right_1.std, seg_right_1.data.length);
                        if (ttest_2sample_2 > (0.01 / genome_size) * this.binSize * (n + n_right)) { continue }
                    }

                    var ttest_1sample_1 = t_test_1_sample(this.globalMean, seg_array.mean, seg_array.std, seg_array.data.length);
                    if (ttest_1sample_1 > 0.05) { continue }
                    let segments_rd = nm_levels.slice(seg[0], seg[1]);
                    // console.log("segments_rd: ", segments_rd)
                    var raw_seg_data = new DataStat(segments_rd);

                    masked.fill(true, seg[0], seg[1]);
                    levels.fill(raw_seg_data.mean, seg[0], seg[1]);
                }
        
            });
            // console.log("after applying partition: ", levels)
            chrLevels[chr] = levels;
            // break
        }
        
        return chrLevels
    }

    cnvCalling(levels) {

        // console.log("levels: ", levels)
        var delta = 0.25 * this.globalMean;
        var min = this.globalMean - delta, max = this.globalMean + delta;
        var normal_genome_size = 2971000000;

        // var levels = this.meanShiftCaller(bin_size)
        // var levels = this.MeanShiftCallerV2(bin_size)

        var merged_level = {};
        var cnv_levels = [];
        
        Object.entries(levels).forEach(([chr, chr_levels]) => {
            
            var done = false;
            while (!done) {
                done = true;

                //
                // get the borders
                //
                var borders = new Array(1).fill(0);
                for (let i = 0; i < chr_levels.length - 1; i++) {
                    var diff = Math.abs(chr_levels[i + 1] - chr_levels[i]);
                    if (diff > 0.01) borders.push(i + 1);
                }
                borders.push(chr_levels.length);

                for (let ix = 0; ix < borders.length - 2; ix++) {
                    var v1 = Math.abs(chr_levels[borders[ix]] - chr_levels[borders[ix + 1]]);
                    // console.log(ix, v1);
                    if (v1 < delta) {
                        var v2 = v1 + 1, v3 = v1 + 1;

                        if (ix > 0) { v2 = Math.abs(chr_levels[borders[ix]] - chr_levels[borders[ix - 1]]); }
                        if (ix < borders.length - 3) { v3 = Math.abs(levels[borders[ix + 1]] - chr_levels[borders[ix + 2]]); }

                        if (v1 < v2 && v1 < v3) {
                            done = false;

                            var tmp_array = new DataStat(chr_levels.slice(borders[ix], borders[ix + 2]));
                            chr_levels.fill(tmp_array.mean, borders[ix], borders[ix + 2]);
                            borders.splice(ix + 1, ix + 1);
                        }
                    }
                }
            }
            // console.log('updated levels', chr_levels)

            // var chr_rd = this.rd[chr]
            var chr_rd = [];
            Object.entries(this.wigFeatures[chr]).forEach(([bin, binDict]) => { chr_rd.push(binDict.binScore); });
            // console.log('cnv_calling', chr_rd)

            //
            // Calling Segments
            //

            //var flags = [""] * levels.length;
            var flags = new Array(chr_levels.length).fill("");

            // console.log('default levels', chr, chr_levels)
            var b = 0;
            var pval = (0.05 * this.binSize) / normal_genome_size;
            while (b < chr_levels.length) {
                var b0 = b, border_start = b;
                while ((b < chr_levels.length) & (chr_levels[b] < min)) {
                    b += 1;
                }
                var border_end = b;

                if (border_end > border_start + 1) {
                    // console.log('border min', border_start, border_end)
                    var adj = adjustToEvalue(this.globalMean, this.std, chr_rd, border_start, border_end, pval);
                    // console.log(adj)
                    if (adj) {
                        var border_start, border_end = adj;
                        flags.fill("D", border_start, border_end);
                    }
                }
                border_start = b;
                while ((b < chr_levels.length) & (chr_levels[b] > max)) { b += 1; }
                border_end = b;

                if (border_end > border_start + 1) {
                    adj = adjustToEvalue(this.globalMean, this.std, chr_rd, border_start, border_end, pval);
                    // console.log(adj)
                    if (adj) {
                        (border_end = adj);
                        // flags[bs:be] = ["A"] * (be - bs)
                        flags.fill("A", border_start, border_end);
                    }
                }
                if (b == b0) b += 1;
            }

            //console.log(chr, 'segments', segments)
            //
            //  Calling additional deletions
            //
            b = 0;
            while (b < chr_levels.length) {
                while ((b < chr_levels.length) & (flags[b] != "")) b += 1;
                border_start = b;
                while ((b < chr_levels.length) & (chr_levels[b] < min)) b += 1;
                border_end = b;
                if (border_end > border_start + 1) {
                    if (gaussianEValue(this.globalMean, this.std, chr_rd, border_start, border_end) < 0.05 / normal_genome_size) {
                        flags.fill(["d"] * (border_end - border_start), border_start, border_end);
                    }
                    b -= 1;
                }
                b += 1;
            }

            b = 0;
            var cf;
            if (b < chr_levels.length) cf = flags[b];

            border_start = 0;

            //var merge = [...this.rd]
            var merge = [...chr_rd];
            // console.log('initial merge', merge)
            while (b < chr_levels.length) {
                while (flags[b] == cf) {
                    b += 1;
                    if (b >= flags.length) break;
                }
                if (b > border_start) {
                    var merge_arr = new DataStat(merge.slice(border_start, b));
                    merge.fill(merge_arr.mean, border_start, b);
                }
                if (b < chr_levels.length) cf = flags[b];
                border_start = b;
            }

            merged_level[chr] = merge;


            //
            // calculate calls
            // 
            b = 0;
            while (b < chr_levels.length) {
                cf = flags[b];
                if (cf == "") {
                    b += 1;
                    continue
                }
                border_start = b;
                while (b < chr_levels.length & cf == flags[b]) { b += 1; }
                var border_arr = new DataStat(merge.slice(border_start, b));
                let cnv = border_arr.mean / this.globalMean;
                let event_type;
                if (cf == "D") {
                    event_type = "deletion";
                } else {
                    event_type = "duplication";
                }
                let cnv_dict = {
                    chr: chr,
                    start: this.binSize * border_start + 1,
                    end: this.binSize * b,
                    size: this.binSize * (b - border_start + 1),
                    value: cnv * 2,
                    event_type: event_type
                };
                cnv_levels.push(cnv_dict);

            }
        });

        return [merged_level, cnv_levels]
    }


}

function erf(x) {
    var m = 1.0, s = 1.0, sum = x * 1.0;
    for (var i = 1; i < 50; i++) {
        m *= i;
        s *= -1;
        sum += (s * Math.pow(x, 2.0 * i + 1.0)) / (m * (2.0 * i + 1.0));
    }
    return (2 * sum) / Math.sqrt(3.14159265358979)
}

function getEValue(mean, sigma, rd, start, end) {
    var arr = new DataStat(rd.slice(start, end));
    if (arr.std == 0) {
        if (sigma > 0) { arr.std = (sigma * arr.mean) / mean; }
        else { arr.std = 1; }
    }
    var p_val = t_test_1_sample(mean, arr.mean, arr.std, end - start) / (end - start);
    return p_val
}

function gaussianEValue(mean, sigma, rd, start, end) {
    var arr = new DataStat(rd.slice(start, end));

    if (arr.mean < mean) {
        var x = (arr.max - arr.mean) / (sigma * Math.sqrt(2));
        return Math.pow(0.5 * (1 + erf(x)), end - start)
    }
    var x = (arr.min - arr.mean) / (sigma * Math.sqrt(2));
    return Math.pow(0.5 * (1 - erf(x)), end - start)
}

function adjustToEvalue(mean, sigma, rd, start, end, pval, max_steps = 1000) {
    var val = getEValue(mean, sigma, rd, start, end);
    var step = 0, done = false;
    while ((val > pval) & !done & (step < max_steps)) {
        done = true;
        step += 1;
        var [v1, v2, v3, v4] = [1e10, 1e10, 1e10, 1e10];
        if (start > 0) v1 = getEValue(mean, sigma, rd, start - 1, end);
        if (end - start > 2) {
            var v2 = getEValue(mean, sigma, rd, start + 1, end);
            var v3 = getEValue(mean, sigma, rd, start, end - 1);
        }
        if (end < rd.length) { var v4 = getEValue(mean, sigma, rd, start, end + 1); }
        if (Math.min[(v4)] < val) {
            done = false;
            if (v1 == Math.min[(v4)]) {
                start -= 1;
                val = v1;
            }
            elif(v2 == Math.min[(v4)]); {
                start += 1;
                val = v2;
            }
            elif(v3 == Math.min[(v4)]); {
                end -= 1;
                val = v3;
            }
            elif(v4 == Math.min[(v4)]); {
                end += 1;
                val = v4;
            }
        }
    }
    if (val <= pval) { return end }
    return 0;
}

class DataStat {
    constructor(data_array) {
        this.data = data_array;
        this.mean = data_array.reduce((acc, n) => acc + n) / data_array.length;
        this.std = Math.sqrt(data_array.reduce((acc, n) => (n - this.mean) ** 2) / data_array.length);
    }
}

function t_test_1_sample(mean, m, s, n) {
    if (s == 0) s = 1;
    var t = ((mean - m) / s) * Math.sqrt(n);
    var p = 1.0 - t_dist.TdistributionCDF(Math.abs(t), (n - 1));
    return p
}

function t_test_2_samples(m1, s1, n1, m2, s2, n2) {
    if (s1 == 0) s1 = 1;
    if (s2 == 0) s2 = 1;
    var t = (m1 - m2) / Math.sqrt(s1 ** 2 / n1 + s2 ** 2 / n2);
    var df = ((s1 ** 2 / n1 + s2 ** 2 / n2) ** 2 * (n1 - 1) * (n2 - 1)) /
        ((s1 ** 4 * (n2 - 1)) / n1 ** 2 + (s2 ** 4 * (n1 - 1)) / n2 ** 2);

    var p = 1.0 - t_dist.TdistributionCDF(Math.abs(t), parseInt(df + 0.5));

    return p
}

class Partition {

    constructor(rd, mean, std) {
        this.rd = rd;
        this.mean = mean;
        this.std = std;
        this.bin_bands = [2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, 64, 80, 96, 112, 128];
    }

    get_rd_signal_bandwidth(data_array) {
        var new_array = [];

        data_array.forEach((value, index) => {
            var tmp_value = 0;
            if (value > this.mean / 4) {
                tmp_value = this.mean / (this.std ** 2 * value);
            } else {
                tmp_value = 4 / this.std ** 2;
            }
            new_array.push(tmp_value);
        });
        return new_array
    }

    meanShiftCaller(bins_size, repeats = 3) {

        var ChrLevels = {};
        
        Object.entries(this.rd).forEach(([chr, chr_rd]) => {
            var masked = new Array(chr_rd.length).fill(false);

            // set the level
            var levels = new Array(chr_rd.length);

            for (let b = 0; b < chr_rd.length; b++) {
                if (!masked[b]) { levels[b] = chr_rd[b]; }
            }

            this.bin_bands.forEach((bin_band, bin_band_index) => {
                // not masked levels at current bin
                // get boolean values
                var not_masked = masked.map((value, index) => { return !value; });

                // not masked level at current bin
                var nm_levels = [];

                Object.entries(chr_rd).forEach(([k, v]) => { nm_levels.push(v.binScore); });
                
                // set the mask border
                var mask_borders = [0];
                var count = 0;

                for (let i = 0; i < masked.length; i++) {
                    if (masked[i]) {
                        if (count > 0) {
                            mask_borders.push(mask_borders[mask_borders.length - 1] + count - 1);
                            count = 0;
                        }
                    } else { count += 1; }
                }

                mask_borders.shift();

                // repeating steps
                for (let step = 0; step < repeats; step++) {
                    var isig = this.get_rd_signal_bandwidth(nm_levels);
                    var grad = new Array(nm_levels.length).fill(0);

                    for (let i = 0; i < nm_levels.length; i++) {
                        for (let j = i - 3 * bin_band; j <= i + 3 * bin_band + 1; j++) {
                            if (j < 0 || j >= nm_levels.length) continue;
                            if (Math.abs(i - j) >= nm_levels.length) continue;

                            var g_value = (j - i) * Math.exp((-0.5 * (j - i) ** 2) / bin_band ** 2) *
                                Math.exp(-0.5 * (nm_levels[i] - nm_levels[j]) ** 2 * isig[i]);

                            grad[i] += g_value;
                        }
                    }
                    
                    // get the border
                    var border = new Array();
                    for (var i = 0; i < grad.length - 1; i++) {
                        if ((grad[i] < 0) & (grad[i + 1] >= 0)) border.push(i);
                    }

                    border.push(grad.length - 1);
                    border = border.concat(mask_borders).sort((a, b) => a - b);
                    border = Array.from(new Set(border));

                    var pb = 0;
                    for (var i = 0; i < border.length; i++) {
                        var range_array = nm_levels.slice(pb, border[i] + 1);
                        var range_mean = range_array.reduce((acc, n) => acc + n) / range_array.length;

                        nm_levels.fill(range_mean, pb, border[i] + 1);
                        pb = border[i] + 1;
                    }
                }

                for (var i = 0, j = 0; i < levels.length; i++) {
                    if (not_masked[i]) {
                        levels[i] = nm_levels[j];
                        j++;
                    }
                }

                //get the border
                var border = new Array();
                for (var i = 0; i < levels.length - 1; i++) {
                    //if(i == levels.length -1) continue;
                    var diff = Math.abs(levels[i + 1] - levels[i]);

                    if (diff > 0.01) border.push(i + 1);
                }

                border.unshift(0);
                border.push(levels.length);

                // reset the mask
                masked = new Array(this.rd.length).fill(false);

                // check the borders
                for (var i = 1; i < border.length; i++) {
                    var seg = [border[i - 1], border[i]];
                    var seg_left = [border[i - 1], border[i - 1]];
                    if (i > 1) { seg_left[0] = border[i - 2]; } else continue;

                    var seg_right = [border[i], border[i]];
                    if (i < border.length - 1) { seg_right[1] = border[i + 1]; } else continue;

                    var n = seg[1] - seg[0];
                    var n_left = seg_left[1] - seg_left[0];
                    var n_right = seg_right[1] - seg_right[0];
                    if (n <= 1) continue;
                    var seg_array = new DataStat(levels.slice(seg[0], seg[1]));

                    if (n_right <= 15 || n_left <= 15 || n <= 15) {
                        var ns = 1.8 * Math.sqrt(levels[seg_left[0]] / this.mean) * this.std;
                        if (Math.abs(levels[seg_left[0]] - levels[seg[0]]) < ns) { continue }

                        ns = 1.8 * Math.sqrt(levels[seg_right[0]] / this.mean) * this.std;
                        if (Math.abs(levels[seg_right[0]] - levels[seg[0]]) < ns) { continue }
                    } else {
                        var seg_left_array = levels.slice(seg_left[0], seg_left[1]);
                        var seg_left_1 = new DataStat(seg_left_array);

                        var seg_right_array = levels.slice(seg_right[0], seg_right[1]);
                        var seg_right_1 = new DataStat(seg_right_array);

                        var ttest_2sample_1 = t_test_2_samples(seg_array.mean, seg_array.std, seg_array.data.length,
                            seg_left_1.mean, seg_left_1.std, seg_left_1.data.length);
                        if (ttest_2sample_1 > (0.01 / genome_size) * bin_size * (n + n_left)) { continue }

                        var ttest_2sample_2 = t_test_2_samples(seg_array.mean, seg_array.std, seg_array.data.length,
                            seg_right_1.mean, seg_right_1.std, seg_right_1.data.length);
                        if (ttest_2sample_2 > (0.01 / genome_size) * bin_size * (n + n_right)) { continue }
                    }

                    var ttest_1sample_1 = t_test_1_sample(this.mean, seg_array.mean, seg_array.std, seg_array.data.length);
                    if (ttest_1sample_1 > 0.05) { continue }
                    let segments_rd = nm_levels.slice(seg[0], seg[1]);
                    var raw_seg_data = new DataStat(segments_rd);

                    masked.fill(true, seg[0], seg[1]);
                    levels.fill(raw_seg_data.mean, seg[0], seg[1]);
                }
            });

            ChrLevels[chr] = levels;

        });
        return ChrLevels
    }
    call_mean_shift(repeats = 3) {
        const bin_size = 1000;
        // const genome_size = bin_size * this.rd.length;
        var masked = new Array(this.rd.length).fill(false);

        // set the level
        var levels = new Array(this.rd.length);
        for (var b = 0; b < this.rd.length; b++) {
            if (!masked[b]) {
                levels[b] = this.rd[b];
            }
        }
        this.bin_bands.forEach((bin_band, bin_band_index) => {
            // not masked levels at current bin
            // get boolean values
            var not_masked = masked.map((value, index) => {
                return !value;
            });

            // not masked level at current bin
            // var nm_levels = not_masked.map((value, index) => {if(value) return this.rd[index]});
            var nm_levels = [];
            not_masked.forEach((value, index) => {
                if (value) nm_levels.push(this.rd[index]);
            });

            // console.log(bin_band, nm_levels);

            // set the mask border
            var mask_borders = [0];
            var count = 0;

            for (var i = 0; i < masked.length; i++) {
                if (masked[i]) {
                    if (count > 0) {
                        mask_borders.push(
                            mask_borders[mask_borders.length - 1] + count - 1,
                        );
                        count = 0;
                    }
                } else {
                    count += 1;
                }
            }

            mask_borders.shift();
            // repeating steps
            for (var step = 0; step < repeats; step++) {
                var isig = this.get_rd_signal_bandwidth(nm_levels);
                // console.log(isig);
                var grad = new Array(nm_levels.length).fill(0);

                for (var i = 0; i < nm_levels.length; i++) {
                    for (var j = i - 3 * bin_band; j <= i + 3 * bin_band + 1; j++) {
                        if (j < 0 || j >= nm_levels.length) continue;
                        if (Math.abs(i - j) >= nm_levels.length) continue;

                        var g_value =
                            (j - i) *
                            Math.exp((-0.5 * (j - i) ** 2) / bin_band ** 2) *
                            Math.exp(-0.5 * (nm_levels[i] - nm_levels[j]) ** 2 * isig[i]);
                        // console.log(g_value);
                        grad[i] += g_value;
                    }
                }
                // console.log(grad);

                // get the border
                var border = new Array();
                for (var i = 0; i < grad.length - 1; i++) {
                    if ((grad[i] < 0) & (grad[i + 1] >= 0)) border.push(i);
                }

                border.push(grad.length - 1);
                border = border.concat(mask_borders).sort((a, b) => a - b);
                border = Array.from(new Set(border));

                var pb = 0;
                for (var i = 0; i < border.length; i++) {
                    var range_array = nm_levels.slice(pb, border[i] + 1);
                    var range_mean =
                        range_array.reduce((acc, n) => acc + n) / range_array.length;

                    nm_levels.fill(range_mean, pb, border[i] + 1);
                    pb = border[i] + 1;
                }
            }

            for (var i = 0, j = 0; i < levels.length; i++) {
                if (not_masked[i]) {
                    levels[i] = nm_levels[j];
                    j++;
                }
            }

            //get the border
            var border = new Array();
            for (var i = 0; i < levels.length - 1; i++) {
                //if(i == levels.length -1) continue;
                var diff = Math.abs(levels[i + 1] - levels[i]);

                if (diff > 0.01) border.push(i + 1);
            }

            border.unshift(0);
            border.push(levels.length);

            // reset the mask
            masked = new Array(this.rd.length).fill(false);

            // check the borders
            for (var i = 1; i < border.length; i++) {
                var seg = [border[i - 1], border[i]];
                var seg_left = [border[i - 1], border[i - 1]];
                if (i > 1) {
                    seg_left[0] = border[i - 2];
                } else continue;

                var seg_right = [border[i], border[i]];
                if (i < border.length - 1) {
                    seg_right[1] = border[i + 1];
                } else continue;

                var n = seg[1] - seg[0];
                var n_left = seg_left[1] - seg_left[0];
                var n_right = seg_right[1] - seg_right[0];
                if (n <= 1) continue;
                var seg_array = new DataStat(levels.slice(seg[0], seg[1]));

                if (n_right <= 15 || n_left <= 15 || n <= 15) {
                    var ns = 1.8 * Math.sqrt(levels[seg_left[0]] / this.mean) * this.std;
                    if (Math.abs(levels[seg_left[0]] - levels[seg[0]]) < ns) {
                        continue;
                    }

                    ns = 1.8 * Math.sqrt(levels[seg_right[0]] / this.mean) * this.std;
                    if (Math.abs(levels[seg_right[0]] - levels[seg[0]]) < ns) {
                        continue;
                    }
                } else {
                    var seg_left_array = levels.slice(seg_left[0], seg_left[1]);
                    var seg_left_1 = new DataStat(seg_left_array);

                    var seg_right_array = levels.slice(seg_right[0], seg_right[1]);
                    var seg_right_1 = new DataStat(seg_right_array);

                    var ttest_2sample_1 = t_test_2_samples(
                        seg_array.mean,
                        seg_array.std,
                        seg_array.data.length,
                        seg_left_1.mean,
                        seg_left_1.std,
                        seg_left_1.data.length,
                    );
                    if (
                        ttest_2sample_1 >
                        (0.01 / genome_size) * bin_size * (n + n_left)
                    ) {
                        continue;
                    }

                    var ttest_2sample_2 = t_test_2_samples(
                        seg_array.mean,
                        seg_array.std,
                        seg_array.data.length,
                        seg_right_1.mean,
                        seg_right_1.std,
                        seg_right_1.data.length,
                    );
                    if (
                        ttest_2sample_2 >
                        (0.01 / genome_size) * bin_size * (n + n_right)
                    ) {
                        continue;
                    }
                }

                var ttest_1sample_1 = t_test_1_sample(
                    this.mean,
                    seg_array.mean,
                    seg_array.std,
                    seg_array.data.length,
                );
                if (ttest_1sample_1 > 0.05) {
                    continue;
                }
                var raw_seg_data = new DataStat(this.rd.slice(seg[0], seg[1]));

                masked.fill(true, seg[0], seg[1]);
                levels.fill(raw_seg_data.mean, seg[0], seg[1]);
            }
        });
        return levels;
    }

    cnv_calling(bin_size = 100000) {

        var delta = 0.25 * this.mean;

        var min = this.mean - delta, max = this.mean + delta;
        // console.log('min: ', min, ', Max: ', max)

        // console.log('delta', delta)
        var normal_genome_size = 2971000000;

        var levels = this.meanShiftCaller(bin_size);

        
        var merged_level = {};
        var cnv_levels = [];
        // var t_value = cdf(Math.abs(10), (5)) 
        // console.log('Testing student t test:', t_value)

        Object.entries(levels).forEach(([chr, chr_levels]) => {
            
            var done = false;
            while (!done) {
                done = true;

                //
                // get the borders
                //
                var borders = new Array(1).fill(0);
                for (let i = 0; i < chr_levels.length - 1; i++) {
                    var diff = Math.abs(chr_levels[i + 1] - chr_levels[i]);
                    if (diff > 0.01) borders.push(i + 1);
                }
                borders.push(chr_levels.length);

                for (let ix = 0; ix < borders.length - 2; ix++) {
                    var v1 = Math.abs(chr_levels[borders[ix]] - chr_levels[borders[ix + 1]]);
                    // console.log(ix, v1);
                    if (v1 < delta) {
                        var v2 = v1 + 1, v3 = v1 + 1;

                        if (ix > 0) { v2 = Math.abs(chr_levels[borders[ix]] - chr_levels[borders[ix - 1]]); }
                        if (ix < borders.length - 3) { v3 = Math.abs(levels[borders[ix + 1]] - chr_levels[borders[ix + 2]]); }

                        if (v1 < v2 && v1 < v3) {
                            done = false;

                            var tmp_array = new DataStat(chr_levels.slice(borders[ix], borders[ix + 2]));
                            chr_levels.fill(tmp_array.mean, borders[ix], borders[ix + 2]);
                            borders.splice(ix + 1, ix + 1);
                        }
                    }
                }
            }
            // console.log('updated levels', chr_levels)

            // var chr_rd = this.rd[chr]
            var chr_rd = [];
            Object.entries(this.rd[chr]).forEach(([bin, binDict]) => { chr_rd.push(binDict.binScore); });
            // console.log('cnv_calling', chr_rd)

            //
            // Calling Segments
            //

            //var flags = [""] * levels.length;
            var flags = new Array(chr_levels.length).fill("");

            // console.log('default levels', chr, chr_levels)
            var b = 0;
            var pval = (0.05 * bin_size) / normal_genome_size;
            while (b < chr_levels.length) {
                var b0 = b, border_start = b;
                while ((b < chr_levels.length) & (chr_levels[b] < min)) {
                    b += 1;
                }
                var border_end = b;

                if (border_end > border_start + 1) {
                    // console.log('border min', border_start, border_end)
                    var adj = adjustToEvalue(this.mean, this.std, chr_rd, border_start, border_end, pval);
                    // console.log(adj)
                    if (adj) {
                        var border_start, border_end = adj;
                        flags.fill("D", border_start, border_end);
                    }
                }
                border_start = b;
                while ((b < chr_levels.length) & (chr_levels[b] > max)) { b += 1; }
                border_end = b;

                if (border_end > border_start + 1) {
                    adj = adjustToEvalue(this.mean, this.std, chr_rd, border_start, border_end, pval);
                    // console.log(adj)
                    if (adj) {
                        (border_end = adj);
                        // flags[bs:be] = ["A"] * (be - bs)
                        flags.fill("A", border_start, border_end);
                    }
                }
                if (b == b0) b += 1;
            }

            //console.log(chr, 'segments', segments)
            //
            //  Calling additional deletions
            //
            b = 0;
            while (b < chr_levels.length) {
                while ((b < chr_levels.length) & (flags[b] != "")) b += 1;
                border_start = b;
                while ((b < chr_levels.length) & (chr_levels[b] < min)) b += 1;
                border_end = b;
                if (border_end > border_start + 1) {
                    if (gaussianEValue(this.mean, this.std, chr_rd, border_start, border_end) < 0.05 / normal_genome_size) {
                        flags.fill(["d"] * (border_end - border_start), border_start, border_end);
                    }
                    b -= 1;
                }
                b += 1;
            }

            b = 0;
            var cf;
            if (b < chr_levels.length) cf = flags[b];

            border_start = 0;

            //var merge = [...this.rd]
            var merge = [...chr_rd];
            // console.log('initial merge', merge)
            while (b < chr_levels.length) {
                while (flags[b] == cf) {
                    b += 1;
                    if (b >= flags.length) break;
                }
                if (b > border_start) {
                    var merge_arr = new DataStat(merge.slice(border_start, b));
                    merge.fill(merge_arr.mean, border_start, b);
                }
                if (b < chr_levels.length) cf = flags[b];
                border_start = b;
            }

            merged_level[chr] = merge;


            //
            // calculate calls
            // 
            b = 0;
            while (b < chr_levels.length) {
                cf = flags[b];
                if (cf == "") {
                    b += 1;
                    continue
                }
                border_start = b;
                while (b < chr_levels.length & cf == flags[b]) { b += 1; }
                var border_arr = new DataStat(merge.slice(border_start, b));
                let cnv = border_arr.mean / this.mean;
                let event_type;
                if (cf == "D") {
                    event_type = "deletion";
                } else {
                    event_type = "duplication";
                }
                let cnv_dict = {
                    chr: chr,
                    start: bin_size * border_start + 1,
                    end: bin_size * b,
                    size: bin_size * (b - border_start + 1),
                    value: cnv * 2,
                    event_type: event_type
                };
                cnv_levels.push(cnv_dict);

            }
        });

        return [merged_level, cnv_levels]
    }
}



var read_depth_caller = { Partition, MeanShiftCaller };

class CNVpytorVCF {
    /**
     * Create a class instance.
     * 
     * @param {Array} allVariants - An array containing all variants
     * @param {number} binSize - The bin size for processing variants.
     * @param {string} refGenome - Reference genome name
     */
    constructor(allVariants, binSize, refGenome) {
        this.allVariants = allVariants;
        this.rowBinSize = 10000;
        this.binSize = binSize;
        this.binFactor = parseInt(binSize / this.rowBinSize);
        this.refGenome = refGenome;
    }

    /**
     * Read rd and BAF information from the vcf file and call accoring to the caller
     */
    async read_rd_baf(caller='ReadDepth'){
        
        // Step1: Parse data from the vcf file; for a fixed rowBinSize
        var wigFeatures = {};
        for (let i = this.allVariants.length-1; i >= 0; i--){
            var featureBin;
            // assign and free space for the all_variants
            let snp = this.allVariants[i];
            this.allVariants.splice(i, 1);

            let chr = snp.chr;
            featureBin = Math.max(Math.floor(snp.start / this.rowBinSize), 0);
            if (!wigFeatures[chr]) { wigFeatures[chr] = []; }
            if (!wigFeatures[chr][featureBin]) {
                wigFeatures[chr][featureBin] = {
                    'chr': chr,
                    start: featureBin * this.rowBinSize,
                    end: (featureBin + 1) * this.rowBinSize,
                    dp_sum_score: 0,
                    dp_count: 0,
                    hets_count:0,
                    hets: [],
                };
            }

            // JTR -- note, there is an implicit assumption there that there is 1 and only 1 genotype.  Previously
            // this was hardcoded to id "9" => snp.calls[9].  By convention callset IDs == column number but this could change
            //const call = snp.calls[9]
            const calls = Object.values(snp.calls);
            if(calls.length !== 1) {
                throw Error(`Unexpected number of genotypes: ${calls.length}.  CNVPytor expects 1 and only 1 genotype`)
            }
            const call = calls[0];

            const dpValue = call.info["DP"];
            if (dpValue) {
                
                wigFeatures[chr][featureBin].dp_sum_score += Number.parseInt(dpValue);
                wigFeatures[chr][featureBin].dp_count++;
            }
            
            let ad_score = call.info["AD"].split(',');
            let genotype = call.genotype;
            if ((genotype[0] == 0 && genotype[1] == 1) || (genotype[0] == 1 && genotype[1] == 0)) {
                //apply the beta function
                wigFeatures[chr][featureBin].hets_count++;
                let ad_a = parseInt(ad_score[0]), ad_b = parseInt(ad_score[1]);
                wigFeatures[chr][featureBin].hets.push({ref:ad_a, alt:ad_b});
            }
            
        }

        // get the chromosome names
        this.chromosomes = Object.keys(wigFeatures);

        let delete_likelihood_scores = caller == 'ReadDepth' ? true : false;

        // Step2: Update the binsize according to user provided bin size
        var avgbin = this.adjust_bin_size(wigFeatures, delete_likelihood_scores=delete_likelihood_scores);

        // this is to save objects
        // console.log("avgbin: ", avgbin)
        
        // Step3: Run the CNV caller
        var finalFeatureSet;
        if(caller == 'ReadDepth'){
            // ------------ new code
            // console.log("setting up meanShift CNV calling")
            let callerObj = new read_depth_caller.MeanShiftCaller(avgbin,  this.binSize, this.refGenome);
            
            let processedBins = await callerObj.callMeanshift();
            
            // finalFeatureSet = [processedBins.binScore, processedBins.gcCorrectedBinScore, processedBins.segmentsCNV]
            finalFeatureSet = [processedBins.binScore, processedBins.gcCorrectedBinScore, processedBins.segmentsCNV];
            // var baf = this.formatDataStructure_BAF(avgbin, 'max_likelihood')
            // var baf = callerObj.format_BAF_likelihood(avgbin)
            var baf = callerObj.formatDataStructure_BAF('max_likelihood', -1);


        }else if(caller=='2D'){
            
            let caller_obj = new combined_caller.CombinedCaller(avgbin,  this.binSize, this.refGenome);        
            let processed_bins = await caller_obj.call_2d();
            
            finalFeatureSet = [processed_bins.binScore, processed_bins.gcCorrectedBinScore, processed_bins.segmentScore];
            var baf = caller_obj.formatDataStructure_BAF('max_likelihood', -1);
            
        }
        
        return [finalFeatureSet, baf]
    }

    format_BAF_likelihood(wigFeatures) {
        const results = [];

        for (const [chr, wig] of Object.entries(wigFeatures)) {
            for(let sample of wig) {
                var new_sample = { ...sample };
                if (sample.value != 0.5) {
                    new_sample.value = 1 - sample.value;
                    results.push(new_sample);
                }
            }
        }
        return results
    }

    get_max_min_score(sample) {

        if (sample.likelihood_score.length > 0) {
            const max = Math.max(...sample.likelihood_score);
            const res = sample.likelihood_score.indexOf(max);
            sample.value = Math.max(res / 100, 1 - res / 100);
            sample.min_score = Math.min(res / 100, 1 - res / 100);

        } else {
            sample.score = 0;
        }

        return sample
    }
    /*
    async getAllbins() {
        const bins = await this.computeDepthFeatures()
        const fitter = new g_utils.GetFit(bins)
        const distParams = fitter.fit_data()
        
        return bins
    }*/
    
    /**
     * Adjust the bin values to actual bin size
     * @param {*} wigFeatures - wig features after processing the varaints
     * @returns 
     */
    adjust_bin_size(wigFeatures, delete_likelihood_scores=false){
        
        var avgbin = {};
        const scale = this.binSize/150;
        for (let chr of this.chromosomes) {
            if (!avgbin[chr]) { avgbin[chr] = []; }
            for (let k = 0; k < wigFeatures[chr].length / this.binFactor; k++) {
                const featureBin = k;
                if (!avgbin[chr][k]) {
                    avgbin[chr][k] = {
                        chr,
                        start: featureBin * this.binSize,
                        end: (featureBin + 1) * this.binSize,
                        dp_count: 0,
                        hets_count: 0,
                        binScore: null,
                        likelihood_score: [],
                        dp_sum_score: 0
                    };
                }

                for (var j = k * this.binFactor; j < this.binFactor * k + this.binFactor; j++) {
                   
                    if (wigFeatures[chr][j]) {

                        avgbin[chr][k].dp_sum_score += wigFeatures[chr][j].dp_sum_score;
                        avgbin[chr][k].dp_count += wigFeatures[chr][j].dp_count;
                        avgbin[chr][k].hets_count += wigFeatures[chr][j].hets_count;

                        if (wigFeatures[chr][j].hets.length != 0){
                        
                            for(let hets of wigFeatures[chr][j].hets)  {
                                if(avgbin[chr][k].likelihood_score.length == 0){
                                    avgbin[chr][k].likelihood_score = g_utils.linspace(0, 1, 100).map((value, index) => {
                                        return beta(hets.ref, hets.alt, value);
                                    });
                                }
                                else {
                                    var likelihood_sum = 0;
                                    avgbin[chr][k].likelihood_score = g_utils.linspace(0, 1, 100).map((value, index) => {
                                        var likelihood_value = avgbin[chr][k].likelihood_score[index] * beta(hets.ref, hets.alt, value);
                                        likelihood_sum += likelihood_value;
                                        return likelihood_value;
                                    });

                                    avgbin[chr][k].likelihood_score = g_utils.linspace(0, 1, 100).map((value, index) => {
                                        return avgbin[chr][k].likelihood_score[index] / likelihood_sum;
                                    });
                       
                                }
                            }
                            
                            // avgbin[chr][k].likelihood_score *= wigFeatures[chr][j].likelihood_score
                        }
                    }
                }
                if (avgbin[chr][k].dp_count > 0) {
                    // avgbin[chr][k].binScore = parseInt(avgbin[chr][k].dp_sum_score / avgbin[chr][k].dp_count) * scale;
                    avgbin[chr][k].binScore = avgbin[chr][k].dp_sum_score / avgbin[chr][k].dp_count * scale;
                }
                const updated_bin = this.get_max_min_score(avgbin[chr][k]);
                avgbin[chr][k].max_likelihood = updated_bin.value;

                // delete the likelihood_score array; otherwise it takes too much memory
                // this is used in the combined caller; so it can be deleted depending on the caller i.e., ReadDepth caller 
                if (delete_likelihood_scores) {
                    delete avgbin[chr][k].likelihood_score;
                }
            }
        }
        return avgbin
    }

}

function beta(a, b, p, phased = true) { 
    return Math.pow(p, a) * Math.pow(1-p, b) + Math.pow(p, b) * Math.pow(1-p, a)
}

class CNVPytorTrack extends TrackBase {

    static DEFAULT_TRACK_HEIGHT = 250

    constructor(config, browser) {
        super(config, browser);
    }

    init(config) {

        // NOTE -- don't use the "defaults" convention for this track, it will not work with VariantTrack.convertToPytor()
        this.featureType = 'numeric';
        this.type = "cnvpytor";
        if (!config.max) {
            this.defaultScale = true;
            this.autoscale = false;
        }
        if(!config.height) config.height = CNVPytorTrack.DEFAULT_TRACK_HEIGHT;

        this.type = "cnvpytor";
        this.graphType = config.graphType || "points";
        this.bin_size = config.bin_size || 100000;
        this.signal_name = config.signal_name || "rd_snp";
        this.cnv_caller = config.cnv_caller || '2D';
        this.colors = config.colors || ['gray', 'black', 'green', 'blue'];
        this.hasChroms = {};
        super.init(config);

    }

    get supportsWholeGenome() {
        return true
    }

    get_signals() {
        let signals = [];

        if (this.signal_name == 'rd_snp') {
            signals = ["RD_Raw", "RD_Raw_gc_coor", this.cnv_caller, "BAF1", "BAF2"];

        } else if (this.signal_name == 'rd') {
            signals = ["RD_Raw", "RD_Raw_gc_coor", this.cnv_caller];

        } else if (this.signal_name == 'snp') {
            signals = ["BAF1", "BAF2"];

        } else if (this.signal_name == 'cnh') {
            signals = [this.cnv_caller];
        }
        return signals
    }

    get_signal_colors() {

        let signal_colors = [
            {singal_name: 'RD_Raw', color: this.colors[0]},
            {singal_name: 'RD_Raw_gc_coor', color: this.colors[1]},
            {singal_name: 'ReadDepth', color: this.colors[2]},
            {singal_name: '2D', color: this.colors[2]},
            {singal_name: 'BAF1', color: this.colors[3]},
            {singal_name: 'BAF2', color: this.colors[3]},
        ];
        return signal_colors
    }

    async postInit() {

        if (this.config.format == 'vcf') {

            let allVariants;
            if (this.featureSource) {
                allVariants = Object.values(this.featureSource.getAllFeatures()).flat();
            } else {
                this.featureSource = this.featureSource || FeatureSource(this.config, this.browser.genome);
                this.header = await this.getHeader();
                allVariants = this.featureSource.reader.features;
            }

            const refGenome = this.browser.config.genome;
            
            // Initializing CNVpytorVCF class
            const cnvpytor_obj = new CNVpytorVCF(allVariants, this.bin_size, refGenome);
            let wigFeatures;
            let bafFeatures;
            this.wigFeatures_obj = {};
            this.wigFeatures_obj[this.bin_size] = {};

            let dataWigs;

            if (this.cnv_caller == '2D') {

                dataWigs = await cnvpytor_obj.read_rd_baf('2D');

                wigFeatures = dataWigs[0];
                bafFeatures = dataWigs[1];
                this.wigFeatures_obj[this.bin_size]['2D'] = wigFeatures[2];

                this.available_callers = ['2D'];
            } else {
                dataWigs = await cnvpytor_obj.read_rd_baf();
                wigFeatures = dataWigs[0];
                bafFeatures = dataWigs[1];
                this.wigFeatures_obj[this.bin_size]['ReadDepth'] = wigFeatures[2];
                this.available_callers = ['ReadDepth'];
            }

            this.wigFeatures_obj[this.bin_size]['RD_Raw'] = wigFeatures[0];
            this.wigFeatures_obj[this.bin_size]['RD_Raw_gc_coor'] = wigFeatures[1];
            this.wigFeatures_obj[this.bin_size]['BAF1'] = bafFeatures[0];
            this.wigFeatures_obj[this.bin_size]['BAF2'] = bafFeatures[1];

            this.available_bins = [this.bin_size];

            this.set_available_callers();

        } else {
            this.cnvpytor_obj = new HDF5Reader(this.config, this.bin_size);
            // get chrom list that currently user viewing
            let chroms = [ ...new Set(this.browser.referenceFrameList.map(val => val.chr))];
            
            let aliasRecord = this.getAliasChromsList(chroms);
            this.wigFeatures_obj = await this.cnvpytor_obj.get_rd_signal(this.bin_size, aliasRecord);

            // Save the processed chroms names to check later for the availability
            this.update_hasChroms(this.wigFeatures_obj, chroms);

            this.available_bins = this.cnvpytor_obj.availableBins;
            // reset the bin size if its not exits
            if(! this.available_bins.includes(this.bin_size)){
                this.bin_size = this.available_bins.at(-1);    
            }

            this.available_callers = this.cnvpytor_obj.callers;
            this.set_available_callers();
        }

        this.tracks = [];
        const p = [];

        this.signals = this.get_signals();
        this.signal_colors = this.get_signal_colors();

        for (let bin_size in this.wigFeatures_obj) {
            for (const [signal_name, wig] of Object.entries(this.wigFeatures_obj[bin_size])) {

                if (this.signals.includes(signal_name)) {
                    let tconf = {};
                    tconf.type = "wig";
                    tconf.isMergedTrack = true;
                    tconf.features = wig;
                    tconf.name = signal_name;
                    tconf.color = this.signal_colors.filter(x => x.singal_name === signal_name).map(x => x.color);
                    const t = await this.browser.createTrack(tconf);
                    if (t) {
                        t.autoscale = false;     // Scaling done from merged track
                        this.tracks.push(t);
                    } else {
                        console.warn("Could not create track " + tconf);
                    }

                    if (typeof t.postInit === 'function') {
                        p.push(t.postInit());
                    }
                }
            }

        }


        this.flipAxis = this.config.flipAxis ? this.config.flipAxis : false;
        this.logScale = this.config.logScale ? this.config.logScale : false;
        this.autoscale = this.config.autoscale;
        if (!this.autoscale) {
            this.dataRange = {
                min: this.config.min || 0,
                max: this.config.max
            };
        }
        for (let t of this.tracks) {
            t.autoscale = false;
            t.dataRange = this.dataRange;
        }

        return Promise.all(p)
    }

    getAliasChromsList(chroms){
        let aliasRecord = chroms.map(chr => {
            let records = this.browser.genome.chromAlias.aliasRecordCache.get(chr);
            return Object.values(records)
        });
        aliasRecord = aliasRecord.flat();
        return aliasRecord
    }
    
    set_available_callers() {
        if (!this.available_callers.includes(this.cnv_caller)) {
            if (this.available_callers.length > 0) {
                this.cnv_caller = this.available_callers[0];
            } else {
                this.cnv_caller = null;
            }
        }
    }

    async getHeader() {

        if (!this.header) {
            if (typeof this.featureSource.getHeader === "function") {
                const header = await this.featureSource.getHeader();
                if (header) {
                    this.callSets = header.callSets || [];
                }
                this.header = header;
            }
            this.sampleKeys = this.callSets ? this.callSets.map(cs => cs.sample) : [];
            this.sampleNames = this.sampleKeys;
        }

        return this.header
    }

    get height() {
        return this._height
    }

    set height(h) {
        this._height = h;
        if (this.tracks) {
            for (let t of this.tracks) {
                t.height = h;
                t.config.height = h;
            }
        }
    }

    menuItemList() {
        let items = [];

        if (this.flipAxis !== undefined) {
            items.push({
                label: "Flip y-axis",
                click: function flipYAxisHandler() {
                    this.flipAxis = !this.flipAxis;
                    this.trackView.repaintViews();
                }
            });
        }

        items = items.concat(this.numericDataMenuItems());

        items.push('<hr/>');
        items.push("Bin Sizes");
        for (let rd_bin of this.available_bins) {

            items.push({
                element: createCheckbox(rd_bin, rd_bin === this.bin_size),
                click: async function binSizesHandler() {
                    this.bin_size = rd_bin;
                    // data loader image
                    this.trackView.startSpinner();

                    await this.recreate_tracks(rd_bin);
                    this.clearCachedFeatures();
                    this.trackView.updateViews();
                    this.trackView.repaintViews();
                }
            });
        }
        items.push('<hr/>');
        items.push("Signal Type");

        let signal_dct = {"rd_snp": "RD and BAF Likelihood", "rd": "RD Signal", "snp": "BAF Likelihood"};
        for (let signal_name in signal_dct) {

            items.push({
                element: createCheckbox(signal_dct[signal_name], signal_name === this.signal_name),
                click: async function signalTypeHandler() {
                    this.signal_name = signal_name;
                    await this.recreate_tracks(this.bin_size);
                    this.clearCachedFeatures();
                    this.trackView.updateViews();
                    this.trackView.repaintViews();

                }
            });
        }

        // cnv caller setting
        items.push('<hr/>');
        items.push("CNV caller");
        for (let cnv_caller of this.available_callers) {

            items.push({
                element: createCheckbox(cnv_caller, cnv_caller === this.cnv_caller),
                click: async function cnvCallerHandler() {
                    this.cnv_caller = cnv_caller;
                    // data loader image
                    this.trackView.startSpinner();

                    await this.recreate_tracks(this.bin_size);
                    this.clearCachedFeatures();
                    this.trackView.updateViews();
                    this.trackView.repaintViews();
                }
            });
        }

        // variant track conversion -- only if track was originally created from a VariantTrack
        if (this.variantState) {
            items.push('<hr/>');
            for (let cnv_caller of this.available_callers) {
                items.push({
                    label: 'Convert to variant track',
                    click: () => {
                        this.convertToVariant();
                    }
                });

            }
        }


        return items
    }

    async recreate_tracks(bin_size) {
        this.tracks = [];
        const p = [];

        if (!(bin_size in this.wigFeatures_obj)) {
            if(Object.keys(this.hasChroms).length > 0) {
                let chroms = [ ...new Set(this.browser.referenceFrameList.map(val => val.chr))];
                if(chroms[0] == "all"){
                    chroms = this.browser.genome.chromosomeNames;
                }

                this.wigFeatures_obj = {...this.wigFeatures_obj, ...await this.cnvpytor_obj.get_rd_signal(bin_size, chroms)};
                this.update_hasChroms(this.wigFeatures_obj, chroms);
            } else {
                this.wigFeatures_obj = {...this.wigFeatures_obj, ...await this.cnvpytor_obj.get_rd_signal(bin_size)};
            }
            
        }

        this.signals = this.get_signals();
        this.signal_colors = this.get_signal_colors();

        for (const [signal_name, wig] of Object.entries(this.wigFeatures_obj[bin_size])) {
            if (this.signals.includes(signal_name)) {
                let tconf = {};
                tconf.type = "wig";
                tconf.isMergedTrack = true;
                tconf.features = wig;
                tconf.name = signal_name;
                tconf.color = this.signal_colors.filter(x => x.singal_name === signal_name).map(x => x.color);
                const t = await this.browser.createTrack(tconf);
                if (t) {
                    t.autoscale = false;     // Scaling done from merged track
                    this.tracks.push(t);
                } else {
                    console.warn("Could not create track " + tconf);
                }

                if (typeof t.postInit === 'function') {
                    p.push(t.postInit());
                }
            }

        }


        this.flipAxis = this.config.flipAxis ? this.config.flipAxis : false;
        this.logScale = this.config.logScale ? this.config.logScale : false;
        this.autoscale = this.config.autoscale;
        if (!this.autoscale) {
            this.dataRange = {
                min: this.config.min || 0,
                max: this.config.max
            };
        }
        for (let t of this.tracks) {
            t.autoscale = false;
            t.dataRange = this.dataRange;
        }
        return Promise.all(p)
    }

    update_hasChroms(wigFeatures, chroms){
        for (let binSize in wigFeatures){
            if (!this.hasChroms[binSize]) {
                this.hasChroms[binSize] = new Set();
            }
            chroms.forEach(item => this.hasChroms[binSize].add(item));

        }
        return this.hasChroms

    }

    async getFeatures(chr, bpStart, bpEnd, bpPerPixel) {
        
        if(Object.keys(this.hasChroms).length > 0) {
            
            // Need to find the current binSize
            if (this.hasChroms[this.bin_size].size != 0){
                let chroms = [ ...new Set(this.browser.referenceFrameList.map(val => val.chr))];
                if(chroms[0] == "all"){
                    chroms = this.browser.genome.chromosomeNames;
                }
                let newChroms = chroms.filter(val => !this.hasChroms[this.bin_size].has(val));

                if(newChroms.length != 0){

                    let aliasRecords = this.getAliasChromsList(newChroms);
                    // update the hasChroms list
                    let tmp_wig = await this.cnvpytor_obj.get_rd_signal(this.bin_size, aliasRecords);

                    this.update_hasChroms(tmp_wig, newChroms);

                    // here we need to update the wig
                    // this part is probaby not required; code improve required
                    
                    for (let bin_size in tmp_wig){
                        for (const [signal_name, wig] of Object.entries(tmp_wig[bin_size])) {
                            await this.wigFeatures_obj[bin_size][signal_name].push(...wig);
                        }
                    }

                    for (let wig of this.tracks){
                        await wig.featureSource.updateFeatures(this.wigFeatures_obj[this.bin_size][wig.name] );
                    }
                }
            }

        }

        if (this.tracks) {
            const promises = this.tracks.map((t) => t.getFeatures(chr, bpStart, bpEnd, bpPerPixel));
            return Promise.all(promises)
        } else {
            return undefined  // This can happen if a redraw is triggered before the track has initialized.
        }
    }

    // TODO: refactor to igvUtils.js
    getScaleFactor(min, max, height, logScale) {
        const scale = logScale ? height / (Math.log10(max + 1) - (min <= 0 ? 0 : Math.log10(min + 1))) : height / (max - min);
        return scale
    }

    computeYPixelValue(yValue, yScaleFactor) {
        return (this.flipAxis ? (yValue - this.dataRange.min) : (this.dataRange.max - yValue)) * yScaleFactor
    }

    computeYPixelValueInLogScale(yValue, yScaleFactor) {
        let maxValue = this.dataRange.max;
        let minValue = this.dataRange.min;
        if (maxValue <= 0) return 0 // TODO:
        if (minValue <= -1) minValue = 0;
        minValue = (minValue <= 0) ? 0 : Math.log10(minValue + 1);
        maxValue = Math.log10(maxValue + 1);
        yValue = Math.log10(yValue + 1);
        return ((this.flipAxis ? (yValue - minValue) : (maxValue - yValue)) * yScaleFactor)
    }

    draw(options) {

        // const mergedFeatures = options.features    // Array of feature arrays, 1 for each track
        const mergedFeatures = options.features;
        if (!mergedFeatures) return

        if (this.defaultScale) {
            if (this.signal_name == 'rd_snp') {
                this.dataRange = {
                    min: this.config.min || this.dataRange.min || -1,
                    max: this.config.max || this.dataRange.max || 5
                };
            } else if (this.signal_name == 'rd') {
                this.dataRange = {
                    min: this.config.min || this.dataRange.min || 0,
                    max: this.config.max || this.dataRange.max || 5
                };
            } else if (this.signal_name == 'snp') {
                this.dataRange = {
                    min: this.config.min || this.dataRange.min || -1,
                    max: this.config.max || this.dataRange.max || 0
                };
            }
        }

        if (this.autoscale) {
            this.dataRange = autoscale(options.referenceFrame.chr, mergedFeatures);
        }

        if (this.tracks) {
            for (let i = 0, len = this.tracks.length; i < len; i++) {
                const trackOptions = Object.assign({}, options);
                trackOptions.features = mergedFeatures[i];
                this.tracks[i].dataRange = this.dataRange;
                this.tracks[i].flipAxis = this.flipAxis;
                this.tracks[i].logScale = this.logScale;
                if (this.graphType) {
                    this.tracks[i].graphType = this.graphType;
                }
                this.tracks[i].draw(trackOptions);
            }
        }

        // guides lines
        const scaleFactor = this.getScaleFactor(this.dataRange.min, this.dataRange.max, options.pixelHeight, this.logScale);
        const yScale = (yValue) => this.logScale
            ? this.computeYPixelValueInLogScale(yValue, scaleFactor)
            : this.computeYPixelValue(yValue, scaleFactor);

        // Draw guidelines
        if (this.config.hasOwnProperty('guideLines')) {
            for (let line of this.config.guideLines) {
                if (line.hasOwnProperty('color') && line.hasOwnProperty('y') && line.hasOwnProperty('dotted')) {
                    let y = yScale(line.y);
                    let props = {
                        'strokeStyle': line['color'],
                        'strokeWidth': 1
                    };
                    if (line['dotted']) IGVGraphics.dashedLine(options.context, 0, y, options.pixelWidth, y, 5, props);
                    else IGVGraphics.strokeLine(options.context, 0, y, options.pixelWidth, y, props);
                }
            }
        }

        let props = {
            'strokeStyle': 'lightgray',
            'strokeWidth': 0.5
        };
        let y = yScale(2);
        IGVGraphics.dashedLine(options.context, 0, y, options.pixelWidth, y, 5, props);

    }

    paintAxis(ctx, pixelWidth, pixelHeight) {

        var x1,
            x2,
            y1,
            y2,
            a,
            b,
            reference,
            shim,
            font = {
                'font': 'normal 10px Arial',
                'textAlign': 'right',
                'strokeStyle': "black"
            };

        if (undefined === this.dataRange || undefined === this.dataRange.max || undefined === this.dataRange.min) {
            return
        }

        let flipAxis = (undefined === this.flipAxis) ? false : this.flipAxis;

        IGVGraphics.fillRect(ctx, 0, 0, pixelWidth, pixelHeight, {'fillStyle': "rgb(255, 255, 255)"});

        reference = 0.95 * pixelWidth;
        x1 = reference - 8;
        x2 = reference;

        //shim = 0.5 * 0.125;
        shim = .01;
        y1 = y2 = shim * pixelHeight;

        a = {x: x2, y: y1};

        // tick
        IGVGraphics.strokeLine(ctx, x1, y1, x2, y2, font);
        IGVGraphics.fillText(ctx, prettyPrint(flipAxis ? this.dataRange.min : this.dataRange.max), x1 + 4, y1 + 12, font);

        //shim = 0.25 * 0.125;
        y1 = y2 = (1.0 - shim) * pixelHeight;

        b = {x: x2, y: y1};

        // tick
        IGVGraphics.strokeLine(ctx, x1, y1, x2, y2, font);
        IGVGraphics.fillText(ctx, prettyPrint(flipAxis ? this.dataRange.max : this.dataRange.min), x1 + 4, y1 - 4, font);

        IGVGraphics.strokeLine(ctx, a.x, a.y, b.x, b.y, font);

        function prettyPrint(number) {
            // if number >= 100, show whole number
            // if >= 1 show 1 significant digits
            // if <  1 show 2 significant digits

            // change the label for negative number to positive; For BAF likelihood section
            if(number < 0){
                return Math.abs(number)
            }

            if (number === 0) {
                return "0"
            } else if (Math.abs(number) >= 10) {
                return number.toFixed()
            } else if (number % 1 == 0) {
                return number.toFixed()
            } else if (Math.abs(number) >= 1) {
                return number.toFixed(1)
            } else {
                return number.toFixed(2)
            }
        }

        const scaleFactor = this.getScaleFactor(this.dataRange.min, this.dataRange.max, pixelHeight, this.logScale);
        const yScale = (yValue) => this.logScale
            ? this.computeYPixelValueInLogScale(yValue, scaleFactor)
            : this.computeYPixelValue(yValue, scaleFactor);

        const n = Math.ceil((this.dataRange.max - this.dataRange.min) / 10);
        for (let p = Math.ceil(this.dataRange.min + 1); p < Math.round(this.dataRange.max - 0.4); p += n) {
            const yp = yScale(p);
            IGVGraphics.strokeLine(ctx, 45, yp, 50, yp, font); // Offset dashes up by 2 pixel
            IGVGraphics.fillText(ctx, prettyPrint(flipAxis ? this.dataRange.max - p : p), 44, yp + 4, font);

        }

    }

    popupData(clickState, features) {

        const featuresArray = features || clickState.viewport.cachedFeatures;

        if (featuresArray && featuresArray.length === this.tracks.length) {
            // Array of feature arrays, 1 for each track
            const popupData = [];
            for (let i = 0; i < this.tracks.length; i++) {
                if (i > 0) popupData.push('<hr/>');
                popupData.push(`<div style=background-color:rgb(245,245,245);border-bottom-style:dashed;border-bottom-width:1px;padding-bottom:5px;padding-top:10px;font-weight:bold;font-size:larger >${this.tracks[i].name}</div>`);
                const trackPopupData = this.tracks[i].popupData(clickState, featuresArray[i]);
                popupData.push(...trackPopupData);

            }
            return popupData
        }
    }

    /**
     * Applicable if this track was originally created from a VariantTrack -- attempt to convert it back
     * @returns {Promise<void>}
     */
    async convertToVariant() {

        if (this.variantState) {
            Object.setPrototypeOf(this, VariantTrack.prototype);
            this.init(this.variantState);
            await this.postInit();
            this.trackView.clearCachedFeatures();
            if(this.variantState.trackHeight) {
                this.trackView.setTrackHeight(this.variantState.trackHeight);
            }
            this.trackView.checkContentHeight();
            this.trackView.updateViews();
            delete this.variantState;
        }
    }

}

function autoscale(chr, featureArrays) {

    let min = 0;
    let max = -Number.MAX_VALUE;
    for (let features of featureArrays) {
        for (let f of features) {
            if (typeof f.value !== 'undefined' && !Number.isNaN(f.value)) {
                min = Math.min(min, f.value);
                max = Math.max(max, f.value);
            }
        }
    }
    return {min: min, max: max}
}

const isString$1 = isString$3;

const DEFAULT_VISIBILITY_WINDOW = 1000000;
const TOP_MARGIN = 10;

class VariantTrack extends TrackBase {

    static defaultColor = 'rgb(0,0,150)'

    static defaults = {
        displayMode: "EXPANDED",
        sortDirection: "ASC",
        showGenotypes: true,
        expandedVariantHeight: 10,
        squishedVariantHeight: 2,
        squishedCallHeight: 1,
        expandedCallHeight: 10,
        expandedVGap: 2,
        squishedVGap: 1,
        expandedGroupGap: 10,
        squishedGroupGap: 5,
        featureHeight: 14,
        noGenotypeColor: "rgb(200,180,180)",
        noCallColor: "rgb(225, 225, 225)",
        nonRefColor: "rgb(200, 200, 215)",
        mixedColor: "rgb(200, 220, 200)",
        homrefColor: "rgb(200, 200, 200)",
        homvarColor: "rgb(17,248,254)",
        hetvarColor: "rgb(34,12,253)",
        refColor: "rgb(0,0,220)",
        altColor: "rgb(255,0,0)",
        visibilityWindow: undefined,
        labelDisplayMode: undefined,
        type: "variant"
    }

    _sortDirections = new Map()

    constructor(config, browser) {
        super(config, browser);
    }

    // Note -- init gets called during base class construction.  Confusing
    init(config) {

        super.init(config);

        if (config.variantHeight) {
            // Override for backward compatibility
            this.expandedVariantHeight = config.variantHeight;
        }

        this.featureSource = FeatureSource(config, this.browser.genome);

        this.colorTables = new Map();
        if (config.colorTable) {
            const key = config.colorBy || "*";
            this.colorTables.set(key, new ColorTable(config.colorTable));
        }

        this.strokecolor = config.strokecolor;
        this._context_hook = config.context_hook;

        // If a color is explicitly set disable colorBy
        if (config.color) {
            this.colorBy = undefined;
        }

        // The number of variant rows are computed dynamically, but start with "1" by default
        this.nVariantRows = 1;

        // Explicitly set samples -- used to select a subset of samples from a dataset
        if (config.samples) {
            // Explicit setting, keys == names
            for (let s of config.samples) {
                this.sampleKeys = config.samples;
            }
        }

        if (config.sort) {
            this.initialSort = config.sort;
        }

        this._colorByItems = new Map([['none', 'None']]);
    }

    async postInit() {

        this.header = await this.getHeader();

        // Set colorBy, if not explicitly set default to allele frequency, if available, otherwise default to none (undefined)
        if (this.header.INFO) {
            const infoFields = new Set(Object.keys(this.header.INFO));
            if (this.config.colorBy) {
                this.colorBy = this.config.colorBy;
            } else if (!this.config.color && infoFields.has('AF')) {
                this.colorBy = 'AF';
            }

            // Configure menu items based on info available
            if (infoFields.has('AF')) {
                this._colorByItems.set('AF', 'Allele frequency');
            }
            if (infoFields.has('VT')) {
                this._colorByItems.set('VT', 'Variant Type');
            }
            if (infoFields.has('SVTYPE')) {
                this._colorByItems.set('SVTYPE', 'SV Type');
            }
        }

        if (this.config.colorBy && !this._colorByItems.has(this.config.colorBy)) {
            this._colorByItems.set(this.config.colorBy, this.config.colorBy);
        }


        if (this.disposed) return   // This track was removed during async load

        if (this.header && !this.sampleKeys) {
            this.sampleKeys = this.header.sampleNameMap ? Array.from(this.header.sampleNameMap.keys()) : [];
        }
        if (undefined === this.visibilityWindow && this.config.indexed !== false) {
            const fn = isFile$1(this.config.url) ? this.config.url.name : this.config.url;
            if (isString$1(fn) && fn.toLowerCase().includes("gnomad")) {
                this.visibilityWindow = 1000;  // these are known to be very dense
            } else if (typeof this.featureSource.defaultVisibilityWindow === 'function') {
                this.visibilityWindow = await this.featureSource.defaultVisibilityWindow();
            } else {
                this.visibilityWindow = DEFAULT_VISIBILITY_WINDOW;
            }
        }

        this._initialColor = this.color || this.constructor.defaultColor;
        this._initialAltColor = this.altColor || this.constructor.defaultColor;

        return this
    }

    get supportsWholeGenome() {
        return !this.config.indexURL || this.config.supportsWholeGenome === true
    }

    get color() {
        return this._color || VariantTrack.defaultColor
    }

    set color(c) {
        this._color = c;
        if (c) {
            this.colorBy = undefined;
        }
    }

    async getHeader() {
        if (!this.header) {
            if (typeof this.featureSource.getHeader === "function") {
                this.header = await this.featureSource.getHeader();
            }
        }
        return this.header
    }

    getSampleCount() {
        return this.sampleKeys ? this.sampleKeys.length : 0
    }

    async getFeatures(chr, start, end, bpPerPixel) {

        if (this.header === undefined) {
            this.header = await this.getHeader();
        }
        const features = await this.featureSource.getFeatures({
            chr,
            start,
            end,
            bpPerPixel,
            visibilityWindow: this.visibilityWindow
        });

        if (this.initialSort) {
            const sort = this.initialSort;
            if (sort.option === undefined || sort.option.toUpperCase() === "GENOTYPE") {
                this.sortSamplesByGenotype(sort, features);
            } else if ("ATTRIBUTE" === sort.option.toUpperCase() && sort.attribute) {
                const sortDirection = "ASC" === sort.direction ? 1 : -1;
                this.sortByAttribute(sort.attribute, sortDirection);
            }
            this.initialSort = undefined;  // Sample order is sorted,
        }

        return features
    }

    hasSamples() {
        return this.getSampleCount() > 0
    }

    /**
     * Required method of the sample name and info viewports
     */
    getSamples() {

        const vGap = ("SQUISHED" === this.displayMode) ? this.squishedVGap : this.expandedVGap;
        const nVariantRows = "COLLAPSED" === this.displayMode ? 1 : this.nVariantRows;
        const variantHeight = ("SQUISHED" === this.displayMode) ? this.squishedVariantHeight : this.expandedVariantHeight;
        const callHeight = ("SQUISHED" === this.displayMode ? this.squishedCallHeight : this.expandedCallHeight);
        const height = nVariantRows * (callHeight + vGap);

        // Y Offset at which samples begin
        const yOffset = TOP_MARGIN + nVariantRows * (variantHeight + vGap);

        return {
            names: this.sampleKeys,
            yOffset,
            height,
            // groups: this.groups,
            // groupIndeces,
            // groupMarginHeight: this.getGroupMarginHeight()
        }
    }


    /**
     * The required height in pixels required for the track content.   This is not the visible track height, which
     * can be smaller (with a scrollbar) or larger.
     *
     * @param features
     * @returns {*}
     */
    computePixelHeight(features) {

        if (!features || 0 === features.length) return TOP_MARGIN

        const nVariantRows = (this.displayMode === "COLLAPSED") ? 1 : this.nVariantRows;
        const vGap = (this.displayMode === "SQUISHED") ? this.squishedVGap : this.expandedVGap;
        const variantHeight = (this.displayMode === "SQUISHED") ? this.squishedVariantHeight : this.expandedVariantHeight;
        const callHeight = (this.displayMode === "SQUISHED") ? this.squishedCallHeight : this.expandedCallHeight;
        const nGenotypes = this.showGenotypes === false ? 0 : this.getSampleCount() * nVariantRows;
        const h = TOP_MARGIN + nVariantRows * (variantHeight + vGap);
        return h + vGap + (nGenotypes + 1) * (callHeight + vGap)

    }

    variantRowCount(count) {
        this.nVariantRows = count;
    }

    draw({context, pixelWidth, pixelHeight, bpPerPixel, bpStart, pixelTop, features}) {

        IGVGraphics.fillRect(context, 0, pixelTop, pixelWidth, pixelHeight, {'fillStyle': "rgb(255, 255, 255)"});

        const vGap = ("SQUISHED" === this.displayMode) ? this.squishedVGap : this.expandedVGap;
        const rowCount = ("COLLAPSED" === this.displayMode) ? 1 : this.nVariantRows;
        const variantHeight = ("SQUISHED" === this.displayMode) ? this.squishedVariantHeight : this.expandedVariantHeight;
        this.variantBandHeight = TOP_MARGIN + rowCount * (variantHeight + vGap);

        let callSets = this.sampleColumns;

        const hasSamples = this.hasSamples();
        if (callSets && hasSamples && this.showGenotypes !== false) {
            IGVGraphics.strokeLine(context, 0, this.variantBandHeight, pixelWidth, this.variantBandHeight, {strokeStyle: 'rgb(224,224,224) '});
        }

        if (features) {

            const callHeight = ("SQUISHED" === this.displayMode) ? this.squishedCallHeight : this.expandedCallHeight;
            const vGap = ("SQUISHED" === this.displayMode) ? this.squishedVGap : this.expandedVGap;
            const bpEnd = bpStart + pixelWidth * bpPerPixel + 1;

            // Loop through variants.  A variant == a row in a VCF file
            for (let v of features) {

                if (this._filter && !this._filter(v)) continue
                if (v.end < bpStart) continue
                if (v.start > bpEnd) break

                const variantHeight = ("SQUISHED" === this.displayMode) ? this.squishedVariantHeight : this.expandedVariantHeight;
                const y = TOP_MARGIN + ("COLLAPSED" === this.displayMode ? 0 : v.row * (variantHeight + vGap));
                const h = variantHeight;

                // Compute pixel width.   Minimum width is 3 pixels,  if > 5 pixels create gap between variants
                let x = (v.start - bpStart) / bpPerPixel;
                let x1 = (v.end - bpStart) / bpPerPixel;

                let w = Math.max(1, x1 - x);
                if (w < 3) {
                    w = 3;
                    x -= 1;
                } else if (w > 5) {
                    x += 1;
                    w -= 2;
                }

                const variant = v._f || v;   // True variant record, used for whole genome view and SV mate records
                let af;
                try {
                    af = variant.alleleFreq();
                } catch (e) {
                }
                if ("AF" === this.colorBy && af) {
                    const hAlt = Math.min(1, af) * h;
                    const hRef = h - hAlt;
                    context.fillStyle = variant.isFiltered() ? this.refColorFiltered : this.refColor;
                    context.fillRect(x, y, w, hRef);
                    context.fillStyle = variant.isFiltered() ? this.altColorFiltered : this.altColor;
                    context.fillRect(x, y + hRef, w, hAlt);

                } else {
                    context.fillStyle = this.getColorForFeature(variant);
                    context.fillRect(x, y, w, h);
                }

                //only paint stroke if a color is defined
                let strokecolor = this.getVariantStrokecolor(variant);
                if (strokecolor) {
                    context.strokeStyle = strokecolor;
                    context.strokeRect(x, y, w, h);
                }

                // call hook if _context_hook fn is defined
                this.callContextHook(variant, context, x, y, w, h);

                //variant.pixelRect = {x, y, w, h}

                // Loop though the samples for this variant.
                if (hasSamples && this.showGenotypes !== false) {

                    const nVariantRows = "COLLAPSED" === this.displayMode ? 1 : this.nVariantRows;
                    this.sampleYOffset = this.variantBandHeight + vGap;
                    this.sampleHeight = nVariantRows * (callHeight + vGap);  // For each sample, there is a call for each variant at this position

                    let sampleNumber = 0;

                    for (let sample of this.sampleKeys) {

                        const index = this.header.sampleNameMap.get(sample);
                        const call = variant.calls[index];
                        if (call) {
                            const row = "COLLAPSED" === this.displayMode ? 0 : variant.row;
                            const py = this.sampleYOffset + sampleNumber * this.sampleHeight + row * (callHeight + vGap);
                            let allVar = true;  // until proven otherwise
                            let allRef = true;
                            let noCall = false;

                            if (call.genotype) {
                                for (let g of call.genotype) {
                                    if ('.' === g) {
                                        noCall = true;
                                        break
                                    } else {
                                        if (g !== 0) allRef = false;
                                        if (g === 0) allVar = false;
                                    }
                                }
                            }

                            if (!call.genotype) {
                                context.fillStyle = this.noGenotypeColor;
                            } else if (noCall) {
                                context.fillStyle = this.noCallColor;
                            } else if (allRef) {
                                context.fillStyle = this.homrefColor;
                            } else if (allVar) {
                                context.fillStyle = this.homvarColor;
                            } else {
                                context.fillStyle = this.hetvarColor;
                            }

                            context.fillRect(x, py, w, callHeight);

                            //callSet.pixelRect = {x, y: py, w, h: callHeight}
                        }
                        sampleNumber++;
                    }

                }
            }

        }
    };

    get refColorFiltered() {
        if (!this._refColorFiltered) {
            this._refColorFiltered = IGVColor.addAlpha(this.refColor, 0.2);
        }
        return this._refColorFiltered
    }

    get altColorFiltered() {
        if (!this._altColorFiltered) {
            this._altColorFiltered = IGVColor.addAlpha(this.altColor, 0.2);
        }
        return this._altColorFiltered
    }

    getColorForFeature(variant) {

        const v = variant._f || variant;
        let variantColor;

        if (this.colorBy && 'none' !== this.colorBy) {

            const value = v.getAttributeValue(this.colorBy);
            variantColor = value !== undefined ? this.getVariantColorTable(this.colorBy).getColor(value) : "gray";

        } else if (this.color) {
            variantColor = (typeof this.color === "function") ? this.color(variant) : this.color;
        } else if ("NONVARIANT" === v.type) {
            variantColor = this.nonRefColor;
        } else if ("MIXED" === v.type) {
            variantColor = this.mixedColor;
        } else {
            variantColor = this.color;
        }

        if (v.isFiltered()) {
            variantColor = IGVColor.addAlpha(variantColor, 0.2);
        }

        return variantColor
    }


    getVariantStrokecolor(variant) {

        const v = variant._f || variant;
        let variantStrokeColor;

        if (this.strokecolor) {
            variantStrokeColor = (typeof this.strokecolor === "function") ? this.strokecolor(v) : this.strokecolor;
        } else {
            variantStrokeColor = undefined;
        }
        return variantStrokeColor
    }

    callContextHook(variant, context, x, y, w, h) {
        if (this._context_hook) {
            if (typeof this._context_hook === "function") {
                const v = variant._f || variant;

                context.save();
                this._context_hook(v, context, x, y, w, h);
                context.restore();
            }
        }
    }

    clickedFeatures(clickState) {

        let featureList = super.clickedFeatures(clickState);

        const vGap = (this.displayMode === 'EXPANDED') ? this.expandedVGap : this.squishedVGap;
        const callHeight = vGap + ("SQUISHED" === this.displayMode ? this.squishedCallHeight : this.expandedCallHeight);
        // Find the variant row (i.e. row assigned during feature packing)
        const yOffset = clickState.y;
        if (yOffset <= this.variantBandHeight) {
            // Variant
            const variantHeight = ("SQUISHED" === this.displayMode) ? this.squishedVariantHeight : this.expandedVariantHeight;
            const variantRow = Math.floor((yOffset - TOP_MARGIN) / (variantHeight + vGap));
            if ("COLLAPSED" !== this.displayMode) {
                featureList = featureList.filter(f => f.row === variantRow);
            }
        } else if (this.sampleKeys) {
            const sampleY = yOffset - this.variantBandHeight;
            const sampleRow = Math.floor(sampleY / this.sampleHeight);
            if (sampleRow >= 0 && sampleRow < this.sampleKeys.length) {
                const variantRow = Math.floor((sampleY - sampleRow * this.sampleHeight) / callHeight);
                const variants = "COLLAPSED" === this.displayMode ? featureList : featureList.filter(f => f.row === variantRow);
                const sampleName = this.sampleKeys[sampleRow];
                const index = this.header.sampleNameMap.get(sampleName);
                featureList = variants.map(v => {
                    const call = v.calls[index];
                    // This is hacky, but it avoids expanding all calls in advance in case one is clicked, or
                    // alternatively storing backpoints to the variant for all calls.
                    call.genotypeString = expandGenotype(call, v);
                    return call
                });
            }
        }

        return featureList
    }


    /**
     * Return "popup data" for feature @ genomic location.  Data is an array of key-value pairs
     */
    popupData(clickState, featureList) {

        if (featureList === undefined) featureList = this.clickedFeatures(clickState);
        const genomicLocation = clickState.genomicLocation;
        const genomeID = this.browser.genome.id;

        let popupData = [];
        for (let v of featureList) {

            const f = v._f || v;    // Get real variant from psuedo-variant, e.g. whole genome or SV mate

            if (popupData.length > 0) {
                popupData.push({html: '<hr style="border-top-width:2px ;border-color: #c9c3ba" />'});
            }

            if (typeof f.popupData === 'function') {
                const v = f.popupData(genomicLocation, genomeID);
                Array.prototype.push.apply(popupData, v);
            }
        }
        return popupData

    }


// VariantTrack.prototype.contextMenuItemList = function (clickState) {
//
//     const self = this;
//     const menuItems = [];
//
//     const featureList = this.clickedFeatures(clickState);
//
//     if (this.sampleColumns && featureList && featureList.length > 0) {
//
//         featureList.forEach(function (variant) {
//
//             if ('str' === variant.type) {
//
//                 menuItems.push({
//                     label: 'Sort by allele length',
//                     click: function () {
//                         sortCallSetsByAlleleLength(self.sampleColumns, variant, self.sortDirection);
//                         self.sortDirection = (self.sortDirection === "ASC") ? "DESC" : "ASC";
//                         self.trackView.repaintViews();
//                     }
//                 });
//
//             }
//
//         });
//     }
//
//
//     function sortCallSetsByAlleleLength(callSets, variant, direction) {
//         var d = (direction === "DESC") ? 1 : -1;
//         Object.keys(callSets).forEach(function (property) {
//             callSets[property].sort(function (a, b) {
//                 var aNan = isNaN(variant.calls[a.id].genotype[0]);
//                 var bNan = isNaN(variant.calls[b.id].genotype[0]);
//                 if (aNan && bNan) {
//                     return 0;
//                 } else if (aNan) {
//                     return 1;
//                 } else if (bNan) {
//                     return -1;
//                 } else {
//                     var a0 = getAlleleString(variant.calls[a.id], variant, 0);
//                     var a1 = getAlleleString(variant.calls[a.id], variant, 1);
//                     var b0 = getAlleleString(variant.calls[b.id], variant, 0);
//                     var b1 = getAlleleString(variant.calls[b.id], variant, 1);
//                     var result = Math.max(b0.length, b1.length) - Math.max(a0.length, a1.length);
//                     if (result === 0) {
//                         result = Math.min(b0.length, b1.length) - Math.min(a0.length, a1.length);
//                     }
//                     return d * result;
//                 }
//             });
//         });
//     }
//
//
//     return menuItems;
//
// };

    menuItemList() {

        const menuItems = [];

        // color-by INFO attribute
        if (this.header.INFO) {
            //Code below will present checkboxes for all info fields of type "String".   Wait until this is requested
            //const stringInfoKeys = Object.keys(this.header.INFO).filter(key => "String" === this.header.INFO[key].Type);

            // For now stick to explicit info fields (well, exactly 1 for starters)
            if (this.header.INFO) {
                //const stringInfoKeys = Object.keys(this.header.INFO).filter(key => this.header.INFO[key].Type === "String")
                const colorByItems = this._colorByItems;
                menuItems.push('<hr/>');
                const element = createElementWithString('<div class="igv-track-menu-category igv-track-menu-border-top">');
                element.textContent = 'Color by:';
                menuItems.push({name: undefined, element, click: undefined, init: undefined});
                for (let key of colorByItems.keys()) {
                    const selected = (this.colorBy === key);
                    menuItems.push(this.colorByCB({key, label: colorByItems.get(key)}, selected));
                }

                menuItems.push(this.colorByCB({key: 'info', label: 'Info field...'}));

            }
        }

        if (true === doSortByAttributes(this.browser.sampleInfo, this.sampleKeys)) {

            menuItems.push('<hr/>');

            menuItems.push("Sort by attribute:");
            for (const attribute of this.browser.sampleInfo.attributeNames) {

                if (this.sampleKeys.some(s => {
                    const attrs = this.browser.sampleInfo.getAttributes(s);
                    return attrs && attrs[attribute]
                })) {


                    const element = document.createElement('div');
                    element.innerHTML = `&nbsp;&nbsp;${attribute.split(SampleInfo.emptySpaceReplacement).join(' ')}`;

                    const attributeSort = () => {
                        const sortDirection = this._sortDirections.get(attribute) || 1;
                        this.sortByAttribute(attribute, sortDirection);
                        this.config.sort = {
                            option: "ATTRIBUTE",
                            attribute: attribute,
                            direction: sortDirection > 0 ? "ASC" : "DESC"
                        };
                        this._sortDirections.set(attribute, sortDirection * -1);
                    };

                    menuItems.push({element, click: attributeSort});
                }
            }
        }

        menuItems.push('<hr/>');

        if (this.getSampleCount() > 0) {
            menuItems.push({ element: createElementWithString('<div class="igv-track-menu-border-top">') });
            menuItems.push({
                element: createCheckbox("Show Genotypes", this.showGenotypes),
                click: function showGenotypesHandler() {
                    this.showGenotypes = !this.showGenotypes;
                    this.trackView.checkContentHeight();
                    this.trackView.repaintViews();
                    this.browser.sampleNameControl.performClickWithState(this.browser, this.showGenotypes);
                    this.browser.sampleInfoControl.performClickWithState(this.browser, this.showGenotypes);
                }
            });
        }

        menuItems.push({element: createElementWithString('<div class="igv-track-menu-border-top">')});
        for (let displayMode of ["COLLAPSED", "SQUISHED", "EXPANDED"]) {
            var lut =
                {
                    "COLLAPSED": "Collapse",
                    "SQUISHED": "Squish",
                    "EXPANDED": "Expand"
                };

            menuItems.push(
                {
                    element: createCheckbox(lut[displayMode], displayMode === this.displayMode),
                    click: function displayModeHandler() {
                        this.displayMode = displayMode;
                        this.trackView.checkContentHeight();
                        this.trackView.repaintViews();
                    }
                });
        }

        // Experimental JBrowse circular view integration
        if (this.browser.circularView) {

            menuItems.push('<hr>');
            menuItems.push({
                label: 'Add SVs to circular view',
                click: function circularViewHandler() {
                    for (let viewport of this.trackView.viewports) {
                        this.sendChordsForViewport(viewport);
                    }
                }
            });
        }

        // Experimental CNVPytor support
        if (this.canCovertToPytor()) {
            menuItems.push('<hr>');
            menuItems.push({
                label: 'Convert to CNVpytor track',
                click: function cnvPytorHandler() {
                    this.convertToPytor();
                }
            });
        }

        return menuItems
    }


    contextMenuItemList(clickState) {

        const list = [];

        if (this.hasSamples() && this.showGenotypes) {
            const referenceFrame = clickState.viewport.referenceFrame;
            const genomicLocation = clickState.genomicLocation;

            // We can't know genomic location intended with precision, define a buffer 5 "pixels" wide in genomic coordinates
            const bpWidth = referenceFrame.toBP(2.5);

            const direction = this._sortDirections.get('genotype') || 1;
            this._sortDirections.set('genotype', direction * -1);  // Toggle for next sort

            list.push(
                {
                    label: 'Sort by genotype',
                    click: (e) => {

                        const sort = {
                            direction,
                            option: 'genotype',
                            chr: clickState.viewport.referenceFrame.chr,
                            start: Math.floor(genomicLocation - bpWidth),
                            end: Math.ceil(genomicLocation + bpWidth)

                        };
                        const viewport = clickState.viewport;
                        const features = viewport.cachedFeatures;
                        this.sortSamplesByGenotype(sort, features);

                        this.config.sort = sort;
                    }
                }
            );
            list.push('<hr/>');
        }

        // Experimental JBrowse circular view integration
        if (this.browser.circularView) {

            const viewport = clickState.viewport;
            list.push({
                label: 'Add SVs to Circular View',
                click: () => {
                    this.sendChordsForViewport(viewport);
                }
            });
            list.push('<hr/>');
        }

        return list

    }


    async sortSamplesByGenotype({chr, position, start, end, direction}, featureList) {

        if (start === undefined) start = position - 1;
        if (end === undefined) end = position;

        if (!featureList) {
            featureList = await this.featureSource.getFeatures({chr, start, end});
        }
        if (!featureList) return

        const scores = new Map();
        const d2 = (direction === "ASC" ? 1 : -1);

        // Compute score for each sample
        for (let variant of featureList) {
            if (variant.end < start) continue
            if (variant.start > end) break
            for (let call of variant.calls) {
                const sample = call.sample;
                const callScore = call.zygosityScore();
                scores.set(sample, scores.has(sample) ? scores.get(sample) + callScore : callScore);
            }
        }

        // Now sort sample names by score
        this.sampleKeys.sort(function (a, b) {
            let sa = scores.get(a) || 0;
            let sb = scores.get(b) || 0;
            return d2 * (sa - sb)
        });

        this.trackView.repaintViews();
    }

    sortByAttribute(attribute, sortDirection) {

        this.config.sort = {
            option: "ATTRIBUTE",
            attribute: attribute,
            direction: sortDirection === 1 ? "ASC" : "DESC"
        };

        this.sampleKeys = this.browser.sampleInfo.sortSampleKeysByAttribute(this.sampleKeys, attribute, sortDirection);
        this.trackView.repaintViews();

    }


    sendChordsForViewport(viewport) {
        const refFrame = viewport.referenceFrame;
        let inView;
        if ("all" === refFrame.chr) {
            const all = this.featureSource.getAllFeatures();
            const arrays = Object.keys(all).map(k => all[k]);
            inView = [].concat(...arrays);
        } else {
            inView = this.featureSource.featureCache.queryFeatures(refFrame.chr, refFrame.start, refFrame.end);

        }

        const chords = makeVCFChords(inView);
        sendChords(chords, this, refFrame, 0.5);
    }

    /**
     * Create a "color by" checkbox menu item, optionally initially checked
     * @param menuItem
     * @param showCheck
     * @returns {{init: undefined, name: undefined, click: clickHandler, element: (jQuery|HTMLElement|jQuery.fn.init)}}
     */
    colorByCB(menuItem, showCheck) {

        const element = createCheckbox(menuItem.label, showCheck);

        if (menuItem.key !== 'info') {
            function clickHandler() {
                const colorBy = ('none' === menuItem.key) ? undefined : menuItem.key;
                this.colorBy = colorBy;
                this.config.colorBy = colorBy;
                this.trackView.repaintViews();
            }

            return {name: undefined, element, click: clickHandler, init: undefined}
        } else {
            function dialogPresentationHandler(ev) {
                this.browser.inputDialog.present({
                    label: 'Info field',
                    value: '',
                    callback: (infoField) => {
                        if (infoField) {
                            this.colorBy = infoField;
                            this._colorByItems.set(infoField, infoField);
                        } else {
                            this.colorBy = undefined;
                        }
                        this.trackView.repaintViews();
                    }
                }, ev);
            }

            return {name: undefined, element, dialog: dialogPresentationHandler, init: undefined}
        }
    }

    getState() {

        const config = super.getState();
        if (this.color && typeof this.color !== "function") {
            config.color = this.color;
        }
        return config

    }

    /**
     * Return the variant type for the given key, which should be a colorable attribute
     * @param key
     * @returns {any}
     */
    getVariantColorTable(key) {

        if (this.colorTables.has(key)) {
            return this.colorTables.get(key)
        } else if (this.colorTables.has("*")) {
            return this.colorTables.get("*")
        } else {
            let tbl;
            switch (key) {
                case "SVTYPE" :
                    tbl = SV_COLOR_TABLE;
                    break
                default:
                    tbl = new PaletteColorTable("Set1");
            }
            this.colorTables.set(key, tbl);
            return tbl
        }
    }

    ///////////// CNVPytor converstion support follows ////////////////////////////////////////////////////////////

    /**
     * This do-nothing method is neccessary to allow conversion to a CNVPytor track, which needs dom elements for an
     *     // axis.  The dom elements are created as a side effect of this function being defined
     */
    paintAxis() {
    }

    /**
     * Check conditions for pytor track
     * (1) 1 and only 1 genotype (callset)
     * (2) DP info field
     * (3) AD info field
     * (4) Not indexed -- must read entire file
     */
    canCovertToPytor() {

        if (this.config.indexURL) {
            return false
        }
        if (this.header) {
            return Object.keys(this.sampleKeys).length === 1 &&
                this.header.FORMAT &&
                this.header.FORMAT.AD &&
                this.header.FORMAT.DP
        } else {
            // Cant know until header is read
            return false
        }
    }

    async convertToPytor() {

        // Store state in case track is reverted
        this.variantState = {...this.config, ...this.getState()};
        this.variantState.trackHeight = this.height;


        this.trackView.startSpinner();
        // The timeout is neccessary to give the spinner time to start.
        setTimeout(async () => {
            try {
                const newConfig = Object.assign({}, this.config);
                Object.setPrototypeOf(this, CNVPytorTrack.prototype);

                this.init(newConfig);
                await this.postInit();

                this.trackView.clearCachedFeatures();
                this.trackView.setTrackHeight(this.config.height || CNVPytorTrack.DEFAULT_TRACK_HEIGHT);
                this.trackView.checkContentHeight();
                this.trackView.updateViews();

            } finally {
                this.trackView.stopSpinner();
            }
        }, 100);
    }

    getFilterableAttributes() {
        return this.header.INFO
    }

    /**
     * Repack cached features, if any, for all viewports on this track
     */
    _repackCachedFeatures() {
        for (let viewport of this.trackView.viewports) {
            if (viewport.cachedFeatures) {
                const maxRows = this.config.maxRows || Number.MAX_SAFE_INTEGER;
                packFeatures(viewport.cachedFeatures, maxRows, this._filter);
            }
        }
    }
}


function expandGenotype(call, variant) {

    if (call.genotype) {
        let gt = '';
        if (variant.alternateBases === ".") {
            gt = "No Call";
        } else {
            const altArray = variant.alternateBases.split(",");
            for (let allele of call.genotype) {
                if (gt.length > 0) {
                    gt += " | ";
                }
                if ('.' === allele) {
                    gt += '.';
                } else if (allele === 0) {
                    gt += variant.referenceBases;
                } else {
                    let alt = altArray[allele - 1].replace("<", "&lt;");
                    gt += alt;
                }
            }
        }
        return gt
    }
}


const SV_COLOR_TABLE = new ColorTable({
    'DEL': '#ff2101',
    'INS': '#001888',
    'DUP': '#028401',
    'INV': '#008688',
    'CNV': '#8931ff',
    'BND': '#891100',
    '*': '#002eff'
});

const DEFAULT_SEARCH_CONFIG = {
    timeout: 5000,
    type: "plain",
    url: 'https://igv.org/genomes/locus.php?genome=$GENOME$&name=$FEATURE$',
    coords: 0
};

async function searchFeatures(browser, name) {

    const searchConfig = browser.searchConfig || DEFAULT_SEARCH_CONFIG;
    let feature;

    name = name.toUpperCase();
    const searchableTracks = browser.tracks.filter(t => t.searchable);
    for (let track of searchableTracks) {
        const feature = await track.search(name);
        if (feature) {
            return feature
        }
    }

    // If still not found try webservice, if enabled
    if (browser.config && false !== browser.config.search) {
        try {
            feature = await searchWebService(browser, name, searchConfig);
            return feature    // Might be undefined
        } catch (error) {
        }
    }

}

/**
 * Return an object representing the locus of the given string.  Object is of the form
 * {
 *   chr,
 *   start,
 *   end,
 *   locusSearchString,
 *   gene,
 *   snp
 * }
 * @param browser
 * @param string
 * @returns {Promise<*>}
 */
async function search(browser, string) {

    if (undefined === string || '' === string.trim()) {
        return
    }

    const loci = string.split(' ');

    let list = [];

    const searchForLocus = async (locus) => {

        if (locus.trim().toLowerCase() === "all" || locus === "*") {
            if (browser.genome.wholeGenomeView) {
                const wgChr = browser.genome.getChromosome("all");
                return {chr: "all", start: 0, end: wgChr.bpLength}
            } else {
                return undefined
            }
        }

        let locusObject;
        let chromosome;
        if (locus.includes(":")) {
            locusObject = parseLocusString(locus, browser.isSoftclipped());
            if (locusObject) {
                chromosome = await browser.genome.loadChromosome(locusObject.chr);
            }
        }

        if (!chromosome) {

            // Not a locus string
            locusObject = undefined;

            // Not a locus string, search track annotations
            const feature = await searchFeatures(browser, locus);
            if(feature) {
                locusObject = {
                    chr: feature.chr,
                    start: feature.start,
                    end: feature.end,
                    name: (feature.name || locus).toUpperCase(),

                };
            }

            // If still not found assume locus is a chromosome name.
            if (!locusObject) {
                chromosome = await browser.genome.loadChromosome(locus);
                if (chromosome) {
                    locusObject = {chr: chromosome.name};
                }
            }
        }

        // Force load chromosome here (a side effect, but neccessary to do this in an async function so it's available)
        if (locusObject) {
            chromosome = chromosome || await browser.genome.loadChromosome(locusObject.chr);
            locusObject.chr = chromosome.name;    // Replace possible alias with canonical name
            if (locusObject.start === undefined && locusObject.end === undefined) {
                locusObject.start = 0;
                locusObject.end = chromosome.bpLength;
            }
        }

        return locusObject
    };

    for (let locus of loci) {
        const locusObject = await searchForLocus(locus);
        if (locusObject) {
            list.push(locusObject);
        }
    }

    // If nothing is found, consider possibility that loci name itself has spaces
    if (list.length === 0) {
        const locusObject = await searchForLocus(string.replaceAll(' ', '+'));
        if (locusObject) {
            list.push(locusObject);
        }
    }

    return 0 === list.length ? undefined : list
}

/**
 * Parse a locus string of the form <chr>:<start>-<end>.  If string does not parse as a locus return undefined
 *
 * @param locus
 * @param isSoftclipped
 * @returns {{start: number, end: number, chr: *}|undefined|{start: number, chr: *}}
 */
function parseLocusString(locus, isSoftclipped = false) {

    // Check for tab delimited locus string
    const tabTokens = locus.split('\t');
    if (tabTokens.length > 2) {
        // Possibly a tab-delimited locus
        try {
            const chr = tabTokens[0];//  browser.genome.getChromosomeName(tabTokens[0])
            const start = parseInt(tabTokens[1].replace(/,/g, ''), 10) - 1;
            const end = parseInt(tabTokens[2].replace(/,/g, ''), 10);
            if (!isNaN(start) && !isNaN(end)) {
                return {chr, start, end}
            }
        } catch (e) {
            // Not a tab delimited locus, apparently, but not really an error as that was a guess
        }
    }

    const a = locus.split(':');
    const locusObject = {chr: a[0]};
    if (a.length > 1) {

        let b = a[1].split('-');
        if (b.length > 2) {
            // Allow for negative coordinates, which is possible if showing alignment soft clips
            if (a[1].startsWith('-')) {
                const i = a[1].indexOf('-', 1);
                if (i > 0) {
                    const t1 = a[1].substring(0, i);
                    const t2 = a[1].substring(i + 1);
                    b = [t1, t2];
                }
            } else {
                return undefined
            }
        }

        let numeric;
        numeric = b[0].replace(/,/g, '');
        if (isNaN(numeric)) {
            return undefined
        }

        locusObject.start = parseInt(numeric, 10) - 1;
        locusObject.end = locusObject.start + 1;

        if (1 === b.length) {
            // Don't clamp coordinates if single coordinate is supplied.
            locusObject.start -= 20;
            locusObject.end += 20;
        }

        if (2 === b.length) {
            numeric = b[1].replace(/,/g, '');
            if (isNaN(numeric)) {
                return undefined
            } else {
                locusObject.end = parseInt(numeric, 10);
            }

            // Allow negative coordinates only if browser is softclipped, i.e. there is at least alignment track with softclipping on
            if (locusObject.start < 0 && !isSoftclipped) {
                const delta = -extent.start;
                locusObject.start += delta;
                locusObject.end += delta;
            }
        }
    }

    return locusObject

}

async function searchWebService(browser, locus, searchConfig) {

    let path = searchConfig.url.replace("$FEATURE$", locus.toUpperCase());
    if (path.indexOf("$GENOME$") > -1) {
        path = path.replace("$GENOME$", (browser.genome.id ? browser.genome.id : "hg19"));
    }
    const options = searchConfig.timeout ? {timeout: searchConfig.timeout} : undefined;
    const result = await igvxhr$1.loadString(path, options);

    return processSearchResult(browser, result, searchConfig)
}

function processSearchResult(browser, result, searchConfig) {

    let results;

    if ('plain' === searchConfig.type) {
        results = parseSearchResults(browser, result);
    } else {
        results = JSON.parse(result);
    }

    if (searchConfig.resultsField) {
        results = results[searchConfig.resultsField];
    }

    if (!results || 0 === results.length) {
        return undefined

    } else {

        const chromosomeField = searchConfig.chromosomeField || "chromosome";
        const startField = searchConfig.startField || "start";
        const endField = searchConfig.endField || "end";
        const coords = searchConfig.coords || 1;


        let result;
        if (Array.isArray(results)) {
            // Ignoring all but first result for now
            // TODO -- present all and let user select if results.length > 1
            result = results[0];
        } else {
            // When processing search results from Ensembl REST API
            // Example: https://rest.ensembl.org/lookup/symbol/macaca_fascicularis/BRCA2?content-type=application/json
            result = results;
        }

        if (!(result.hasOwnProperty(chromosomeField) && (result.hasOwnProperty(startField)))) {
            console.error("Search service results must include chromosome and start fields: " + result);
        }

        const chr = result[chromosomeField];
        let start = result[startField] - coords;
        let end = result[endField];
        if (undefined === end) {
            end = start + 1;
        }

        const locusObject = {chr, start, end};

        // Some GTEX hacks
        result.type ? result.type : "gene";
        if (searchConfig.geneField && searchConfig.snpField) {
            const name = result[searchConfig.geneField] || result[searchConfig.snpField];  // Should never have both
            if (name) locusObject.name = name.toUpperCase();
        }

        return locusObject
    }
}

/**
 * Parse the igv line-oriented (non json) search results.
 * NOTE:  currently, and probably permanently,  this will always be a single line
 * Example
 *    EGFR    chr7:55,086,724-55,275,031    refseq
 *
 */
function parseSearchResults(browser, data) {

    const results = [];
    const lines = splitLines$3(data);

    for (let line of lines) {

        const tokens = line.split("\t");

        if (tokens.length >= 3) {
            const locusTokens = tokens[1].split(":");
            const rangeTokens = locusTokens[1].split("-");
            results.push({
                chromosome: browser.genome.getChromosomeName(locusTokens[0].trim()),
                start: parseInt(rangeTokens[0].replace(/,/g, '')),
                end: parseInt(rangeTokens[1].replace(/,/g, '')),
                name: tokens[0].toUpperCase()
            });
        }
    }

    return results

}

class QTLTrack extends TrackBase {

    constructor(config, browser) {

        super(config, browser);
    }

    init(config) {
        super.init(config);

        this.type = "qtl";
        this.name = config.name;

        const min = config.minLogP || config.min;
        const max = config.maxLogP || config.max;
        this.dataRange = {
            min: min !== undefined ? min :  3.5,
            max: max || 25
        };
        if (!max) {
            this.autoscale = true;
        } else {
            this.autoscale = config.autoscale;
        }
        this.autoscalePercentile = (config.autoscalePercentile === undefined ? 98 : config.autoscalePercentile);


        this.background = config.background;    // No default
        this.divider = config.divider || "rgb(225,225,225)";
        this.dotSize = config.dotSize || 2;
        this.height = config.height || 100;
        this.disableButtons = config.disableButtons;

        this.featureSource = FeatureSource(config, this.browser.genome);
    }

    paintAxis(ctx, pixelWidth, pixelHeight) {

        const yScale = (this.dataRange.max - this.dataRange.min) / pixelHeight;

        const font = {
            'font': 'normal 10px Arial',
            'textAlign': 'right',
            'strokeStyle': "black"
        };

        IGVGraphics.fillRect(ctx, 0, 0, pixelWidth, pixelHeight, {'fillStyle': "rgb(255, 255, 255)"});

        // Determine a tick spacing such that there is at least 10 pixels between ticks

        const n = Math.ceil((this.dataRange.max - this.dataRange.min) * 10 / pixelHeight);

        for (let p = 4; p <= this.dataRange.max; p += n) {

            // TODO: Dashes may not actually line up with correct scale. Ask Jim about this

            const ref = 0.85 * pixelWidth;
            const x1 = ref - 5;
            const x2 = ref;
            const y = pixelHeight - (p - this.dataRange.min) / yScale;

            IGVGraphics.strokeLine(ctx, x1, y, x2, y, font); // Offset dashes up by 2 pixel

            if (y > 8) {
                IGVGraphics.fillText(ctx, p, x1 - 1, y + 2, font);
            } // Offset numbers down by 2 pixels;
        }

        font['textAlign'] = 'center';

        IGVGraphics.fillText(ctx, "-log10(pvalue)", pixelWidth / 4, pixelHeight / 2, font, {rotate: {angle: -90}});

    };

    async getFeatures(chr, start, end) {
        const visibilityWindow = this.visibilityWindow;
        const features = await this.featureSource.getFeatures({chr, start, end, visibilityWindow});
        return features
    }

    draw(options) {

        const {context, referenceFrame, pixelWidth, pixelHeight} = options;

        if (this.background) {
            IGVGraphics.fillRect(context, 0, 0, pixelWidth, pixelHeight, {'fillStyle': this.background});
        }
        IGVGraphics.strokeLine(context, 0, pixelHeight - 1, pixelWidth, pixelHeight - 1, {'strokeStyle': this.divider});

        referenceFrame.feature && referenceFrame.feature.match(/RS[0-9]+/);

        const drawEqtls = (drawSelected) => {

            const radius = drawSelected ? 2 * this.dotSize : this.dotSize;
            const bpStart = options.bpStart;
            const yScale = (this.dataRange.max - this.dataRange.min) / pixelHeight;

            for (let eqtl of options.features) {

                const px = (eqtl.start - bpStart + 0.5) / options.bpPerPixel;
                if (px < 0) continue
                else if (px > pixelWidth) break

                const phenotype = eqtl.phenotype;
                let isSelected;
                // 3 modes, specific qtl, snp, or phenotype (e.g. gene) focused.
                if (this.browser.qtlSelections.qtl) {
                    isSelected = compareQTLs(this.browser.qtlSelections.qtl, eqtl);
                } else if (this.browser.qtlSelections.snps.size > 0) {
                    isSelected = this.browser.qtlSelections.hasSnp(eqtl.snp) &&
                        this.browser.qtlSelections.hasPhenotype(phenotype);
                } else {
                    isSelected = this.browser.qtlSelections.hasPhenotype(phenotype);
                }

                if (!drawSelected || isSelected) {

                    var mLogP = -Math.log(eqtl.pValue) / Math.LN10;
                    if (mLogP >= this.dataRange.min) {
                        let capped;
                        if (mLogP > this.dataRange.max) {
                            mLogP = this.dataRange.max;
                            capped = true;
                        } else {
                            capped = false;

                        }

                        const py = Math.max(0 + radius, pixelHeight - Math.round((mLogP - this.dataRange.min) / yScale));
                        eqtl.px = px;
                        eqtl.py = py;
                        eqtl.radius = radius;

                        let color;
                        if (drawSelected && isSelected) {
                            color = this.browser.qtlSelections.colorForGene(phenotype);
                            IGVGraphics.setProperties(context, {fillStyle: color, strokeStyle: "black"});
                        } else {
                            color = capped ? "rgb(150, 150, 150)" : "rgb(180, 180, 180)";
                            IGVGraphics.setProperties(context, {fillStyle: color, strokeStyle: color});
                        }

                        IGVGraphics.fillCircle(context, px, py, radius);
                        IGVGraphics.strokeCircle(context, px, py, radius);
                    }
                }
            }
        };

        // Draw in two passes, with "selected" eqtls drawn last
        drawEqtls(false);
        drawEqtls(true);

    }

    /**
     * Return "popup data" for feature @ genomic location.  Data is an array of key-value pairs
     */
    popupData(clickState, features) {

        if (features === undefined) features = clickState.viewport.cachedFeatures;
        if (!features || features.length === 0) return []
        const tissue = this.name;
        const popupData = [];

        for (let feature of this._clickedFeatures(clickState, features)) {
            if (popupData.length > 0) {
                popupData.push('<hr/>');
            }
            if (typeof feature.popupData === 'function') {
                popupData.push(...feature.popupData(clickState));
            } else {
                popupData.push(
                    {name: "snp id", value: feature.snp},
                    {name: "gene id", value: feature.gencodeId},
                    {name: "gene name", value: feature.geneSymbol},
                    {name: "p value", value: feature.pValue},
                    {name: "tissue", value: tissue});
            }
        }
        return popupData
    }

    _clickedFeatures(clickState, features) {
        const dist = (f, cs) => {
            return Math.sqrt((f.px - cs.canvasX) * (f.px - cs.canvasX) + (f.py - cs.canvasY) * (f.py - cs.canvasY))
        };

        const tolerance = 6;
        const candidateFeatures = features.filter(feature => dist(feature, clickState) < tolerance);

        if (candidateFeatures.length > 1) {
            candidateFeatures.sort((a, b) => dist(a, clickState) - dist(b, clickState));
            const firstD = dist(candidateFeatures[0], clickState);
            return candidateFeatures.filter(f => dist(f, clickState) <= firstD)
        } else {
            return candidateFeatures
        }

    }

    contextMenuItemList(clickState) {

        const menuData = [];

        // We use the cached features rather than method to avoid async load.  If the
        // feature is not already loaded this won't work,  but the user wouldn't be mousing over it either.
        const features = clickState.viewport.cachedFeatures;
        if (features) {
            const clickedFeatures = this._clickedFeatures(clickState, features);
            if (clickedFeatures.length > 0) {
                menuData.push({
                    label: `Highlight associated features`,
                    click: async () => {
                        this.browser.qtlSelections.clear();
                        for (let f of clickedFeatures) {
                            this.browser.qtlSelections.qtl = f;
                            this.browser.qtlSelections.addPhenotype(f.phenotype);
                        }
                        this.browser.repaintViews();
                    }
                });
                menuData.push('<hr\>');
            }
        }
        return menuData

    }

    menuItemList() {
        const menuItems = [];
        menuItems.push(...this.numericDataMenuItems());
        menuItems.push('<hr/>');

        function dialogPresentationHandler(ev) {

            this.browser.inputDialog.present({
                label: 'Search for snp or phenotype',
                value: '',
                callback: async (term) => {

                    if (term) {
                        term = term.trim().toUpperCase();

                        // Find qtls from this track matching either snp or phenotype
                        const matching = f => {
                            return ((f.phenotype && f.phenotype.toUpperCase()) === term || (f.snp && f.snp.toUpperCase() === term)) &&
                                -Math.log(f.pValue) / Math.LN10 > this.dataRange.min
                        };
                        let matchingFeatures = await this.featureSource.findFeatures(matching);
                        if (matchingFeatures.length == 0) {
                            // Possibly move to another genomic locus containing the search term
                            const found = await this.browser.search(term);
                            if (found) {
                                matchingFeatures = await this.featureSource.findFeatures(matching);
                            }
                        }

                        // Add found features to qtlSelections and compute spanned genomic region.  Currently
                        // this only works for cis-QTLs
                        let chr, start, end;
                        if (matchingFeatures.length > 0) {
                            this.browser.qtlSelections.clear();
                            const genes = new Set();
                            chr = matchingFeatures[0].chr;
                            start = matchingFeatures[0].start;
                            end = matchingFeatures[0].end;
                            for (let qtl of matchingFeatures) {
                                if (qtl.snp && qtl.snp.toUpperCase() === term) {
                                    this.browser.qtlSelections.addSnp(qtl.snp);
                                }
                                this.browser.qtlSelections.addPhenotype(qtl.phenotype);
                                genes.add(qtl.phenotype);

                                if (qtl.chr === chr) {
                                    start = Math.min(start, qtl.start);
                                    end = Math.max(end, qtl.end);
                                }
                            }


                            // possibly Expand region to bring phenotype in view
                            const canonicalChrName = this.browser.genome.getChromosomeName(chr);
                            for (let term of genes) {
                                const feature = await searchFeatures(this.browser, term);
                                if (feature) {
                                    if (canonicalChrName === this.browser.genome.getChromosomeName(feature.chr)) {
                                        start = Math.min(start, feature.start);
                                        end = Math.max(end, feature.end);
                                    }
                                }
                            }

                            const flanking = Math.floor(0.1 * (end - start));
                            start = Math.max(0, start - flanking);
                            end += flanking;

                            await this.browser.search(`${chr}:${start}-${end}`);
                        }
                    }
                }
            }, ev);
        }

        menuItems.push({label: 'Search for...', dialog: dialogPresentationHandler});

        return menuItems
    }

    doAutoscale(featureList) {

        let max = this.config.max || 25; // default
        if(featureList.length > 0) {
            const values = featureList.map(eqtl => -Math.log(eqtl.pValue) / Math.LN10);
            values.sort((a, b) => a-b);
            const k = Math.floor(values.length * (this.autoscalePercentile / 100));
            max = values[k];
        }
        this.dataRange.max = Math.max(max, 10);
        return this.dataRange
    }

}


function compareQTLs(a, b) {
    return a.chr === b.chr && a.start === b.start && a.pValue === b.pValue
}

/**
 * Colors used for coding omosomes
 */

const GWASColors = {
    "X": "rgb(204, 153, 0)",
    "Y": "rgb(153, 204, 0)",
    "Un": "darkGray)",
    "1": "rgb(80, 80, 255)",
    "2": "rgb(206, 61, 50)",
    "2a": "rgb(210, 65, 55)",
    "2b": "rgb(215, 70, 60)",
    "3": "rgb(116, 155, 88)",
    "4": "rgb(240, 230, 133)",
    "5": "rgb(70, 105, 131)",
    "6": "rgb(186, 99, 56)",
    "7": "rgb(93, 177, 221)",
    "8": "rgb(128, 34, 104)",
    "9": "rgb(107, 215, 107)",
    "10": "rgb(213, 149, 167)",
    "11": "rgb(146, 72, 34)",
    "12": "rgb(131, 123, 141)",
    "13": "rgb(199, 81, 39)",
    "14": "rgb(213, 143, 92)",
    "15": "rgb(122, 101, 165)",
    "16": "rgb(228, 175, 105)",
    "17": "rgb(59, 27, 83)",
    "18": "rgb(205, 222, 183)",
    "19": "rgb(97, 42, 121)",
    "20": "rgb(174, 31, 99)",
    "21": "rgb(231, 199, 111)",
    "22": "rgb(90, 101, 94)",
    "23": "rgb(204, 153, 0)",
    "24": "rgb(153, 204, 0)",
    "25": "rgb(51, 204, 0)",
    "26": "rgb(0, 204, 51)",
    "27": "rgb(0, 204, 153)",
    "28": "rgb(0, 153, 204)",
    "29": "rgb(10, 71, 255)",
    "30": "rgb(71, 117, 255)",
    "31": "rgb(255, 194, 10)",
    "32": "rgb(255, 209, 71)",
    "33": "rgb(153, 0, 51)",
    "34": "rgb(153, 26, 0)",
    "35": "rgb(153, 102, 0)",
    "36": "rgb(128, 153, 0)",
    "37": "rgb(51, 153, 0)",
    "38": "rgb(0, 153, 26)",
    "39": "rgb(0, 153, 102)",
    "40": "rgb(0, 128, 153)",
    "41": "rgb(0, 51, 153)",
    "42": "rgb(26, 0, 153)",
    "43": "rgb(102, 0, 153)",
    "44": "rgb(153, 0, 128)",
    "45": "rgb(214, 0, 71)",
    "46": "rgb(255, 20, 99)",
    "47": "rgb(0, 214, 143)",
    "48": "rgb(20, 255, 177)"
};

//  aliasing
for (let key of Object.keys(GWASColors)) {
    const altName = "chr" + key;
    GWASColors[altName] = GWASColors[key];
}

// romanizing
for(let a = 1; a <= 48; a++) {
    if(a === 10) continue   // Don't overide "X"
    const roman = romanize(a);
    GWASColors[roman] = GWASColors[a.toString()];
}


function romanize (num) {
    if (!+num) return false;
    var digits = String(+num).split('');
    var key = ['','C','CC','CCC','CD','D','DC','DCC','DCCC','CM',
        '','X','XX','XXX','XL','L','LX','LXX','LXXX','XC',
        '','I','II','III','IV','V','VI','VII','VIII','IX'];
    var roman = '', i = 3;
    while (i--) roman = (key[+digits.pop() + (i * 10)] || '') + roman;
    return Array(+digits.join('') + 1).join('M') + roman;
}

const DEFAULT_POPOVER_WINDOW = 100000000;

//const type = "gwas";

class GWASTrack extends TrackBase {

    constructor(config, browser) {

        super(config, browser);
    }

    init(config) {

        super.init(config);

        this.useChrColors = config.useChrColors === undefined ? true : config.useChrColors;
        this.trait = config.trait;
        this.posteriorProbability = config.posteriorProbability;
        this.valueProperty = "bed" === config.format ? "score" : "value";
        this.height = config.height || 100;   // The preferred height
        this.autoscale = config.autoscale;
        this.autoscalePercentile = config.autoscalePercentile === undefined ? 98 : config.autoscalePercentile;
        this.background = config.background;    // No default
        this.divider = config.divider || "rgb(225,225,225)";
        this.dotSize = config.dotSize || 3;
        this.popoverWindow = (config.popoverWindow === undefined ? DEFAULT_POPOVER_WINDOW : config.popoverWindow);

        // Color settings
        if (this.useChrColors) {
            this.colorScale = new ColorTable(config.colorTable || GWASColors);
        } else if (config.color) {
            this.colorScale = new ConstantColorScale(config.color);
        } else {
            this.colorScale =
                new BinnedColorScale(config.colorScale ||
                    {
                        thresholds: [5e-8, 5e-4, 0.5],
                        colors: ["rgb(255,50,50)", "rgb(251,100,100)", "rgb(251,170,170)", "rgb(227,238,249)"],
                    });
        }

        this.featureSource = FeatureSource(config, this.browser.genome);
    }

    async postInit() {

        if (typeof this.featureSource.getHeader === "function") {
            this.header = await this.featureSource.getHeader();
            if (this.disposed) return   // This track was removed during async load
        }

        // Set properties from track line
        if (this.header) {
            this.setTrackProperties(this.header);   // setTrackProperties defined in TrackBase
        }

        // Set initial range if specfied, unless autoscale == true
        if (!this.autoscale) {
            if (this.posteriorProbability) {
                this.dataRange = {
                    min: this.config.min === undefined ? 0 : this.config.min,
                    max: this.config.max === undefined ? 1 : this.config.max
                };
            } else {
                this.dataRange = {
                    min: this.config.min === undefined ? 0 : this.config.min,
                    max: this.config.max === undefined ? 25 : this.config.max
                };
            }
        }

        this._initialColor = this.color || this.constructor.defaultColor;
        this._initialAltColor = this.altColor || this.constructor.defaultColor;

        return this

    }


    get supportsWholeGenome() {
        return true
    }

    async getFeatures(chr, start, end) {
        const visibilityWindow = this.visibilityWindow;
        return this.featureSource.getFeatures({chr, start, end, visibilityWindow})
    }

    draw(options) {

        const featureList = options.features;
        const ctx = options.context;
        const pixelWidth = options.pixelWidth;
        const pixelHeight = options.pixelHeight;
        if (this.background) {
            IGVGraphics.fillRect(ctx, 0, 0, pixelWidth, pixelHeight, {'fillStyle': this.background});
        }
        IGVGraphics.strokeLine(ctx, 0, pixelHeight - 1, pixelWidth, pixelHeight - 1, {'strokeStyle': this.divider});

        if (featureList) {

            const bpPerPixel = options.bpPerPixel;
            const bpStart = options.bpStart;
            const bpEnd = bpStart + pixelWidth * bpPerPixel + 1;
            for (let variant of featureList) {
                const pos = variant.start;
                if (pos < bpStart) continue
                if (pos > bpEnd) break

                let val;
                if (this.posteriorProbability) {
                    val = variant[this.valueProperty];
                } else {
                    const pvalue = variant[this.valueProperty];
                    if (!pvalue) continue
                    val = -Math.log10(pvalue);
                }

                const colorKey = this.useChrColors ?
                    variant._f ? variant._f.chr : variant.chr :
                    val;

                const color = this.colorScale.getColor(colorKey);
                const yScale = (this.dataRange.max - this.dataRange.min) / pixelHeight;
                const px = Math.round((pos - bpStart) / bpPerPixel);
                const py = Math.max(this.dotSize, pixelHeight - Math.round((val - this.dataRange.min) / yScale));

                if (color) {
                    IGVGraphics.setProperties(ctx, {fillStyle: color, strokeStyle: "black"});
                }
                IGVGraphics.fillCircle(ctx, px, py, this.dotSize);
                variant.px = px;
                variant.py = py;
            }
        }
    }

    paintAxis(ctx, pixelWidth, pixelHeight) {

        IGVGraphics.fillRect(ctx, 0, 0, pixelWidth, pixelHeight, {'fillStyle': "rgb(255, 255, 255)"});
        var font = {
            'font': 'normal 10px Arial',
            'textAlign': 'right',
            'strokeStyle': "black"
        };

        const yScale = (this.dataRange.max - this.dataRange.min) / pixelHeight;
        if (this.posteriorProbability) {
            const n = 0.1;
            for (let p = this.dataRange.min; p < this.dataRange.max; p += n) {
                const yp = pixelHeight - Math.round((p - this.dataRange.min) / yScale);
                IGVGraphics.strokeLine(ctx, 45, yp - 2, 50, yp - 2, font); // Offset dashes up by 2 pixel
                IGVGraphics.fillText(ctx, p.toFixed(1), 44, yp + 2, font); // Offset numbers down by 2 pixels;
            }
        } else {
            const n = Math.ceil((this.dataRange.max - this.dataRange.min) * 10 / pixelHeight);
            for (let p = this.dataRange.min; p < this.dataRange.max; p += n) {
                const yp = pixelHeight - Math.round((p - this.dataRange.min) / yScale);
                IGVGraphics.strokeLine(ctx, 45, yp, 50, yp, font); // Offset dashes up by 2 pixel
                IGVGraphics.fillText(ctx, Math.floor(p), 44, yp + 4, font); // Offset numbers down by 2 pixels;
            }
        }

        font['textAlign'] = 'center';
        if (this.posteriorProbability) {
            IGVGraphics.fillText(ctx, "PPA", pixelWidth / 2, pixelHeight / 2, font, {rotate: {angle: -90}});
        } else {
            IGVGraphics.fillText(ctx, "-log10(pvalue)", pixelWidth / 2, pixelHeight / 2, font, {rotate: {angle: -90}});
        }
    }

    popupData(clickState, features) {

        if (features === undefined) features = clickState.viewport.cachedFeatures;

        let data = [];
        const track = clickState.viewport.trackView.track;

        if (features) {
            let count = 0;
            for (let f of features) {
                const xDelta = Math.abs(clickState.canvasX - f.px);
                const yDelta = Math.abs(clickState.canvasY - f.py);
                if (xDelta < this.dotSize && yDelta < this.dotSize) {
                    if (count > 0) {
                        data.push("<HR/>");
                    }
                    if (count == 5) {
                        data.push("...");
                        break
                    }
                    f = f._f || f;     // Extract "real" feature from potential psuedo feature (e.g. whole genome)
                    if (typeof f.popupData === 'function') {
                        data = data.concat(f.popupData());
                    } else {
                        const value = f[this.valueProperty];
                        const chr = f.chr;
                        const pos = numberFormatter$1(f.start + 1);
                        data.push({name: 'chromosome', value: chr});
                        data.push({name: 'position', value: pos});
                        data.push({name: 'name', value: f.name});
                        if (track.posteriorProbability) {
                            data.push({name: 'posterior probability', value: value});
                        } else {
                            data.push({name: 'pValue', value: value});
                        }
                    }
                    count++;
                }
            }
        }

        return data
    }

    menuItemList() {
        return this.numericDataMenuItems()
    }

    doAutoscale(featureList) {

        if (featureList.length > 0) {
            // posterior probabilities are treated without modification, but we need to take a negative logarithm of P values
            const valueProperty = this.valueProperty;
            const posterior = this.posteriorProbability;
            const features =
                featureList.map(function (feature) {
                    const v = feature[valueProperty];
                    return {value: posterior ? v : -Math.log(v) / Math.LN10}
                });
            this.dataRange = doAutoscale(features);

        } else {
            // No features -- pick something reasonable for PPAs and p-values
            if (this.posteriorProbability) {
                this.dataRange = {min: this.config.min || 0, max: this.config.max || 1};
            } else {
                this.dataRange = {min: this.config.max || 25, max: this.config.min || 0};
            }
        }

        return this.dataRange
    }

}

const X_PIXEL_DIFF_THRESHOLD = 1;

class GCNVTrack extends TrackBase {

    constructor(config, browser) {
        super(config, browser);
    }

    init(config) {
        super.init(config);

        this.autoscale = config.autoscale || config.max === undefined;
        this.dataRange = {
            min: config.min || 0,
            max: config.max
        };

        this.windowFunction = config.windowFunction || "mean";
        this.paintAxis = paintAxis;
        this.graphType = config.graphType || "bar";

        //hack to avoid creating feature source multiple times.  If config.type is not set the file must be read
        //to determine type, which results in creation of a feature source.
        if (config._featureSource) {
            this.featureSource = config._featureSource;
            delete config._featureSource;
        } else {
            this.featureSource = FeatureSource(this.config, this.browser.genome);
        }

        // Visibility window hardcoded to -1  (== whole chromosome).  Draw method needs feature beyond current view,
        // when/if this is resolved visibilityWindow can be used.
        this.visibilityWindow = -1;
        this.featureSource.visibilityWindow = this.visibilityWindow;
    }

    async postInit() {

        if (typeof this.featureSource.getHeader === "function") {
            this.header = await this.featureSource.getHeader();
            if(this.disposed) return;   // This track was removed during async load
            this.sampleKeys = this.header.columnNames.slice(3);

            // Set generic properties from track line
            this.setTrackProperties(this.header);   // setTrackProperties defined in TrackBase

            // set option to highlight sample track line on click
            if (this.header.hasOwnProperty("clickToHighlight")) {
                let colour = this.header["clickToHighlight"];
                this.config.clickToHighlight = colour;
                this.config.samplesClickedToHighlight = {};
            }

            // enables onlyHandleClicksForHighlightedSamples to be set from file header
            if (this.header.hasOwnProperty("onlyHandleClicksForHighlightedSamples")) {
                this.config.onlyHandleClicksForHighlightedSamples = true;
            }

            // Special track line properties
            if (this.header.hasOwnProperty("highlight")) {
                this.config.highlightSamples = {};
                let v = this.header["highlight"];
                if (!Array.isArray(v)) v = [v];
                for (let h of v) {
                    const tokens = h.split(";");
                    if (tokens.length === 2) {
                        this.config.highlightSamples[tokens[0]] = tokens[1];
                    }

                }
            }
        }

        this._initialColor = this.color || this.constructor.defaultColor;
        this._initialAltColor = this.altColor || this.constructor.defaultColor;

    }

    menuItemList() {
        return this.numericDataMenuItems()
    }

    async getFeatures(chr, start, end) {
        const chrFeatures = await this.featureSource.getFeatures({
            chr,
            start: 0,
            end: Number.MAX_SAFE_INTEGER,
            visibilityWindow: this.visibilityWindow
        });
        let prevIndex = undefined;
        let nextIndex = undefined;
        for (let i = 1; i < chrFeatures.length - 1; i++) {
            if (prevIndex === undefined && chrFeatures[i].end > start) {
                prevIndex = i - 1;
            }
            if (nextIndex === undefined && chrFeatures[i].start > end) {
                nextIndex = i + 1;
                break
            }
        }
        if (prevIndex === undefined) prevIndex = 0;
        if (nextIndex === undefined) nextIndex = chrFeatures.length;
        return chrFeatures.slice(prevIndex, nextIndex)
    }

    draw(options) {

        const {features, context, bpPerPixel, bpStart, pixelWidth, pixelHeight} = options;

        ///let baselineColor;
        //if (typeof self.color === "string" && self.color.startsWith("rgb(")) {
        //    baselineColor = IGVColor.addAlpha(self.color, 0.1);
        //}

        const yScale = (yValue) => {
            return ((this.dataRange.max - yValue) / (this.dataRange.max - this.dataRange.min)) * pixelHeight
        };

        const getX = function (bpPosition) {
            let x = Math.floor((bpPosition - bpStart) / bpPerPixel);
            if (isNaN(x)) console.warn('isNaN(x). feature start ' + numberFormatter$1(bpPosition) +
                ' bp start ' + numberFormatter$1(bpStart));
            return x
        };

        const drawGuideLines = (options) => {
            if (this.config.hasOwnProperty('guideLines')) {
                for (let line of this.config.guideLines) {
                    if (line.hasOwnProperty('color') && line.hasOwnProperty('y') && line.hasOwnProperty('dotted')) {
                        let y = yScale(line.y);
                        let props = {
                            'strokeStyle': line['color'],
                            'strokeWidth': 2
                        };
                        if (line['dotted']) IGVGraphics.dashedLine(options.context, 0, y, options.pixelWidth, y, 5, props);
                        else IGVGraphics.strokeLine(options.context, 0, y, options.pixelWidth, y, props);
                    }
                }
            }
        };

        if (features && features.length > 0) {

            if (this.dataRange.min === undefined) this.dataRange.min = 0;

            // Max can be less than min if config.min is set but max left to autoscale. If that's the case there is
            // nothing to paint.
            if (this.dataRange.max > this.dataRange.min) {
                const highlightSamples = this.config.highlightSamples;
                const onlyHandleClicksForHighlightedSamples = this.config.onlyHandleClicksForHighlightedSamples;
                const clickToHighlight = this.config.clickToHighlight;

                let previousEnd = -1;
                let previousValues = {};

                let highlightConnectorLines = [];
                let highlightFeatureLines = [];

                // clickDetectorCache allows fast retrieval of whether a mouse click hits a rendered line segment
                // by storing lists of rendered line segments, keyed by their right x coordinate in canvas pixel space.
                // this cache is regenerated on every draw.
                this.clickDetectorCache = {};

                for (let feature of features) {
                    const x1 = getX(feature.start);
                    const x2 = getX(feature.end);
                    const previousX = previousEnd >= 0 ? getX(previousEnd) : x1;

                    if (isNaN(x1) || isNaN(x2)) continue
                    // if ((x1 - previousX < X_PIXEL_DIFF_THRESHOLD) && (x2 - x1 < X_PIXEL_DIFF_THRESHOLD)) continue;

                    this.clickDetectorCache[x1] = [];
                    this.clickDetectorCache[x2] = [];
                    for (let i = 0; i < feature.values.length; i++) {
                        const sampleName = this.sampleKeys[i];
                        const value = feature.values[i];
                        const y = yScale(value);
                        if (x1 - previousX >= X_PIXEL_DIFF_THRESHOLD) {
                            const previousValue = previousValues[sampleName];
                            const previousY = yScale(previousValue);
                            const highlightColor = highlightSamples && highlightSamples[sampleName];


                            if (highlightColor) {
                                highlightConnectorLines.push([previousX, previousY, x1, y, highlightColor]);
                            } else if (clickToHighlight && sampleName in this.config.samplesClickedToHighlight) {
                                highlightConnectorLines.push([previousX, previousY, x1, y, this.config.samplesClickedToHighlight[sampleName]]);
                            } else {
                                IGVGraphics.strokeLine(context, previousX, previousY, x1, y, {strokeStyle: '#D9D9D9'});
                            }
                            if (!onlyHandleClicksForHighlightedSamples || sampleName in highlightSamples) {
                                this.clickDetectorCache[x1].push([previousX, previousY, x1, y, sampleName, highlightColor || 'gray']);
                            }
                        }

                        if (x2 - x1 >= X_PIXEL_DIFF_THRESHOLD) {
                            const highlightColor = highlightSamples && highlightSamples[sampleName];
                            if (highlightColor) {
                                highlightFeatureLines.push([x1, y, x2, y, highlightColor]);
                            } else if (clickToHighlight && sampleName in this.config.samplesClickedToHighlight) {
                                highlightFeatureLines.push([x1, y, x2, y, this.config.samplesClickedToHighlight[sampleName]]);
                            } else {
                                IGVGraphics.strokeLine(context, x1, y, x2, y, {strokeStyle: 'gray'});
                            }
                            if (!onlyHandleClicksForHighlightedSamples || sampleName in highlightSamples) {
                                this.clickDetectorCache[x2].push([x1, y, x2, y, sampleName, highlightColor || 'gray']);
                            }
                        }

                        previousValues[sampleName] = value;

                        //IGVGraphics.fillCircle(ctx, px, y, pointSize / 2, {"fillStyle": color, "strokeStyle": color});
                        //IGVGraphics.fillRect(ctx, x, y, width, height, {fillStyle: color});
                    }
                    previousEnd = feature.end;
                }

                for (let f of highlightConnectorLines) {
                    IGVGraphics.strokeLine(context, f[0], f[1], f[2], f[3], {strokeStyle: f[4], lineWidth: 1.3});
                }
                for (let f of highlightFeatureLines) {
                    IGVGraphics.strokeLine(context, f[0], f[1], f[2], f[3], {strokeStyle: f[4], lineWidth: 2});
                }

                /*
                // If the track includes negative values draw a baseline
                if (this.dataRange.min < 0) {
                    const basepx = (self.dataRange.max / (this.dataRange.max - this.dataRange.min)) * options.pixelHeight;
                    IGVGraphics.strokeLine(context, 0, basepx, options.pixelWidth, basepx, {strokeStyle: baselineColor});
                }
                */
            }
        }

        drawGuideLines(options);
    }

    doAutoscale(features) {

        let min, max;
        if (features.length > 0) {
            min = Number.MAX_VALUE;
            max = -Number.MAX_VALUE;

            features.forEach(function (feature) {
                min = Math.min(min, ...feature.values);
                max = Math.max(max, ...feature.values);
            });

            min -= 0.01;
            max += 0.01;
        } else {
            // No features -- default
            min = 0;
            max = 100;
        }

        return {min: min, max: max}
    }

    clickedFeatures(clickState) {
        //console.warn('click', clickState.canvasX, clickState.canvasY, clickState)

        const BOUNDING_BOX_PADDING = 10;
        const MIN_DISTANCE_TO_SEGMENT = 5;

        const clickX = clickState.canvasX;
        const clickY = clickState.canvasY;

        let key = null;
        for (key of Object.keys(this.clickDetectorCache)) {
            key = parseInt(key);
            if (key >= clickX) {
                break
            }
        }


        if (key) {
            let closestDistanceSoFar = Number.MAX_VALUE;
            let closestResult = [];
            const segments = this.clickDetectorCache[key];
            for (let segment of segments) {
                const x1 = segment[0];
                const x2 = segment[2];
                if (clickX < x1 || clickX > x2) return []

                const y1 = segment[1];
                const y2 = segment[3];

                if ((clickY < Math.min(y1, y2) - BOUNDING_BOX_PADDING) || (clickY > Math.max(y1, y2) + BOUNDING_BOX_PADDING)) continue

                const distance = distanceToLine(clickX, clickY, x1, y1, x2, y2);
                if (distance < closestDistanceSoFar) {
                    closestResult = [{'name': segment[4], 'color': segment[5]}];
                    closestDistanceSoFar = distance;
                    //console.warn('closest:', 'name', segment[4], 'color', segment[5], distance);
                }
            }

            if (closestDistanceSoFar < MIN_DISTANCE_TO_SEGMENT) {
                // clickToHighlight set, add sample to dict of clicked lines
                if (this.config.clickToHighlight) {
                    if (closestResult[0]['name'] in this.config.samplesClickedToHighlight) {
                        // clicked sample already highlighted => remove if clicked again
                        delete this.config.samplesClickedToHighlight[closestResult[0]['name']];
                    } else if (this.config.clickToHighlight === 'any') {
                        var colourList = [
                            'red', 'darkblue', 'green', 'teal', 'olivedrab',
                            'orange', 'maroon', 'purple', 'blue', 'gold',
                        ];
                        var colour = colourList[Math.floor(Math.random() * (colourList.length + 1))];
                        this.config.samplesClickedToHighlight[closestResult[0]['name']] = colour;
                    } else {
                        this.config.samplesClickedToHighlight[closestResult[0]['name']] = this.config.clickToHighlight;
                    }
                    this.trackView.repaintViews();  // prompt redraw to change colour of clicked sample
                }
                return closestResult
            }
        }

        return []
    }

    popupData(clickState, features) {

        if(features === undefined) features = this.clickedFeatures(clickState);

        const items = [];
        features.forEach(function (f) {
            for (let property of Object.keys(f)) {
                if (isSimpleType(f[property])) {
                    items.push({name: property, value: f[property]});
                }
            }
        });

        return items
    }

    get supportsWholeGenome() {
        return false
    }
}


function distanceToLine(x, y, ax, ay, bx, by) {
    /*
        Finds distance between point (x, y) and line defined by points (ax, ay) (bx, by)
        based on http://mathworld.wolfram.com/Point-LineDistance2-Dimensional.html
    */

    const bx_minus_ax = bx - ax;
    const by_minus_ay = by - ay;
    const v = Math.abs(bx_minus_ax * (ay - y) - (ax - x) * by_minus_ay);
    const r = Math.sqrt(bx_minus_ax * bx_minus_ax + by_minus_ay * by_minus_ay);

    const distance = r > 0 ? v / r : 0;
    //console.warn('Check if', x, y, 'is within', ax, ay, bx, by, '. Distance from line: ', distance);

    return distance
}

class RnaStructTrack extends TrackBase {

    static defaults = {
        height: 300,
        theta: Math.PI / 2,
        arcOrientation: "UP",

    }

    constructor(config, browser) {

        super(config, browser);

        // Backward compatibility hack, arcOrientation was previously a boolean, now a string
        if (config.arcOrientation === false) {
            this.arcOrientation = "DOWN";
        } else if (config.arcOrientation === true) {
            this.arcOrientation = "UP";
        } else if (config.arcOrientation) {
            this.arcOrientation = config.arcOrientation.toUpperCase();
        } else {
            this.arcOrientation = "UP";
        }

        if ("bp" === config.format) {
            this.featureSource = new RNAFeatureSource(config, browser.genome);
        } else {
            this.featureSource = new TextFeatureSource(config, browser.genome);
        }
    }

    async getFeatures(chr, start, end) {
        const visibilityWindow = this.visibilityWindow;
        return this.featureSource.getFeatures({chr, start, end, visibilityWindow})
    }

    draw(options) {

        const ctx = options.context;

        const theta = Math.PI / 2;
        const pixelWidth = options.pixelWidth;
        const pixelHeight = options.pixelHeight;
        options.viewportWidth;
        const bpPerPixel = options.bpPerPixel;
        const bpStart = options.bpStart;
        const xScale = bpPerPixel;
        const orientation = "UP" === this.arcOrientation;

        IGVGraphics.fillRect(ctx, 0, options.pixelTop, pixelWidth, pixelHeight, {'fillStyle': "rgb(255, 255, 255)"});

        const featureList = options.features;

        if (featureList) {

            // Sort by score -- draw lowest scored features first
            sortByScore(featureList, 1);

            for (let feature of featureList) {

                if (feature.startLeft) {

                    let sl = Math.round((feature.startLeft - bpStart) / xScale);
                    let sr = Math.round((feature.startRight - bpStart) / xScale);
                    let el = Math.round((feature.endLeft - bpStart) / xScale);
                    let er = Math.round((feature.endRight - bpStart) / xScale);

                    ctx.fillStyle = feature.color;
                    ctx.strokeStyle = feature.color;
                    ctx.beginPath();

                    // First arc
                    let x1 = (sl + er) / 2;
                    let r1 = (er - sl) / 2;
                    let y1 = this.height;
                    let sa = Math.PI + (Math.PI / 2 - theta);
                    let ea = 2 * Math.PI - (Math.PI / 2 - theta);

                    if (orientation) {
                        ctx.arc(x1, y1, r1, sa, ea);
                        ctx.lineTo(el, y1);
                    } else {
                        y1 = 0;
                        ctx.arc(x1, y1, r1, ea, sa);
                        ctx.lineTo(er, y1);
                    }

                    // Second arc
                    const x2 = (sr + el) / 2;
                    const r2 = (el - sr) / 2;
                    const y2 = y1;                        // Only for theta == pi/2

                    if (orientation) {
                        ctx.arc(x2, y2, r2, ea, sa, true);
                        ctx.lineTo(sl, y2);
                    } else {
                        ctx.arc(x2, y2, r2, sa, ea, true);
                        ctx.lineTo(el, y2);
                    }

                    ctx.stroke();
                    ctx.fill();

                    feature.drawState = {x1: x1, y1: y1, r1: r1, x2: x2, y2: y2, r2: r2, sa: sa, ea: ea};
                } else {
                    let s = Math.round((feature.start - bpStart) / xScale);
                    let e = Math.round((feature.end - bpStart) / xScale);

                    ctx.strokeStyle = feature.color;

                    ctx.beginPath();

                    // First arc
                    let x = (s + e) / 2;
                    let r = (e - s) / 2;
                    let y = this.height;
                    let sa = Math.PI + (Math.PI / 2 - theta);
                    let ea = 2 * Math.PI - (Math.PI / 2 - theta);

                    if (orientation) {
                        ctx.arc(x, y, r, sa, ea);
                    } else {
                        y = 0;
                        ctx.arc(x, y, r, ea, sa);
                    }

                    ctx.stroke();

                    feature.drawState = {x1: x, y1: y, r1: r, sa: sa, ea: ea};

                }

            }
        }
    }

    clickedFeatures(clickState) {

        const features = super.clickedFeatures(clickState);

        const clicked = [];

        // Sort by score in descending order   (opposite order than drawn)
        sortByScore(features, -1);

        for (let f of features) {
            const ds = f.drawState;

            // Distance from arc radius, or outer arc for type ".bp"
            const dx1 = (clickState.canvasX - ds.x1);
            const dy1 = (clickState.canvasY - ds.y1);
            const d1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
            const outerLim = ds.r1 + 3;


            let d2;
            let innerLim;
            if (ds.x2 === undefined) {
                d2 = d1;
                innerLim = ds.r1 - 3;

            } else {
                const dx2 = (clickState.canvasX - ds.x2);
                const dy2 = (clickState.canvasY - ds.y2);
                d2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
                innerLim = ds.r2 - 3;
            }


            // Between outer and inner arcs, with some tolerance
            if (d1 < outerLim && d2 > innerLim) {
                clicked.push(f);
                break
            }
        }
        return clicked
    }

    popupData(clickState, features) {

        if (features === undefined) features = this.clickedFeatures(clickState);

        if (features && features.length > 0) {

            return this.extractPopupData(features[0], this.getGenomeId())

        }
    }

    menuItemList() {
        return [
            {
                name: "Toggle arc direction",
                click: function toggleArcDirectionHandler() {
                    this.arcOrientation = "UP" === this.arcOrientation ? "DOWN" : "UP";
                    this.trackView.repaintViews();
                }
            }
        ]
    }
}

function sortByScore(featureList, direction) {

    featureList.sort(function (a, b) {
        const s1 = a.score === undefined ? -Number.MAX_VALUE : a.score;
        const s2 = b.score === undefined ? -Number.MAX_VALUE : b.score;
        const d = direction === undefined ? 1 : direction;

        return d * (s1 - s2)
    });
}


class RNAFeatureSource {

    constructor(config, genome) {
        this.config = config;
        this.genome = genome;
    }

    async getFeatures({chr, start, end, bpPerPixel, visibilityWindow}) {


        const genome = this.genome;

        if (!this.featureCache) {

            const options = buildOptions(this.config);

            const data = await igvxhr$1.loadByteArray(this.config.url, options);

            if (!data) return null

            const dataWrapper = getDataWrapper$1(data);

            let header = true;
            let line;
            const colors = [];
            const descriptors = [];
            const features = [];
            const chrNames = new Set();

            while ((line = dataWrapper.nextLine()) !== undefined) {

                const tokens = line.split('\t');

                if (header && line.startsWith("color:")) {
                    const color = "rgb(" + tokens[1] + "," + tokens[2] + "," + tokens[3] + ")";
                    colors.push(color);
                    if (tokens.length > 4) {
                        descriptors.push(tokens[4]);
                    }
                    // TODO - use label
                } else {
                    header = false;

                    const chr = tokens[0];
                    const startLeftNuc = Number.parseInt(tokens[1]) - 1;
                    const startRightNuc = Number.parseInt(tokens[2]) - 1;
                    const endLeftNuc = Number.parseInt(tokens[3]);
                    const endRightNuc = Number.parseInt(tokens[4]);
                    var colorIdx = Number.parseInt(tokens[5]);
                    const color = colors[colorIdx];


                    let feature;
                    if (startLeftNuc <= endRightNuc) {
                        feature = {
                            chr: chr,
                            startLeft: Math.min(startLeftNuc, startRightNuc),
                            startRight: Math.max(startLeftNuc, startRightNuc),
                            endLeft: Math.min(endLeftNuc, endRightNuc),
                            endRight: Math.max(endLeftNuc, endRightNuc),
                            color: color,
                            score: colorIdx
                        };
                    } else {
                        feature = {
                            chr: chr,
                            startLeft: Math.min(endLeftNuc, endRightNuc),
                            startRight: Math.max(endLeftNuc, endRightNuc),
                            endLeft: Math.min(startLeftNuc, startRightNuc),
                            endRight: Math.max(startLeftNuc, startRightNuc),
                            color: color,
                            score: colorIdx
                        };
                    }

                    feature.start = feature.startLeft;
                    feature.end = feature.endRight;

                    if (descriptors.length > colorIdx) {
                        feature.description = descriptors[colorIdx];
                    }

                    chrNames.add(chr);
                    features.push(feature);
                }
            }

            this.chromAliasManager = new ChromAliasManager(chrNames, genome);

            this.featureCache = new FeatureCache$1(features);

        }

        const queryChr = this.chromAliasManager ? await this.chromAliasManager.getAliasName(chr) : chr;

        return this.featureCache.queryFeatures(queryChr, start, end)


    }
}

/**
 * Class represents an ideogram of a chromsome cytobands.  It is used for the header of a track panel.
 *
 */
class IdeogramTrack {
    constructor(browser) {
        this.browser = browser;
        this.type = 'ideogram';
        this.id = 'ideogram';
        this.height = browser.config.showCytobandNames ? 20 : 16;
        this.order = Number.MIN_SAFE_INTEGER;
        this.disableButtons = true;
        this.ignoreTrackMenu = true;

        // Check whether we should show the cytoband names in the ideogram
        this.showCytobandNames = browser.config.showCytobandNames;
    }

    computePixelHeight(ignore) {
        return this.height
    }

    draw({context, referenceFrame, pixelWidth, pixelHeight, features}) {

        const chr = referenceFrame.chr;
        const chromosome = referenceFrame.genome.getChromosome(chr);

        if (undefined === chromosome || pixelWidth <= 0 || pixelHeight <= 0 || 'all' === chr.toLowerCase()) {
            return
        }

        const stainColors = [];

        drawIdeogram({
            ctx: context,
            features,
            chr,
            referenceFrame,
            genome: referenceFrame.genome,
            width: pixelWidth,
            height: pixelHeight,
            stainColors,
            showCytobandNames: this.showCytobandNames
        });

        const widthBP = Math.round(referenceFrame.bpPerPixel * pixelWidth);
        const xBP = referenceFrame.start;

        // Total chromosome length can be > chromosome.bpLength for partial fastas.
        let chrLength = chromosome.bpLength;
        const cytobands = referenceFrame.genome.getCytobands(chr);
        if (cytobands && cytobands.length > 0 && cytobands[cytobands.length - 1].end) {
            chrLength = Math.max(chrLength, cytobands[cytobands.length - 1].end);
            chromosome.bpLength = chrLength;   // Correct bp length, bit of a hack
        }

        if (widthBP < chrLength) {

            const percentWidth = widthBP / chrLength;
            const percentX = xBP / chrLength;

            let x = Math.floor(percentX * pixelWidth);
            let ww = Math.floor(percentWidth * pixelWidth);

            x = Math.max(0, x);
            x = Math.min(pixelWidth - ww, x);

            // Push current context
            context.save();

            // Draw red box
            context.strokeStyle = "red";
            context.lineWidth = (ww < 2) ? 1 : 2;

            const xx = x + (context.lineWidth) / 2;
            ww = (ww < 2) ? 1 : ww - context.lineWidth;

            const yy = context.lineWidth / 2;
            const hh = pixelHeight - context.lineWidth;

            context.strokeRect(xx, yy, ww, hh);

            // Pop current context
            context.restore();
        }
    }

    dispose() {
        this.trackView = undefined;
    }
}

function drawIdeogram({ctx, chr, referenceFrame, genome, width, height, stainColors, features, showCytobandNames}) {
    const shim = 1;
    const shim2 = 0.5 * shim;
    const ideogramTop = 0;

    if (undefined === genome) {
        return
    }

    IGVGraphics.fillRect(ctx, 0, 0, width, height, {fillStyle: IGVColor.greyScale(255)});

    const cytobands = features;
    if (cytobands) {

        const center = (ideogramTop + height / 2);

        const xC = [];
        const yC = [];

        if (0 === cytobands.length) {
            return
        }

        // Get chrLength from the cytobands -- chromsome.bpLength might not work for igv-reports fasta files, which
        // contain only a portion of the chromosome sequence
        // *DOESNT WORK* const chrLength = referenceFrame.genome.getChromosome(chr).bpLength;

        const chrLength = cytobands[cytobands.length - 1].end;
        const scale = width / chrLength;

        // round rect clipping path
        ctx.beginPath();
        IGVGraphics.roundRect(ctx, shim2, shim2 + ideogramTop, width - 2 * shim2, height - 2 * shim2, (height - 2 * shim2) / 2, 0, 1);
        ctx.clip();

        for (let i = 0; i < cytobands.length; i++) {

            const cytoband = cytobands[i];
            const start = scale * cytoband.start;
            const end = scale * cytoband.end;

            if (cytoband.type === 'c') {

                if (cytoband.name.charAt(0) === 'p') {
                    xC[0] = start;
                    yC[0] = height + ideogramTop;
                    xC[1] = start;
                    yC[1] = ideogramTop;
                    xC[2] = end;
                    yC[2] = center;
                } else {
                    xC[0] = end;
                    yC[0] = height + ideogramTop;
                    xC[1] = end;
                    yC[1] = ideogramTop;
                    xC[2] = start;
                    yC[2] = center;
                }

                ctx.fillStyle = "rgb(150, 0, 0)";
                ctx.strokeStyle = "rgb(150, 0, 0)";
                IGVGraphics.polygon(ctx, xC, yC, 1, 0);
            } 
            else {
                const backgroundColor = getCytobandColor(stainColors, cytoband);
                ctx.fillStyle = backgroundColor.color;
                IGVGraphics.fillRect(ctx, start, shim + ideogramTop, (end - start), height - 2 * shim);

                if (showCytobandNames) {
                   drawIdeogramCytobandName(ctx, cytoband.name, start, end, ideogramTop, height, backgroundColor.shade);
                }
            }
        }
    }

    // round rect border
    ctx.strokeStyle = IGVColor.greyScale(41);
    IGVGraphics.roundRect(ctx, shim2, shim2 + ideogramTop, width - 2 * shim2, height - 2 * shim2, (height - 2 * shim2) / 2, 0, 1);
}

function drawIdeogramCytobandName(ctx, name, start, end, ideogramTop, height, shade) {
    const padding = 2; // Padding between the rect and the sides of the ideogram

    // Calculate font size to fit the rectangle height and width
    const rectHeight = height - 2 * padding;
    const rectWidth = end - start;

    const maxFontSize = rectHeight - 2 * padding; // Leave some padding for text
    let fontSize = maxFontSize;
    do {
        ctx.font = `${fontSize}px sans-serif`;
        const textWidth = ctx.measureText(name).width;
        if (textWidth <= rectWidth) break;
        fontSize -= 1;
    } while (fontSize > 4); // Minimum font size to avoid infinite loop

    // For safety, we're going to clip the text to the rectangle bounds
    ctx.save(); // Save the current state of the context
    ctx.beginPath();
    ctx.rect(start, padding + ideogramTop, rectWidth, rectHeight);
    ctx.clip();

    // Draw the name of the cytoband, centered within the rectangle
    const centerX = start + rectWidth / 2.0;
    const centerY = padding + ideogramTop + rectHeight / 2.0 + 1;

    // Determine the luminance
    let luminance;
    if (shade !== null) {
        luminance = 0.2126 * shade + 0.7152 * shade + 0.0722 * shade; // Simplified since R=G=B=shade
    } else {
        luminance = 0.2126 * 150 + 0.7152 * 10 + 0.0722 * 10; // Hardcoded for "acen"
    }

    // Choose text color based on luminance
    const textColor = luminance < 128 ? "white" : "black";

    IGVGraphics.fillText(ctx, name, centerX, centerY, {
        fillStyle: textColor,
        textAlign: "center",
        textBaseline: "middle",
        font: `${fontSize}px sans-serif` // Ensure proper font size
    });

    ctx.restore(); // Restore the context to remove clipping
}

function getCytobandColor(colors, data) {
    if (data.type === 'c') { // centromere: "acen"
        return { color: "rgb(150, 10, 10)", shade: null }; // Shade is not relevant here
    } 
    else {
        let stain = data.stain; // Stain value
        let shade = 230; // Default shade for 'g'

        if (data.type === 'p') {
            shade = Math.floor(230 - stain / 100.0 * 230);
        }

        // Cache the color if not already stored
        var c = colors[shade];
        if (!c) {
            c = "rgb(" + shade + "," + shade + "," + shade + ")";
            colors[shade] = c;
        }

        return { color: c, shade }; // Return both the color and shade
    }
}

let JUNCTION_MOTIF_PALETTE = new PaletteColorTable("Dark2");

// Lock in color-to-motif mapping so it's independent of data loading order. This list may not include all possible
// motif values as this varies depending on the RNA-seq pipeline. The current list is based on STAR v2.4 docs.
const someMotifValues = ['GT/AG', 'CT/AC', 'GC/AG', 'CT/GC', 'AT/AC', 'GT/AT', 'non-canonical'];
someMotifValues.forEach(motif => {
    JUNCTION_MOTIF_PALETTE.getColor(motif);
});


class SpliceJunctionTrack extends TrackBase {

    static defaults = {
        margin: 10,
        colorByNumReadsThreshold: 5,
        height: 100
    }

    constructor(config, browser) {
        super(config, browser);
    }


    init(config) {

        super.init(config);

        this.type = config.type || 'junctions';

        if (config._featureSource) {
            this.featureSource = config._featureSource;
            delete config._featureSource;
        } else {
            this.featureSource = config.featureSource ?
                config.featureSource :
                FeatureSource(config, this.browser.genome);
        }

    }

    async postInit() {

        if (typeof this.featureSource.getHeader === "function") {
            this.header = await this.featureSource.getHeader();
            if (this.disposed) return   // This track was removed during async load
        }

        // Set properties from track line
        if (this.header) {
            this.setTrackProperties(this.header);
        }

        if (this.visibilityWindow === undefined && typeof this.featureSource.defaultVisibilityWindow === 'function') {
            this.visibilityWindow = await this.featureSource.defaultVisibilityWindow();
        }

        this._initialColor = this.color || this.constructor.defaultColor;
        this._initialAltColor = this.altColor || this.constructor.defaultColor;

        return this

    }

    get supportsWholeGenome() {
        return false
    }

    async getFeatures(chr, start, end, bpPerPixel) {
        const visibilityWindow = this.visibilityWindow;
        return this.featureSource.getFeatures({chr, start, end, bpPerPixel, visibilityWindow})
    };


    /**
     * The required height in pixels required for the track content.   This is not the visible track height, which
     * can be smaller (with a scrollbar) or larger.
     *
     * @param features
     * @returns {*}
     */
    computePixelHeight(features) {
        return this.height
    };

    draw(options) {

        const featureList = options.features;
        const ctx = options.context;
        const bpPerPixel = options.bpPerPixel;
        const bpStart = options.bpStart;
        const pixelWidth = options.pixelWidth;
        const pixelHeight = options.pixelHeight;
        const bpEnd = bpStart + pixelWidth * bpPerPixel + 1;


        if (!this.isMergedTrack) {
            IGVGraphics.fillRect(ctx, 0, options.pixelTop, pixelWidth, pixelHeight, {'fillStyle': "rgb(255, 255, 255)"});
        }

        if (featureList) {


            // rendering context with values that only need to be computed once per render, rather than for each splice junction
            const junctionRenderingContext = {};

            junctionRenderingContext.referenceFrame = options.viewport.referenceFrame;
            junctionRenderingContext.referenceFrameStart = junctionRenderingContext.referenceFrame.start;
            junctionRenderingContext.referenceFrameEnd = junctionRenderingContext.referenceFrameStart +
                junctionRenderingContext.referenceFrame.toBP(options.viewport.getWidth());

            // For a given viewport, records where features that are < 2px in width have been rendered already.
            // This prevents wasteful rendering of multiple such features onto the same pixels.
            junctionRenderingContext.featureZoomOutTracker = {};

            for (let feature of featureList) {
                if (feature.end < bpStart) continue
                if (feature.start > bpEnd) break
                this.renderJunction(feature, bpStart, bpPerPixel, pixelHeight, ctx, junctionRenderingContext);
            }

        }

    };

    /**
     *
     * @param feature
     * @param bpStart  genomic location of the left edge of the current canvas
     * @param xScale  scale in base-pairs per pixel
     * @param pixelHeight  pixel height of the current canvas
     * @param ctx  the canvas 2d context
     */
    renderJunction(feature, bpStart, xScale, pixelHeight, ctx, junctionRenderingContext) {
        // cache whether this junction is rendered or filtered out. Use later to exclude non-rendered junctions from click detection.
        feature.isVisible = false;

        const junctionLeftPx = Math.round((feature.start - bpStart) / xScale);
        const junctionRightPx = Math.round((feature.end - bpStart) / xScale);
        const junctionMiddlePx = (junctionLeftPx + junctionRightPx) / 2;
        if (junctionRightPx - junctionLeftPx <= 3) {
            if (junctionMiddlePx in junctionRenderingContext.featureZoomOutTracker) {
                return
            }
            junctionRenderingContext.featureZoomOutTracker[junctionMiddlePx] = true;
        }

        // TODO: cache filter and pixel calculations by doing them earlier when features are initially parsed?
        if (this.config.hideAnnotatedJunctions && feature.attributes.annotated_junction === "true") {
            return
        }
        if (this.config.hideUnannotatedJunctions && feature.attributes.annotated_junction === "false") {
            return
        }
        if (this.config.hideMotifs && this.config.hideMotifs.includes(feature.attributes.motif)) {
            return
        }
        if (this.config.hideStrand === feature.strand) {
            return
        }

        // check if splice junction is inside viewport
        if (this.config.minJunctionEndsVisible) {
            let numJunctionEndsVisible = 0;
            if (feature.start >= junctionRenderingContext.referenceFrameStart && feature.start <= junctionRenderingContext.referenceFrameEnd) {
                numJunctionEndsVisible += 1;
            }
            if (feature.end >= junctionRenderingContext.referenceFrameStart && feature.end <= junctionRenderingContext.referenceFrameEnd) {
                numJunctionEndsVisible += 1;
            }
            if (numJunctionEndsVisible < this.config.minJunctionEndsVisible) {
                return
            }
        }

        let uniquelyMappedReadCount;
        let multiMappedReadCount;
        let totalReadCount;
        if (feature.attributes.uniquely_mapped) {
            uniquelyMappedReadCount = parseInt(feature.attributes.uniquely_mapped);
            if (uniquelyMappedReadCount < this.config.minUniquelyMappedReads) {
                return
            }
            multiMappedReadCount = parseInt(feature.attributes.multi_mapped);
            totalReadCount = uniquelyMappedReadCount + multiMappedReadCount;
            if (totalReadCount < this.config.minTotalReads) {
                return
            }
            if (totalReadCount > 0 && multiMappedReadCount / totalReadCount > this.config.maxFractionMultiMappedReads) {
                return
            }
            if (feature.attributes.maximum_spliced_alignment_overhang && parseInt(feature.attributes.maximum_spliced_alignment_overhang) < this.config.minSplicedAlignmentOverhang) {
                return
            }
        }

        let numSamplesWithThisJunction;
        if (feature.attributes.num_samples_with_this_junction) {
            numSamplesWithThisJunction = parseInt(feature.attributes.num_samples_with_this_junction);
            if (this.config.minSamplesWithThisJunction && numSamplesWithThisJunction < this.config.minSamplesWithThisJunction) {
                return
            }
            if (this.config.maxSamplesWithThisJunction && numSamplesWithThisJunction > this.config.maxSamplesWithThisJunction) {
                return
            }
            if (feature.attributes.num_samples_total) {
                feature.attributes.percent_samples_with_this_junction = 100 * numSamplesWithThisJunction / Number(feature.attributes.num_samples_total);
                if (this.config.minPercentSamplesWithThisJunction) {
                    if (feature.attributes.percent_samples_with_this_junction < this.config.minPercentSamplesWithThisJunction ||
                        feature.attributes.percent_samples_with_this_junction > this.config.maxPercentSamplesWithThisJunction) {
                        return
                    }
                }
            }
        }

        const py = this.margin;
        const rowHeight = pixelHeight;

        const cy = py + 0.5 * rowHeight;
        let topY = py;
        const bottomY = py + rowHeight;
        const bezierBottomY = bottomY - 10;

        // draw the junction arc
        const bezierControlLeftPx = (junctionLeftPx + junctionMiddlePx) / 2;
        const bezierControlRightPx = (junctionMiddlePx + junctionRightPx) / 2;

        let lineWidth = 1;
        if (feature.attributes.line_width) {
            lineWidth = Number(feature.attributes.line_width);
        } else {
            if (this.config.thicknessBasedOn === undefined || this.config.thicknessBasedOn === 'numUniqueReads') {
                lineWidth = uniquelyMappedReadCount;
            } else if (this.config.thicknessBasedOn === 'numReads') {
                lineWidth = totalReadCount;
            } else if (this.config.thicknessBasedOn === 'numSamplesWithThisJunction') {
                if (numSamplesWithThisJunction !== undefined) {
                    lineWidth = numSamplesWithThisJunction;
                }
            }
            lineWidth = 1 + Math.log(lineWidth + 1) / Math.log(12);
        }

        let bounceHeight;
        if (this.config.bounceHeightBasedOn === undefined || this.config.bounceHeightBasedOn === 'random') {
            // randomly but deterministically stagger topY coordinates to reduce overlap
            bounceHeight = (feature.start + feature.end) % 7;
        } else if (this.config.bounceHeightBasedOn === 'distance') {
            bounceHeight = 6 * (feature.end - feature.start) / (junctionRenderingContext.referenceFrameEnd - junctionRenderingContext.referenceFrameStart);
        } else if (this.config.bounceHeightBasedOn === 'thickness') {
            bounceHeight = 2 * lineWidth;
        }
        topY += rowHeight * Math.max(7 - bounceHeight, 0) / 10;

        let color;
        if (feature.attributes.color) {
            color = feature.attributes.color;  // Explicit setting
        } else if (this.config.colorBy === undefined || this.config.colorBy === 'numUniqueReads') {
            color = uniquelyMappedReadCount > this.config.colorByNumReadsThreshold ? 'blue' : '#AAAAAA';  // color gradient?
        } else if (this.config.colorBy === 'numReads') {
            color = totalReadCount > this.config.colorByNumReadsThreshold ? 'blue' : '#AAAAAA';
        } else if (this.config.colorBy === 'isAnnotatedJunction') {
            color = feature.attributes.annotated_junction === "true" ? '#b0b0ec' : 'orange';
        } else if (this.config.colorBy === 'strand') {
            color = feature.strand === "+" ? '#b0b0ec' : '#ecb0b0';
        } else if (this.config.colorBy === 'motif') {
            color = JUNCTION_MOTIF_PALETTE.getColor(feature.attributes.motif);
        } else {
            color = '#AAAAAA';
        }

        let label = "";
        if (feature.attributes.label) {
            label = feature.attributes.label.replace(/_/g, " ");
        } else if (this.config.labelWith === undefined || this.config.labelWith === 'uniqueReadCount') {
            //default label
            label = uniquelyMappedReadCount;
        } else if (this.config.labelWith === 'totalReadCount') {
            label = totalReadCount;
        } else if (this.config.labelWith === 'numSamplesWithThisJunction') {
            if (numSamplesWithThisJunction !== undefined) {
                label = numSamplesWithThisJunction;
            }
        } else if (this.config.labelWith === 'percentSamplesWithThisJunction') {
            if (feature.attributes.percent_samples_with_this_junction !== undefined) {
                label = feature.attributes.percent_samples_with_this_junction.toFixed(0) + '%';
            }
        } else if (this.config.labelWith === 'motif') {
            if (feature.attributes.motif !== undefined) {
                label += feature.attributes.motif;
            }
        }

        if (this.config.labelWithInParen === 'uniqueReadCount') {
            label += ' (' + uniquelyMappedReadCount + ')';
        } else if (this.config.labelWithInParen === 'totalReadCount') {
            label += ' (' + totalReadCount + ')';
        } else if (this.config.labelWithInParen === 'multiMappedReadCount') {
            if (multiMappedReadCount > 0) {
                label += ' (+' + multiMappedReadCount + ')';
            }
        } else if (this.config.labelWithInParen === 'numSamplesWithThisJunction') {
            if (numSamplesWithThisJunction !== undefined) {
                label += ' (' + numSamplesWithThisJunction + ')';
            }
        } else if (this.config.labelWithInParen === 'percentSamplesWithThisJunction') {
            if (feature.attributes.percent_samples_with_this_junction !== undefined) {
                label += ' (' + feature.attributes.percent_samples_with_this_junction.toFixed(0) + '%)';
            }
        } else if (this.config.labelWithInParen === 'motif') {
            if (feature.attributes.motif !== undefined) {
                label += ` ${feature.attributes.motif}`;
            }
        }

        // data source: STAR splice junctions (eg. SJ.out.tab file converted to bed).
        // .bed "name" field used to store unique + multi-mapped read counts, so:
        // feature.score:  unique spanning read counts
        // feature.name:   unique + multi-mapped spanning read counts
        //example feature:  { chr: "chr17", start: 39662344, end: 39662803, name: "59", row: 0, score: 38, strand: "+"}
        feature.isVisible = true;
        ctx.beginPath();
        ctx.moveTo(junctionLeftPx, bezierBottomY);
        ctx.bezierCurveTo(bezierControlLeftPx, topY, bezierControlRightPx, topY, junctionRightPx, bezierBottomY);

        ctx.lineWidth = lineWidth;
        ctx.strokeStyle = color;
        ctx.stroke();

        const drawArrowhead = (ctx, x, y, size) => {
            //TODO draw better arrow heads: https://stackoverflow.com/questions/21052972/curved-thick-arrows-on-canvas
            ctx.beginPath();
            ctx.moveTo(x, y);
            ctx.lineTo(x - size / 2, y - size);
            ctx.lineTo(x + size / 2, y - size);
            ctx.lineTo(x, y);
            ctx.closePath();
            ctx.fill();
        };

        if (feature.attributes.left_shape || feature.attributes.right_shape) {
            ctx.fillStyle = color;
            const arrowSize = ctx.lineWidth > 2 ? 10 : 7;
            if (feature.attributes.left_shape) {
                drawArrowhead(ctx, junctionLeftPx, bezierBottomY, arrowSize);
            }
            if (feature.attributes.right_shape) {
                drawArrowhead(ctx, junctionRightPx, bezierBottomY, arrowSize);
            }
        }

        ctx.fillText(label, junctionMiddlePx - ctx.measureText(label).width / 2, (7 * topY + cy) / 8);
    }

    clickedFeatures(clickState) {

        const allFeatures = super.clickedFeatures(clickState);

        return allFeatures.filter(function (feature) {
            return (feature.isVisible && feature.attributes)
        })
    }

    /**
     * Return "popup data" for feature @ genomic location.  Data is an array of key-value pairs
     */
    popupData(clickState, features) {

        if (features === undefined) features = this.clickedFeatures(clickState);
        const genomicLocation = clickState.genomicLocation;

        const data = [];
        for (let feature of features) {

            const featureData = (typeof feature.popupData === "function") ?
                feature.popupData(genomicLocation) :
                this.extractPopupData(feature._f || feature, this.getGenomeId());

            if (featureData) {
                if (data.length > 0) {
                    data.push("<hr/><hr/>");
                }

                Array.prototype.push.apply(data, featureData);
            }
        }

        return data
    }

    /**
     * Called when the track is removed.  Do any needed cleanup here
     */
    dispose() {
        this.trackView = undefined;
    }
}

const defaultColorScaleConfig = {min: 0, max: 3000, color: "rgb(0,0,255)"};

class ShoeboxColorScale {

    constructor(scale) {

        scale = scale || defaultColorScaleConfig;
        this.max = scale.max;
        this.min = scale.min || 0;
        this.cache = [];
        this.nbins = 1000;
        this.binsize = (this.max - this.min) / this.nbins;
        this.updateColor(scale.color || "rgb(0,0,255)");
        this.br = 255;
        this.bg = 255;
        this.bb = 255;

    }

    updateColor(color) {
        const comps = color.substring(4).replace(")", "").split(",");
        if (comps.length === 3) {
            this.r = Number.parseInt(comps[0].trim());
            this.g = Number.parseInt(comps[1].trim());
            this.b = Number.parseInt(comps[2].trim());
        }
        this.cache = [];
    }

    setMinMax(min, max) {
        this.min = min;
        this.max = max;
        this.cache = [];
        this.binsize = (this.max - this.min) / this.nbins;
    }

    getColor(value) {
         if (value <= this.min) return "white"
        else if (value >= this.max) return `rgb(${this.r},${this.g},${this.b})`

        const bin = Math.floor((Math.min(this.max, value) - this.min) / this.binsize);

        if (undefined === this.cache[bin]) {
            const alpha = (value - this.min) / (this.max - this.min);
            const beta = 1 - alpha;
            this.cache[bin] = //`rgba(${this.r},${this.g},${this.b}, ${alpha})`
                `rgb(${ Math.floor(alpha*this.r + beta*this.br)},${Math.floor(alpha*this.g + beta*this.bg)},${Math.floor(alpha*this.b + beta*this.bb)})`;
        }
        return this.cache[bin]
    }

    /**
     *
     * @returns {{min: (*|number), color: string, max}}
     */
    toJson() {
        return {
            min: this.min,
            max: this.max,
            color: `rgb(${this.r},${this.g},${this.b})`
        }
    }

    // For short-term backward compatibility
    static parse(str) {

        const tokens = str.split(",");

        const cs = {
            min: Number.parseFloat(tokens[0]),
            max: Number.parseFloat(tokens[1]),
            color: `${tokens[2]},${tokens[3]},${tokens[4]}`
        };
        return new ShoeboxColorScale(cs)
    }

}

/**
 * Configurable properties
 * min, max   (viewlimits)  - min / max values of the color scale.  Alpha is linearly varied from 100% (min) to 0% (max)
 * color - base color before alpha is applied
 * rowHeight - height of each row
 */

class ShoeboxTrack extends TrackBase {

    static defaults = {
        height: 300,
        rowHeight: 3,
        min: 0.5,
        max: 3,
        scale: 1.0,
        visibilityWindow: 10000,
        supportHiDPI: false,
        startSize: 4,         // Footprint size for the first column of data (first row in track)
        stepSize: 2        // Stepsize for each row in bp for footprint radius
    }

    constructor(config, browser) {
        super(config, browser);
    }

    init(config) {

        super.init(config);

        this.type = "shoebox";

        if (config.max) {
            this.dataRange = {
                min: config.min || 0,
                max: config.max
            };
        }
        // Create featureSource
        const configCopy = Object.assign({}, this.config);
        configCopy.format = 'shoebox';   // bit of a hack
        this.featureSource = FeatureSource(configCopy, this.browser.genome);

        // this.paintAxis = paintAxis
    }


    async postInit() {

        if (typeof this.featureSource.getHeader === "function") {
            this.header = await this.featureSource.getHeader();
            if (this.disposed) return   // This track was removed during async load
        }
        // Set properties from track line
        if (this.header) {
            if (this.header.scale) {
                this.header.scale = Number.parseFloat(this.header.scale);
            }
            this.setTrackProperties(this.header);

            this.rowCount = this.header.firstFeature ? this.header.firstFeature.values.length : 100;
        }

        // Must do the following after setting track properties as they can be overriden via a track line

        // Color settings
        const min = this.dataRange.min;
        const max = this.dataRange.max;
        this.colorScale = new ShoeboxColorScale({min, max, color: this.color});

        // This shouldn't be neccessary
        if (!this.scale) this.scale = 1.0;

        this._initialColor = this.color || this.constructor.defaultColor;
        this._initialAltColor = this.altColor || this.constructor.defaultColor;

    }

    get color() {
        return this._color || "rgb(0,0,255)"
    }

    set color(color) {
        this._color = color;
        if (this.colorScale) {
            this.colorScale.updateColor(color);
        }
    }


    menuItemList() {

        const menuItems = [];

        menuItems.push('<hr/>');

        let element = document.createElement('div');
        element.textContent = 'Set row height';

        const browser = this.browser;

        function dialogHandler(e) {

            const callback = () => {

                const number = parseInt(this.browser.inputDialog.value, 10);

                if (undefined !== number) {

                    browser.sampleNameViewportWidth = undefined;   // TODO, a better way to trigger this

                    const tracks = [];
                    if (this.trackView.track.selected) {
                        tracks.push(...(this.trackView.browser.getSelectedTrackViews().map(({track}) => track)));
                    } else {
                        tracks.push(this);
                    }

                    for (const track of tracks) {
                        track.rowHeight = number;
                        if (track.rowHeight * track.rowCount < track.height) {
                            track.trackView.setTrackHeight(track.rowHeight * track.rowCount, true);
                        }
                        track.trackView.checkContentHeight();
                        track.trackView.repaintViews();
                    }
                }
            };

            const config =
                {
                    label: 'Row Height',
                    value: this.rowHeight,
                    callback
                };

            this.browser.inputDialog.present(config, e);
        }

        menuItems.push({element, dialog: dialogHandler});

        menuItems.push('<hr/>');

        // Data range
        element = document.createElement('div');
        element.textContent = 'Set data range';

        // Note -- menu item handlers must be functions, not arrow functions
        function dataRangeHandler(e) {
            if (this.trackView.track.selected) {
                this.browser.dataRangeDialog.configure(this.trackView.browser.getSelectedTrackViews());
            } else {
                this.browser.dataRangeDialog.configure(this.trackView);
            }
            this.browser.dataRangeDialog.present(e);
        }

        menuItems.push({element, dialog: dataRangeHandler});

        return menuItems
    }

    setDataRange({min, max}) {
        this.dataRange.min = min;
        this.dataRange.max = max;
        this.colorScale.setMinMax(min, max);
        this.trackView.repaintViews();
    }

    async getFeatures(chr, start, end, bpPerPixel) {
        const visibilityWindow = this.visibilityWindow;
        const features = await this.featureSource.getFeatures({chr, start, end, bpPerPixel, visibilityWindow});

        return features
    }

    draw({context, pixelTop, pixelWidth, pixelHeight, features, bpPerPixel, bpStart}) {

        IGVGraphics.fillRect(context, 0, pixelTop, pixelWidth, pixelHeight, {'fillStyle': "rgb(255, 255, 255)"});

        if (features && features.length > 0) {

            const rowHeight = this.rowHeight;
            const pixelBottom = pixelTop + pixelHeight;
            const bpEnd = bpStart + pixelWidth * bpPerPixel + 1;

            const h = rowHeight;

            for (let f of features) {

                // Test for overlap with in-view region
                if (f.end < bpStart || f.start > bpEnd) continue

                // Pixel x values
                const xLeft = Math.floor((f.start - bpStart) / bpPerPixel);
                const xRight = Math.floor((f.end - bpStart) / bpPerPixel);
                const w = Math.max(1, xRight - xLeft);

                // Loop through value array
                for (let i = f.values.length - 1; i >= 0; i--) {

                    const v = f.values[i];                  // / this.scale

                    if (v >= this.dataRange.min) {

                        const row = f.values.length - 1 - i;
                        const y = row * rowHeight;
                        const bottom = y + rowHeight;

                        if (bottom < pixelTop || y > pixelBottom) {
                            continue
                        }

                        const color = this.colorScale.getColor(v);
                        context.fillStyle = color;
                        context.fillRect(xLeft, y, w, h);

                    }

                }
            }
        }

    }

    paintAxis(ctx, pixelWidth, pixelHeight) {

        //IGVGraphics.fillRect(ctx, 0, 0, pixelWidth, pixelHeight, {'fillStyle': "rgb(255, 255, 255)"})
        var font = {
            'font': 'normal 10px Arial',
            'textAlign': 'right',
            'strokeStyle': "black"
        };

        const max = this.startSize + (this.rowCount * this.stepSize);
        const min = this.startSize;
        const yScale = (max - min) / pixelHeight;

        const tickStep = 50;
        for (let p = tickStep; p <= max; p += tickStep) {
            const yp = Math.max(10, pixelHeight - Math.round((p - min) / yScale));
            IGVGraphics.strokeLine(ctx, 35, yp , 40, yp , font);
            if(p > min ) {
                IGVGraphics.fillText(ctx, prettyPrint(p), 30, yp + 4, font); // Offset numbers down by 2 pixels;
            }
        }
        font['textAlign'] = 'center';
        font['font'] = 'normal 10px Arial';
            IGVGraphics.fillText(ctx, "Footprint size (bp)", 10, pixelHeight / 2, font, {rotate: {angle: -90}});
    }

    /**
     * Optional method to compute pixel height to accomodate the list of features.
     *
     * @param features
     * @returns {number}
     */
    computePixelHeight(features) {
        if (!features || features.length === 0) return 0
        return features[0].values.length * this.rowHeight
    }


    clickedFeatures(clickState) {

        const allFeatures = super.clickedFeatures(clickState);
        const y = clickState.y;
        return allFeatures.filter(function (feature) {
            const rect = feature.pixelRect;
            return rect && y >= rect.y && y <= (rect.y + rect.h)
        })
    }

    hoverText(clickState) {
        const features = this.clickedFeatures(clickState);
        if (features && features.length > 0) {
            return `${features[0].sample}: ${features[0].value}`
        }
    }

    popupData(clickState, featureList) {

        if (featureList === undefined) featureList = this.clickedFeatures(clickState);

        const items = [];

        for (let feature of featureList) {

            // Double line divider between features
            if (items.length > 0) {
                items.push('<hr/>');
                items.push('<hr/>');
            }

            // hack for whole genome features, which save the original feature as "_f"
            const f = feature._f || feature;

            const data = (typeof f.popupData === 'function') ?
                f.popupData(this.type, this.browser.genome.id) :
                this.extractPopupData(f);
            Array.prototype.push.apply(items, data);

        }

        return items
    }

    get supportsWholeGenome() {
        return false
    }

    getState() {

        const config = super.getState();
        config.colorScale = this.colorScale.toJson();
        return config

    }

}


function prettyPrint(number) {

    if (Number.isInteger(number)) {
        return number
    } else if (number % 1 === 0) {   // Number can be represented exactly as an integer
        return number
    } else if (Math.abs(number) >= 10) {
        return number.toFixed()
    } else if (Math.abs(number) >= 1) {
        return number.toFixed(1)
    } else if (Math.abs(number) >= 0.1) {
        return number.toFixed(2)
    } else {
        return number.toExponential(1)
    }
}

/**
 * Configurable properties
 * Locus {chr, start, end}
 * url - url to image.   Later url to webservice to fetch image
 */

class ImageTrack extends TrackBase {

    static defaults = {}

    constructor(config, browser) {
        super(config, browser);
    }

    init(config) {

        super.init(config);

        if (!config.images) {
            throw Error("images are required")
        }

        this.locus = config.locus;
        this.type = "image";
        this.resolutionAware = true;
    }


    async postInit() {

        this._images = [];

        for (let i of this.config.images) {
            const img = new Image();
            img.onload = () => {
                i.img = img;
                i.bpPerPixel = (i.end - i.start) / img.width;
                this._images.push(i);
            };
            img.onerror = (err) => {
                console.error(err);
            };
            //if (img.complete) {   //cached image
            //    img.onload()
           // }
            img.src = i.src;
        }

    }

    computePixelHeight(features) {
        return features ? features.height : 0
    }


    menuItemList() {

        const menuItems = [];

        return menuItems
    }


    async getFeatures(chr, start, end, bpPerPixel) {
        // Return  image.  Scaled or not?
        return this.selectImage(chr, start, end, bpPerPixel)
    }

    selectImage(chr, start, end, bpPerPixel) {

        // Select the highest resolution image containing the interval.  If no image contains the interval return
        // the lowest resolution image if it overlaps
        if(this._images.length == 0) {
            return null
        }
        this._images.sort((a, b) => a.bpPerPixel < b.bpPerPixel ? -1 : 1);
        for(let i of this._images) {
            if(i.bpPerPixel > bpPerPixel) {
                return i
            }
        }
        const lowRes = this._images[this._images.length-1];
        if(lowRes.chr === chr) {
            return lowRes
        } else {
            return null
        }
    }

    draw({context, pixelTop, pixelWidth, pixelHeight, features, bpPerPixel, bpStart}) {

        const image = features.img;
        if (image) {
            const nw = image.width;
            const nh = image.height;
            const imageBpPerPixel = (features.end - features.start) / nw;
            const scale = imageBpPerPixel / bpPerPixel;
            const x = (features.start - bpStart) / bpPerPixel;
            context.drawImage(image, x, 0, scale * nw, nh);
        }

    }

    get supportsWholeGenome() {
        return false
    }

}

//import CNVPytorTrack from "./CNVpytor/cnvpytorTrack.js"


const trackFunctions =
    new Map([
        ['ideogram', (config, browser) => new IdeogramTrack(config, browser)],
        ['sequence', (config, browser) => new SequenceTrack(config, browser)],
        ['feature', (config, browser) => new FeatureTrack(config, browser)],
        ['seg', (config, browser) => new SegTrack(config, browser)],
        ['mut', (config, browser) => new SegTrack(config, browser)],
        ['maf', (config, browser) => new SegTrack(config, browser)],
        ['shoebox', (config, browser) => new ShoeboxTrack(config, browser)],
        ['wig', (config, browser) => new WigTrack(config, browser)],
        ['merged', (config, browser) => new MergedTrack(config, browser)],
        ['alignment', (config, browser) => new BAMTrack(config, browser)],
        ['interaction', (config, browser) => new InteractionTrack(config, browser)],
        ['interact', (config, browser) => new InteractionTrack(config, browser)],
        ['variant', (config, browser) => new VariantTrack(config, browser)],
        ['qtl', (config, browser) => new QTLTrack(config, browser)],
        ['eqtl', (config, browser) => new QTLTrack(config, browser)],
        ['gwas', (config, browser) => new GWASTrack(config, browser)],
        ['arc', (config, browser) => new RnaStructTrack(config, browser)],
        ['gcnv', (config, browser) => new GCNVTrack(config, browser)],
        ['junction', (config, browser) => new SpliceJunctionTrack(config, browser)],
        ['blat', (config, browser) => new BlatTrack(config, browser)],
        ['cnvpytor', (config, browser) => new CNVPytorTrack(config, browser)],
        ['image', (config, browser) => new ImageTrack(config, browser)]
    ]);

function knownTrackTypes () {
    return new Set(trackFunctions.keys())
}

/**
 * Return a track of the given type, passing configuration and a point to the IGV "Browser" object to its constructor function*
 * @param type -- track type (string)
 * @param config -- track configuration object
 * @param browser -- the IGV "Browser" object
 * @returns {IdeogramTrack|undefined}
 */
function getTrack (type, config, browser) {

    let trackKey;
    switch (type) {
        case "annotation":
        case "genes":
        case "fusionjuncspan":
        case "snp":
            trackKey = "feature";
            break
        case 'seg':
        case 'maf':
        case 'mut':
            trackKey = 'seg';
            break
        case 'junctions':
        case 'splicejunctions':
            trackKey = 'junction';
            break
        default:
            trackKey = type;
    }

    return trackFunctions.has(trackKey) ?
        trackFunctions.get(trackKey)(config, browser) :
        undefined
}

/**
 * Add a track creator function to the factory lookup table.  Legacy function, superceded by registerTrackClass.
 *
 * @param type
 * @param track
 */
function registerTrackClass(type, trackClass) {
    trackFunctions.set(type, (config, browser) => new trackClass(config, browser));
}



function registerTrackCreatorFunction (type, track) {
    trackFunctions.set(type, track);
}

/**
 * Minimal support for the legacy IGV desktop session format.
 */

class XMLSession {

    constructor(xmlString, knownGenomes) {

        const parser = new DOMParser();
        const xmlDoc = parser.parseFromString(xmlString, "text/xml");

        this.processRootNode(xmlDoc, knownGenomes);

        const resourceElements = xmlDoc.getElementsByTagName("Resource");
        const trackElements = xmlDoc.getElementsByTagName("Track");
        const hasTrackElements = trackElements && trackElements.length > 0;

        const tracks = [];
        this.tracks = tracks;

        const resourceMap = new Map();
        Array.from(resourceElements).forEach(function (r, idx) {
            var config = {
                url: r.getAttribute("path"),
                indexURL: r.getAttribute("index"),
                order: idx
            };
            resourceMap.set(config.url, config);
            if (!hasTrackElements) {
                tracks.push(config);
            }
        });

        // Check for optional Track section
        if (hasTrackElements) {

            Array.from(trackElements).forEach(function (track) {

                const subtracks = track.getElementsByTagName("Track");

                if (subtracks && subtracks.length > 0) {

                    const mergedTrack = {
                        type: 'merged',
                        tracks: []
                    };
                    extractTrackAttributes(track, mergedTrack);

                    tracks.push(mergedTrack);

                    Array.from(subtracks).forEach(function (t) {
                        t.processed = true;
                        const id = t.getAttribute("id");
                        const config = resourceMap.get(id);
                        if (config) {
                            mergedTrack.tracks.push(config);
                            extractTrackAttributes(t, config);
                            config.autoscale = false;
                            mergedTrack.height = config.height;

                            // Add alpha for merged track colors.  Alpha is not recorded by IGV desktop in XML session
                            //const color = t.getAttribute("color");
                            //if (color) {
                            //    config.color = "rgba(" + color + ",0.5)";
                            //}
                        }
                    });
                } else if (!track.processed) {

                    const id = track.getAttribute("id");
                    const res = resourceMap.get(id);
                    if (res) {
                        tracks.push(res);
                        extractTrackAttributes(track, res);
                    }

                }
            });
        }
    }

    processRootNode(xmlDoc, knownGenomes) {

        const elements = xmlDoc.getElementsByTagName("Session");
        if (!elements || elements.length === 0) ;
        const session = elements.item(0);
        const genome = session.getAttribute("genome");
        const locus = session.getAttribute("locus");
        const ucscID = session.getAttribute("ucscID");

        if (knownGenomes && knownGenomes.hasOwnProperty(genome)) {
            this.genome = genome;

        } else {
            this.reference = {
                fastaURL: genome
            };
            if (ucscID) {
                this.reference.id = ucscID;
            }
        }
        if (locus) {
            this.locus = locus;
        }
    }

}


function extractTrackAttributes(track, config) {


    config.name = track.getAttribute("name");

    const color = track.getAttribute("color");
    if (color) {
        config.color = "rgb(" + color + ")";
    }

    const altColor = track.getAttribute("altColor");
    if (color) {
        config.altColor = "rgb(" + altColor + ")";
    }

    const height = track.getAttribute("height");
    if (height) {
        config.height = parseInt(height);
    }

    const autoScale = track.getAttribute("autoScale");
    if (autoScale) {
        config.autoscale = (autoScale === "true");
    }

    const autoscaleGroup = track.getAttribute("autoscaleGroup");
    if (autoscaleGroup) {
        config.autoscaleGroup = autoscaleGroup;
    }

    const windowFunction = track.getAttribute("windowFunction");
    if (windowFunction) {
        config.windowFunction = windowFunction;
    }
    const visWindow = track.getAttribute("visibilityWindow") || track.getAttribute("featureVisibilityWindow");
    if (visWindow) {
        config.visibilityWindow = visWindow;
    }

    const indexed = track.getAttribute("indexed");
    if (indexed) {
        config.indexed = (indexed === "true");
    }

    const normalize = track.getAttribute("normalize");
    if (normalize) {
        config.normalize = normalize === "true";
    }

    const dataRangeCltn = track.getElementsByTagName("DataRange");
    if (dataRangeCltn.length > 0) {
        const dataRange = dataRangeCltn.item(0);
        config.min = Number(dataRange.getAttribute("minimum"));
        config.max = Number(dataRange.getAttribute("maximum"));
        config.logScale = dataRange.getAttribute("type") === "LOG";
    }
}

// Reference frame classes.  Converts domain coordinates (usually genomic) to pixel coordinates

class ReferenceFrame {

    constructor(genome, chr, start, end, bpPerPixel) {
        this.genome = genome;
        this.chr = chr; // this.genome.getChromosomeName(chr)
        this.start = start;
        this.end = end;
        this.bpPerPixel = bpPerPixel;
        this.id = guid$2();
    }

    get center() {
        return (this.start + this.end) / 2
    }

    get locusSearchString() {
        return `${this.chr}:${this.start + 1}-${this.end}`
    }

    /**
     * Extend this frame to accomodate the given locus.  Used th CircularView methods to merge 2 frames.
     * @param locus
     */
    extend(locus) {
        const newStart = Math.min(locus.start, this.start);
        const newEnd = Math.max(locus.end, this.end);
        const ratio = (newEnd - newStart) / (this.end - this.start);
        this.start = newStart;
        this.end = newEnd;
        this.bpPerPixel *= ratio;
    }

    calculateEnd(pixels) {
        return this.start + this.bpPerPixel * pixels
    }

    calculateCenter(pixels) {
        return this.start + this.bpPerPixel * pixels / 2
    }

    calculateBPP(end, pixels) {
        return (end - this.start) / pixels
    }

    set(json) {
        this.chr = json.chr;
        this.start = json.start;
        this.bpPerPixel = json.bpPerPixel;
    }

    toPixels(bp) {
        return bp / this.bpPerPixel
    }

    toBP(pixels) {
        return this.bpPerPixel * pixels
    }

    /**
     * Shift frame by delta in base pairs
     * @param delta
     */
    shift(delta) {
        this.start += delta;
        this.end += delta;
    }

    /**
     * Shift frame by stated pixels.  Return true if view changed, false if not.
     *
     * @param pixels
     * @param clamp -- if true "clamp" shift to prevent panning off edge of chromosome.  This is disabled if "show soft clipping" is on
     * @param viewportWidth
     */
    shiftPixels(pixels, viewportWidth, clamp) {

        const currentStart = this.start;
        const deltaBP = pixels * this.bpPerPixel;

        this.start += deltaBP;

        if (clamp) {
            this.clampStart(viewportWidth);
        }

        this.end = this.start + viewportWidth * this.bpPerPixel;

        return currentStart !== this.start
    }

    clampStart(viewportWidth) {
        // clamp left
        const min = (this.genome.getChromosome(this.chr).bpStart || 0);
        this.start = Math.max(min, this.start);

        // clamp right
        if (viewportWidth) {

            const {bpLength} = this.genome.getChromosome(this.chr);
            const maxStart = bpLength - (viewportWidth * this.bpPerPixel);

            if (this.start > maxStart) {
                this.start = maxStart;
            }
        }
    }

    async zoomWithScaleFactor(browser, scaleFactor, viewportWidth, centerBPOrUndefined) {

        const centerBP = undefined === centerBPOrUndefined ? (this.start + this.toBP(viewportWidth / 2.0)) : centerBPOrUndefined;

        // save initial start and bpp
        const initialStart = this.start;
        const initialBpPerPixel = this.bpPerPixel;
        const bpLength = this.getChromosome().bpLength;
        const bppThreshold = scaleFactor < 1.0 ? browser.minimumBases() / viewportWidth : bpLength / viewportWidth;

        // update bpp
        if (scaleFactor < 1.0) {
            this.bpPerPixel = Math.max(this.bpPerPixel * scaleFactor, bppThreshold);
        } else {
            this.bpPerPixel = Math.min(this.bpPerPixel * scaleFactor, bppThreshold);
        }

        // update start and end
        const widthBP = this.bpPerPixel * viewportWidth;
        this.start = centerBP - 0.5 * widthBP;
        this.clampStart(viewportWidth);

        this.end = this.start + widthBP;

        const viewChanged = initialStart !== this.start || initialBpPerPixel !== this.bpPerPixel;
        if (viewChanged) {
            await browser.updateViews(true);
        }

    }

    getChromosome() {
        return this.genome.getChromosome(this.chr)
    }

    /**
     * Update reference frame based on new viewport width
     * @param {number} viewportWidth - The calculated viewport width
     */
    updateForViewportWidth(viewportWidth) {
        const {chr} = this;
        const {bpLength} = this.getChromosome();
        const viewportWidthBP = this.toBP(viewportWidth);

        // viewportWidthBP > bpLength occurs when locus is full chromosome and user widens browser
        if (GenomeUtils.isWholeGenomeView(chr) || viewportWidthBP > bpLength) {
            this.bpPerPixel = bpLength / viewportWidth;
        } else {
            this.end = this.start + this.toBP(viewportWidth);
        }
    }

    getMultiLocusLabelBPLengthOnly(pixels) {
        const margin = '&nbsp';
        const ss = Math.floor(this.start) + 1;
        const ee = Math.round(this.start + this.bpPerPixel * pixels);
        return `${margin}${this.chr}${margin}${prettyBasePairNumber(ee - ss)}${margin}`
    }

    getMultiLocusLabelLocusOnly(pixels) {
        const margin = '&nbsp';
        const {chr, start, end} = this.getPresentationLocusComponents(pixels);
        return `${margin}${chr}:${start}-${end}${margin}`
    }

    getMultiLocusLabel(pixels) {
        const margin = '&nbsp';
        const {chr, start, end} = this.getPresentationLocusComponents(pixels);
        const ss = Math.floor(this.start) + 1;
        const ee = Math.round(this.start + this.bpPerPixel * pixels);
        return `${margin}${chr}:${start}-${end}${margin}${margin}(${prettyBasePairNumber(ee - ss)})${margin}`
    }

    getPresentationLocusComponents(pixels) {

        if ('all' === this.chr) {
            return {chr: this.chr}
        } else {
            const ss = numberFormatter$1(Math.floor(this.start) + 1);
            const ee = numberFormatter$1(Math.round(this.start + this.bpPerPixel * pixels));

            return {chr: this.chr, start: ss, end: ee}
        }

    }

    getLocusString() {
        if ('all' === this.chr) {
            return 'all'
        } else {
            const chrDisplayName = this.genome.getChromosomeDisplayName(this.chr);
            const ss = numberFormatter$1(Math.floor(this.start) + 1);
            const ee = numberFormatter$1(Math.round(this.end));
            return `${chrDisplayName}:${ss}-${ee}`
        }
    }

    description(blurb) {
    }

    overlaps(interval) {
        return this.chr === interval.chr && this.end >= interval.start && interval.end >= this.start
    }

}

function createReferenceFrameList(loci, genome, browserFlanking, minimumBases, viewportWidth, isSoftclipped) {

    return loci.map(l => {

        const locus = Object.assign({}, l);  // Copy as we might mutate this object

        // If a flanking region is defined, and the search object is a feature (has a name) type, adjust start and end
        if (browserFlanking && locus.name) {
            locus.start = Math.max(0, locus.start - browserFlanking);
            locus.end += browserFlanking;
        }

        // Validate the range.  This potentionally modifies start & end of locus.
        if (!isSoftclipped) {
            const chromosome = genome.getChromosome(locus.chr);
            validateGenomicExtent(chromosome.bpLength, locus, minimumBases);
        }

        const referenceFrame = new ReferenceFrame(
            genome,
            locus.chr,
            locus.start,
            locus.end,
            (locus.end - locus.start) / viewportWidth
        );

        return referenceFrame
    })
}

const _version$1 = "3.5.0";
function version$1() {
    return _version$1
}

const maximumSequenceCountExceeded = "Maximum sequence count exceeded";

class ChromosomeSelectWidget {

    constructor(browser, parent) {

        this.container = div$2({class: 'igv-chromosome-select-widget-container'});
        parent.appendChild(this.container);

        this.select = document.createElement('select');
        this.select.setAttribute('name', 'chromosome-select-widget');
        this.container.appendChild(this.select);

        this.select.addEventListener('change', async () => {
            this.select.blur();
            if (this.select.value !== '' && maximumSequenceCountExceeded !== this.select.value) {

                if (this.select.value.trim().toLowerCase() === "all" || this.select.value === "*") {
                    if (browser.genome.wholeGenomeView) {
                        const wgChr = browser.genome.getChromosome("all");
                        browser.updateLoci([{chr: "all", start: 0, end: wgChr.bpLength}]);
                    }
                } else {
                    const chromosome = await browser.genome.loadChromosome(this.select.value);
                    const locusObject = {chr: chromosome.name};
                    if (locusObject.start === undefined && locusObject.end === undefined) {
                        locusObject.start = 0;
                        locusObject.end = chromosome.bpLength;
                    }
                    browser.updateLoci([locusObject]);
                }
            }
        });

        this.showAllChromosomes = browser.config.showAllChromosomes !== false;   // i.e. default to true
        this.genome = browser.genome;
    }

    show() {
        this.container.style.display = 'flex';
    }

    hide() {
        this.container.style.display = 'none';
    }

    setValue(chrName) {
        this.select.value = this.genome.getChromosomeDisplayName(chrName);
    }

    update(genome) {

        this.genome = genome;

        // Start with explicit chromosome name list
        const list = (genome.wgChromosomeNames) ?
            genome.wgChromosomeNames.map(nm => genome.getChromosomeDisplayName(nm)) :
            [];

        if (this.showAllChromosomes && genome.chromosomeNames.length > 1) {
            const exclude = new Set(list);
            let count = 0;
            for (let nm of genome.chromosomeNames) {
                if (++count === 1000) {
                    list.push(maximumSequenceCountExceeded);
                    break
                }
                if (!exclude.has(nm)) {
                    nm = genome.getChromosomeDisplayName(nm);
                    list.push(nm);
                }
            }
        }

        this.select.innerHTML = '';

        // Add the "all" selector if whole genome view is supported
        if (genome.showWholeGenomeView()) {
            list.unshift("all");
        }

        for (let name of list) {
            const option = document.createElement('option');
            option.setAttribute('value', name);
            option.innerText = genome.getChromosomeDisplayName(name);
            this.select.appendChild(option);
            // if(this.selectDisplayCSS) {
            //     this.select.style.display = this.selectDisplayCSS
            //     this.container.style.display = this.containerDisplayCSS
            //     document.getElementsByClassName("igv-search-container")[0].style.width = his.searchContainerWidthCSS
            //}
        }

    }
}

class WindowSizePanel {
    constructor(parent, browser) {

        this.container = div$2({class: 'igv-windowsize-panel-container'});
        parent.appendChild(this.container);

        browser.on('locuschange', (referenceFrameList) => {
            this.updatePanel(referenceFrameList);
        });

        this.browser = browser;

    }

    show() {
        this.container.style.display = 'block';
    }

    hide() {
        this.container.style.display = 'none';
    }

    updatePanel(referenceFrameList) {
        const width = this.browser.calculateViewportWidth(this.browser.referenceFrameList.length);
        this.container.innerText = 1 === referenceFrameList.length ? prettyBasePairNumber(Math.round(width * referenceFrameList[0].bpPerPixel)) : '';
    }
}

const multiSelectImage =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>multi select</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="multi-select">
            <rect id="backdrop-copy-3" stroke="#737373" stroke-width="12" fill="#FFFFFF" x="6" y="6" width="613" height="613" rx="135"></rect>
            <g id="row-copy-3" transform="translate(81, 427)" fill="#737373">
                <rect id="Rectangle" x="134" y="0" width="329" height="70"></rect>
                <rect id="Rectangle-Copy-16" stroke="#737373" stroke-width="12" x="6" y="6" width="58" height="58"></rect>
            </g>
            <g id="row-copy-2" transform="translate(82, 277)">
                <rect id="Rectangle" fill-opacity="0.33" fill="#CFCECE" x="133" y="0" width="329" height="70"></rect>
                <rect id="Rectangle-Copy-16" stroke-opacity="0.32659528" stroke="#CFCECE" stroke-width="12" x="6" y="6" width="58" height="58"></rect>
            </g>
            <g id="row-copy" transform="translate(81, 119)" fill="#737373">
                <rect id="Rectangle" x="134" y="0" width="329" height="70"></rect>
                <rect id="Rectangle-Copy-17" stroke="#737373" stroke-width="12" x="6" y="6" width="58" height="58"></rect>
            </g>
        </g>
    </g>
</svg>`;

const multiSelectImageHover =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>multi select hover</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="multi-select-hover">
            <rect id="backdrop-copy-4" stroke="#737373" stroke-width="12" fill="#737373" x="6" y="6" width="613" height="613" rx="135"></rect>
            <g id="row-copy-3" transform="translate(81, 427)" fill="#FFFFFF">
                <rect id="Rectangle" x="134" y="0" width="329" height="70"></rect>
                <rect id="Rectangle-Copy-16" stroke="#FFFFFF" stroke-width="12" x="6" y="6" width="58" height="58"></rect>
            </g>
            <g id="row-copy-2" transform="translate(82, 277)">
                <rect id="Rectangle" fill-opacity="0.33" fill="#CFCECE" x="133" y="0" width="329" height="70"></rect>
                <rect id="Rectangle-Copy-16" stroke-opacity="0.33" stroke="#CFCECE" stroke-width="12" x="6" y="6" width="58" height="58"></rect>
            </g>
            <g id="row-copy" transform="translate(81, 119)" fill="#FFFFFF">
                <rect id="Rectangle" x="134" y="0" width="329" height="70"></rect>
                <rect id="Rectangle-Copy-17" stroke="#FFFFFF" stroke-width="12" x="6" y="6" width="58" height="58"></rect>
            </g>
        </g>
    </g>
</svg>`;

class MultiTrackSelectButton extends NavbarButton {

    constructor(parent, browser, navbar, enableMultiTrackSelection) {

        super(parent, browser, 'Select Tracks', buttonLabel, multiSelectImage, multiSelectImageHover, false);

        this.navbar = navbar;
        this.enableMultiTrackSelection = false;  // Initial state
        this.button.addEventListener('mouseenter', event => {
            if (false === enableMultiTrackSelection) {
                this.setState(true);
            }
        });

        this.button.addEventListener('mouseleave', event => {
            if (false === enableMultiTrackSelection) {
                this.setState(false);
            }
        });

        const mouseClickHandler = () => {
            // Toggle the selection state
            this.setMultiTrackSelection(!this.enableMultiTrackSelection);
        };

        this.boundMouseClickHandler = mouseClickHandler.bind(this);

        this.button.addEventListener('click', this.boundMouseClickHandler);

    }

    setMultiTrackSelection(enableMultiTrackSelection) {

        this.enableMultiTrackSelection = enableMultiTrackSelection;
        this.setState(this.enableMultiTrackSelection);

        // If enableMultiTrackSelection is false hide the Overly button
        if (false === this.enableMultiTrackSelection) {
            this.navbar.overlayTrackButton.setVisibility(false);
        }

        for (const trackView of this.browser.trackViews) {
            trackView.enableTrackSelection(enableMultiTrackSelection);
        }

    }

}

const cursorImage =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>cursor</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="cursor" stroke="#737373">
            <rect id="Rectangle-Copy-3" stroke-width="12" fill="#FFFFFF" x="6" y="6" width="613" height="613" rx="135"></rect>
            <g id="Group" transform="translate(5, 9)" stroke-width="24">
                <line x1="0.5" y1="212.5" x2="614.5" y2="212.5" id="Line"></line>
                <line x1="-115.5" y1="303.5" x2="490.5" y2="303.5" id="Line-Copy" transform="translate(187.5, 303.5) scale(-1, 1) rotate(90) translate(-187.5, -303.5)"></line>
            </g>
            <circle id="Oval" stroke-width="24" cx="191.5" cy="221.5" r="88.5"></circle>
        </g>
    </g>
</svg>`;

const cursorImageHover =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>cursor hover</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="cursor-hover">
            <rect id="Rectangle-Copy-5" stroke="#737373" stroke-width="12" fill="#737373" x="6" y="6" width="613" height="613" rx="135"></rect>
            <g id="Group-Copy-2" transform="translate(5, 9)" stroke="#FFFFFF" stroke-width="24">
                <line x1="0.5" y1="212.5" x2="614.5" y2="212.5" id="Line"></line>
                <line x1="-115.5" y1="303.5" x2="490.5" y2="303.5" id="Line-Copy" transform="translate(187.5, 303.5) scale(-1, 1) rotate(90) translate(-187.5, -303.5)"></line>
            </g>
            <circle id="Oval" stroke="#FFFFFF" stroke-width="24" cx="191.5" cy="221.5" r="88.5"></circle>
        </g>
    </g>
</svg>`;

class CursorGuideButton extends NavbarButton {

    constructor(parent, browser) {

        super(parent, browser, 'Crosshairs', buttonLabel, cursorImage, cursorImageHover, browser.doShowCursorGuide);

        this.button.addEventListener('mouseenter', () => {
            if (false === browser.doShowCursorGuide) {
                this.setState(true);
            }
        });

        this.button.addEventListener('mouseleave', () => {
            if (false === browser.doShowCursorGuide) {
                this.setState(false);
            }
        });

        const mouseClickHandler = () => {

            // if (false === browser.doShowCursorGuide && GenomeUtils.isWholeGenomeView(browser.referenceFrameList[0].chr)) {
            //     return
            // }

            browser.doShowCursorGuide = !browser.doShowCursorGuide;
            browser.setCursorGuideVisibility(browser.doShowCursorGuide);
            this.setState(browser.doShowCursorGuide);

        };

        this.boundMouseClickHandler = mouseClickHandler.bind(this);

        this.button.addEventListener('click', this.boundMouseClickHandler);

        this.setVisibility(browser.config.showCursorTrackingGuideButton);

    }

}

const centerlineImage =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>centerline</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="centerline" stroke="#737373">
            <rect id="Rectangle-Copy-2" stroke-width="12" fill="#FFFFFF" x="6" y="6" width="613" height="613" rx="135"></rect>
            <line x1="6.5" y1="312.5" x2="618.5" y2="312.5" id="Line-Copy" stroke-width="31" transform="translate(312.5, 312.5) scale(-1, 1) rotate(90) translate(-312.5, -312.5)"></line>
        </g>
    </g>
</svg>`;

const centerlineImageHover =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>centerline hover</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="centerline-hover">
            <rect id="Rectangle-Copy-9" stroke="#737373" stroke-width="12" fill="#737373" x="6" y="6" width="613" height="613" rx="135"></rect>
            <line x1="6.5" y1="312.5" x2="618.5" y2="312.5" id="Line-Copy" stroke="#FFFFFF" stroke-width="31" transform="translate(312.5, 312.5) scale(-1, 1) rotate(90) translate(-312.5, -312.5)"></line>
        </g>
    </g>
</svg>`;

class CenterLineButton extends NavbarButton {

    constructor(parent, browser) {

        super(parent, browser, 'Center Line', buttonLabel, centerlineImage, centerlineImageHover, browser.config.showCenterGuide);

        this.button.addEventListener('mouseenter', () => {
            if (false === browser.doShowCenterLine) {
                this.setState(true);
            }
        });

        this.button.addEventListener('mouseleave', () => {
            if (false === browser.doShowCenterLine) {
                this.setState(false);
            }
        });

        const mouseClickHandler = () => {

            browser.doShowCenterLine = !browser.doShowCenterLine;
            browser.setCenterLineVisibility(browser.doShowCenterLine);
            this.setState(browser.doShowCenterLine);
        };

        this.boundMouseClickHandler = mouseClickHandler.bind(this);

        this.button.addEventListener('click', this.boundMouseClickHandler);

        this.setVisibility(browser.config.showCenterGuideButton);

    }

}

const trackLabelsImage =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>track labels</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="track-labels" fill="#FFFFFF" stroke="#737373">
            <rect id="Rectangle-Copy-27" stroke-width="12" x="6" y="6" width="613" height="613" rx="135"></rect>
            <g id="group" transform="translate(52.2778, 149.7248)" stroke-width="24">
                <path d="M448.444444,87.1189616 L448.444444,322.431462 L166.619398,322.431462 L19.2344421,204.775212 L166.619398,87.1189616 L448.444444,87.1189616 Z M244.028212,163.997434 C232.925565,163.997434 222.874656,168.493347 215.598982,175.760254 C208.318067,183.032394 203.815972,193.079106 203.815972,204.175675 C203.815972,215.272243 208.318067,225.318955 215.598982,232.591095 C222.874656,239.858002 232.925565,244.353915 244.028212,244.353915 C255.130858,244.353915 265.181767,239.858002 272.457442,232.591095 C279.738356,225.318955 284.240451,215.272243 284.240451,204.175675 C284.240451,193.079106 279.738356,183.032394 272.457442,175.760254 C265.181767,168.493347 255.130858,163.997434 244.028212,163.997434 Z" id="backdrop"></path>
                <path d="M495.232604,12 L495.232604,264.550423 L193.616935,264.550423 L35.4455183,138.275212 L193.616935,12 L495.232604,12 Z M168.133512,95.1660194 C156.441871,95.1660194 145.858581,99.8943121 138.197214,107.534302 C130.523695,115.18641 125.780417,125.758499 125.780417,137.434423 C125.780417,149.110347 130.523695,159.682435 138.197214,167.334543 C145.858581,174.974533 156.441871,179.702826 168.133512,179.702826 C179.825153,179.702826 190.408443,174.974533 198.06981,167.334543 C205.743329,159.682435 210.486607,149.110347 210.486607,137.434423 C210.486607,125.758499 205.743329,115.18641 198.06981,107.534302 C190.408443,99.8943121 179.825153,95.1660194 168.133512,95.1660194 Z" id="hero"></path>
            </g>
        </g>
    </g>
</svg>`;

const trackLabelsImageHover =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>track labels hover</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="track-labels-hover" stroke="#737373">
            <rect id="Rectangle-Copy-29" stroke-width="12" fill="#737373" x="6" y="6" width="613" height="613" rx="135"></rect>
            <g id="group" transform="translate(52.2778, 149.7248)" fill="#FFFFFF" stroke-width="24">
                <path d="M448.444444,85.1189616 L448.444444,320.431462 L166.619398,320.431462 L19.2344421,202.775212 L166.619398,85.1189616 L448.444444,85.1189616 Z M244.028212,161.997434 C232.925565,161.997434 222.874656,166.493347 215.598982,173.760254 C208.318067,181.032394 203.815972,191.079106 203.815972,202.175675 C203.815972,213.272243 208.318067,223.318955 215.598982,230.591095 C222.874656,237.858002 232.925565,242.353915 244.028212,242.353915 C255.130858,242.353915 265.181767,237.858002 272.457442,230.591095 C279.738356,223.318955 284.240451,213.272243 284.240451,202.175675 C284.240451,191.079106 279.738356,181.032394 272.457442,173.760254 C265.181767,166.493347 255.130858,161.997434 244.028212,161.997434 Z" id="backdrop"></path>
                <path d="M495.232604,12 L495.232604,264.550423 L193.616935,264.550423 L35.4455183,138.275212 L193.616935,12 L495.232604,12 Z M168.133512,95.1660194 C156.441871,95.1660194 145.858581,99.8943121 138.197214,107.534302 C130.523695,115.18641 125.780417,125.758499 125.780417,137.434423 C125.780417,149.110347 130.523695,159.682435 138.197214,167.334543 C145.858581,174.974533 156.441871,179.702826 168.133512,179.702826 C179.825153,179.702826 190.408443,174.974533 198.06981,167.334543 C205.743329,159.682435 210.486607,149.110347 210.486607,137.434423 C210.486607,125.758499 205.743329,115.18641 198.06981,107.534302 C190.408443,99.8943121 179.825153,95.1660194 168.133512,95.1660194 Z" id="hero"></path>
            </g>
        </g>
    </g>
</svg>`;

class TrackLabelControl extends NavbarButton {

    constructor(parent, browser) {

        super(parent, browser, 'Track Labels', buttonLabel, trackLabelsImage, trackLabelsImageHover, browser.config.showTrackLabels);

        this.button.addEventListener('mouseenter', () => {
            if (false === browser.doShowTrackLabels) {
                this.setState(true);
            }
        });

        this.button.addEventListener('mouseleave', () => {
            if (false === browser.doShowTrackLabels) {
                this.setState(false);
            }
        });

        const mouseClickHandler = () => {
            browser.doShowTrackLabels = !browser.doShowTrackLabels;
            browser.setTrackLabelVisibility(browser.doShowTrackLabels);
            this.setState(browser.doShowTrackLabels);
        };

        this.boundMouseClickHandler = mouseClickHandler.bind(this);

        this.button.addEventListener('click', this.boundMouseClickHandler);

        this.setVisibility(browser.config.showTrackLabelButton);

    }

}

const roiImage =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>roi</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="roi">
            <rect id="Rectangle-Copy-23" stroke="#737373" stroke-width="12" fill="#FFFFFF" x="6" y="6" width="613" height="613" rx="135"></rect>
            <text id="ROI" font-family="HelveticaNeue-Bold, Helvetica Neue" font-size="258" font-weight="bold" fill="#737373">
                <tspan x="81.445" y="389">ROI</tspan>
            </text>
        </g>
    </g>
</svg>`;

const roiImageHover =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>roi hover</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="roi-hover">
            <rect id="Rectangle-Copy-24" stroke="#737373" stroke-width="12" fill="#737373" x="6" y="6" width="613" height="613" rx="135"></rect>
            <text id="ROI" font-family="HelveticaNeue-Bold, Helvetica Neue" font-size="258" font-weight="bold" fill="#FFFFFF">
                <tspan x="81.445" y="389">ROI</tspan>
            </text>
        </g>
    </g>
</svg>`;

class ROITableControl extends NavbarButton {

    constructor(parent, browser)  {

        super(parent, browser, ['ROI', 'Regions of Interest Table'], buttonLabel, roiImage, roiImageHover, false);

        this.button.addEventListener('mouseenter', () => {
            if (false === browser.doShowROITable) {
                this.setState(true);
            }
        });

        this.button.addEventListener('mouseleave', () => {
            if (false === browser.doShowROITable) {
                this.setState(false);
            }
        });

        this.button.addEventListener('click', () => this.buttonHandler(!browser.doShowROITable));

        this.setVisibility(false);  // Hide initially, it will be un-hidden if ROIs are loaded

    }

    buttonHandler(status) {
        this.setState(status);
        this.browser.setROITableVisibility(status);
    }
}

const sampleInfoImage =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>sample info</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="sample-info">
            <rect id="Rectangle-Copy-6" stroke="#737373" stroke-width="12" fill="#FFFFFF" x="6" y="6" width="613" height="613" rx="135"></rect>
            <g id="Group-2" transform="translate(8, 149)">
                <rect id="Rectangle" fill="#8DD3C7" x="0" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy" fill="#FBB4AE" x="102" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-8" fill="#7FC97F" x="204" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#ADE2CF" x="306" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#E41A1C" x="408" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#3F70AE" x="510" y="0" width="102" height="102"></rect>
            </g>
            <g id="Group-2-Copy" transform="translate(8, 251)">
                <rect id="Rectangle" fill="#E41A1C" x="0" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy" fill="#377EB8" x="102" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-8" fill="#FDB46B" x="204" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#BC80BD" x="306" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#FFFFB3" x="408" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#ADE2CF" x="510" y="0" width="102" height="102"></rect>
            </g>
            <g id="Group-2-Copy-2" transform="translate(314, 404) scale(-1, 1) translate(-314, -404)translate(8, 353)">
                <rect id="Rectangle" fill="#1B9E77" x="0" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy" fill="#377EB8" x="102" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-8" fill="#FDB46B" x="204" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#BAD1E9" x="306" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#4DAF4A" x="408" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#CBF03E" x="510" y="0" width="102" height="102"></rect>
            </g>
            <rect id="Rectangle-Copy-10" stroke="#737373" stroke-width="12" x="6" y="6" width="613" height="613" rx="135"></rect>
        </g>
    </g>
</svg>`;

const sampleInfoImageHover =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>sample info hover</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="sample-info-hover">
            <rect id="Rectangle-Copy-14" stroke="#737373" stroke-width="12" fill="#737373" x="6" y="6" width="613" height="613" rx="135"></rect>
            <g id="row-2" transform="translate(314, 404) scale(-1, 1) translate(-314, -404)translate(8, 353)">
                <rect id="Rectangle" fill="#1B9E77" x="0" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy" fill="#377EB8" x="102" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-8" fill="#FDB46B" x="204" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#BAD1E9" x="306" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#4DAF4A" x="408" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#CBF03E" x="510" y="0" width="102" height="102"></rect>
            </g>
            <g id="row-1" transform="translate(8, 251)">
                <rect id="Rectangle" fill="#E41A1C" x="0" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy" fill="#377EB8" x="102" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-8" fill="#FDB46B" x="204" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#BC80BD" x="306" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#FFFFB3" x="408" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#ADE2CF" x="510" y="0" width="102" height="102"></rect>
            </g>
            <g id="row-0" transform="translate(8, 149)">
                <rect id="Rectangle" fill="#8DD3C7" x="0" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy" fill="#FBB4AE" x="102" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-8" fill="#7FC97F" x="204" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#ADE2CF" x="306" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#E41A1C" x="408" y="0" width="102" height="102"></rect>
                <rect id="Rectangle-Copy-7" fill="#3F70AE" x="510" y="0" width="102" height="102"></rect>
            </g>
            <rect id="Rectangle-Copy-15" stroke="#737373" stroke-width="12" x="6" y="6" width="613" height="613" rx="135"></rect>
            <rect id="scrim" fill="#000000" style="mix-blend-mode: hue;" x="8" y="149" width="612" height="306"></rect>
        </g>
    </g>
</svg>`;

class SampleInfoControl extends NavbarButton {

    constructor(parent, browser) {

        super(parent, browser, 'Sample Info', buttonLabel, sampleInfoImage, sampleInfoImageHover, false);

        this.showSampleInfo = false;

        this.button.addEventListener('mouseenter', () => {
            if (false === this.showSampleInfo) {
                this.setState(true);
            }
        });

        this.button.addEventListener('mouseleave', () => {
            if (false === this.showSampleInfo) {
                this.setState(false);
            }
        });

        this.button.addEventListener('click', () => {
            this.performClickWithState(browser, undefined);
        });

    }

    performClickWithState(browser, doShowSampleInfoOrUndefined) {

        this.showSampleInfo = undefined === doShowSampleInfoOrUndefined ? !this.showSampleInfo : doShowSampleInfoOrUndefined;

        const column = browser.columnContainer.querySelector('.igv-sample-info-column');
        column.style.display = false === this.showSampleInfo ? 'none' : 'flex';

        this.setState(this.showSampleInfo);

        browser.layoutChange();

    }

    setButtonVisibility(isVisible) {

        this.showSampleInfo = isVisible;

        this.setState(this.showSampleInfo);

        if (true === this.showSampleInfo) {
            this.show();
        } else {
            this.hide();
        }
    }

}

const sampleNameImage =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>sample names</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="sample-names" stroke="#737373">
            <rect id="Rectangle-Copy-13" stroke-width="12" fill="#FFFFFF" x="6" y="6" width="613" height="613" rx="135"></rect>
            <line x1="80" y1="465" x2="541" y2="464.5" id="Line-3-Copy-3" stroke-width="32"></line>
            <line x1="80" y1="312.5" x2="542" y2="313" id="Line-3" stroke-width="32"></line>
            <line x1="80" y1="158" x2="541" y2="158" id="Line-3-Copy" stroke-width="32"></line>
        </g>
    </g>
</svg>`;

const sampleNameImageHover =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>sample names hover</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="sample-names-hover">
            <rect id="Rectangle-Copy-18" stroke="#737373" stroke-width="12" fill="#737373" x="6" y="6" width="613" height="613" rx="135"></rect>
            <line x1="80" y1="465" x2="541" y2="464.5" id="Line-3-Copy-3" stroke="#FFFFFF" stroke-width="32" fill="#FFFFFF"></line>
            <line x1="80" y1="312.5" x2="542" y2="313" id="Line-3" stroke="#FFFFFF" stroke-width="32" fill="#FFFFFF"></line>
            <line x1="80" y1="158" x2="541" y2="158" id="Line-3-Copy" stroke="#FFFFFF" stroke-width="32" fill="#FFFFFF"></line>
        </g>
    </g>
</svg>`;

class SampleNameControl extends NavbarButton {

    constructor(parent, browser) {

        super(parent, browser, 'Sample Names', sampleNameButtonLabel, sampleNameImage, sampleNameImageHover, browser.config.showSampleNames);

        this.button.addEventListener('mouseenter', () => {
            if (false === browser.showSampleNames) {
                this.setState(true);
            }
        });

        this.button.addEventListener('mouseleave', () => {
            if (false === browser.showSampleNames) {
                this.setState(false);
            }
        });

        this.button.addEventListener('click', () => {
            this.performClickWithState(browser, undefined);
        });

        if (true === browser.config.showSampleNameButton) {
            this.show();
        } else {
            this.hide();
        }

    }

    performClickWithState(browser, doShowSampleNamesOrUndefined) {

        browser.showSampleNames = undefined === doShowSampleNamesOrUndefined ? !browser.showSampleNames : doShowSampleNamesOrUndefined;

        const column = browser.columnContainer.querySelector('.igv-sample-name-column');
        column.style.display = false === browser.showSampleNames ? 'none' : 'flex';

        this.setState(browser.showSampleNames);

        browser.layoutChange();

    }

}

class Dropdown {
    constructor(parent, shim) {

        this.parent = parent;

        // popover
        this.popover = div$2({ class: "igv-ui-dropdown" });
        parent.appendChild(this.popover);

        // content
        this.popoverContent = div$2();
        this.popover.appendChild(this.popoverContent);

        this.popover.style.display = 'none';

        this.shim = shim;
    }

    configure(dropdownItems) {

        if (0 === dropdownItems.length) {
            return
        }

        const menuElements = createMenuElements$1(dropdownItems, this.popover);

        for (const { element } of menuElements) {
            this.popoverContent.appendChild(element);
        }

    }

    present(event) {
        this.popover.style.display = 'block';

        let { x, y } = translateMouseCoordinates(event, this.parent);

        // this.popover.style.left  = `${ x }px`
        // this.popover.style.top  = `${ y }px`

        this.popover.style.left  = `${ x + this.shim.left }px`;
        this.popover.style.top  = `${ y + this.shim.top }px`;
    }

    _present(event) {

        this.popover.style.display = 'block';

        let { x, y, width } = translateMouseCoordinates(event, this.parent);

        x += this.shim.left;
        y += this.shim.top;

        this.popover.style.top  = `${ y }px`;

        const { width: w } = this.popover.getBoundingClientRect();

        const xmax = x + w;
        const delta = xmax - width;

        this.popover.style.left = `${ xmax > width ? (x - delta) : x }px`;

        // this.popoverContent.style.maxWidth = `${ Math.min(w, width) }px`
    }

    dismiss() {
        this.popover.style.display = 'none';
    }
}

const imageSaveImageSVG =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>save image</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="save-image">
            <rect id="border" stroke="#737373" stroke-width="12" fill="#FFFFFF" x="6" y="6" width="613" height="613" rx="135"></rect>
            <g id="frame" transform="translate(66, 66)" fill="#737373" fill-rule="nonzero">
                <path d="M54.75,493 C39.69375,493 26.8046875,487.639062 16.0828125,476.917187 C5.3609375,466.195312 0,453.30625 0,438.25 L0,274 L54.75,274 L54.75,438.25 L219,438.25 L219,493 L54.75,493 Z" id="Path"></path>
                <path d="M274,493 L274,438.25 L438.25,438.25 L438.25,274 L493,274 L493,438.25 C493,453.30625 487.639062,466.195312 476.917187,476.917187 C466.195312,487.639062 453.30625,493 438.25,493 L274,493 Z" id="Path"></path>
                <path d="M0,219 L0,54.75 C0,39.69375 5.3609375,26.8046875 16.0828125,16.0828125 C26.8046875,5.3609375 39.69375,0 54.75,0 L219,0 L219,54.75 L54.75,54.75 L54.75,219 L0,219 Z" id="Path"></path>
                <path d="M438.25,219 L438.25,54.75 L274,54.75 L274,0 L438.25,0 C453.30625,0 466.195312,5.3609375 476.917187,16.0828125 C487.639062,26.8046875 493,39.69375 493,54.75 L493,219 L438.25,219 Z" id="Path"></path>
            </g>
            <rect id="border-hold-out-matte" fill="#FFFFFF" x="91" y="90" width="444" height="444" rx="38"></rect>
            <g id="artwork" transform="translate(148, 176)" fill="#737373" fill-rule="nonzero">
                <polygon id="Path" points="0 273 82.25 163.4 143.9375 245.6 226.1875 136 329 273"></polygon>
                <path d="M260,82 C248.155556,82 238.361111,78.1277778 230.616667,70.3833333 C222.872222,62.6388889 219,52.8444444 219,41 C219,29.1555556 222.872222,19.3611111 230.616667,11.6166667 C238.361111,3.87222222 248.155556,0 260,0 C271.844444,0 281.638889,3.87222222 289.383333,11.6166667 C297.127778,19.3611111 301,29.1555556 301,41 C301,52.8444444 297.127778,62.6388889 289.383333,70.3833333 C281.638889,78.1277778 271.844444,82 260,82 Z" id="Path"></path>
            </g>
        </g>
    </g>
</svg>`;

const imageSaveImageHoverSVG =
    `<svg width="625px" height="625px" viewBox="0 0 625 625" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>save image</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="save-image">
            <rect id="border" stroke="#737373" stroke-width="12" fill="#FFFFFF" x="6" y="6" width="613" height="613" rx="135"></rect>
            <g id="frame" transform="translate(66, 66)" fill="#737373" fill-rule="nonzero">
                <path d="M54.75,493 C39.69375,493 26.8046875,487.639062 16.0828125,476.917187 C5.3609375,466.195312 0,453.30625 0,438.25 L0,274 L54.75,274 L54.75,438.25 L219,438.25 L219,493 L54.75,493 Z" id="Path"></path>
                <path d="M274,493 L274,438.25 L438.25,438.25 L438.25,274 L493,274 L493,438.25 C493,453.30625 487.639062,466.195312 476.917187,476.917187 C466.195312,487.639062 453.30625,493 438.25,493 L274,493 Z" id="Path"></path>
                <path d="M0,219 L0,54.75 C0,39.69375 5.3609375,26.8046875 16.0828125,16.0828125 C26.8046875,5.3609375 39.69375,0 54.75,0 L219,0 L219,54.75 L54.75,54.75 L54.75,219 L0,219 Z" id="Path"></path>
                <path d="M438.25,219 L438.25,54.75 L274,54.75 L274,0 L438.25,0 C453.30625,0 466.195312,5.3609375 476.917187,16.0828125 C487.639062,26.8046875 493,39.69375 493,54.75 L493,219 L438.25,219 Z" id="Path"></path>
            </g>
            <rect id="border-hold-out-matte" fill="#FFFFFF" x="91" y="90" width="444" height="444" rx="38"></rect>
            <g id="artwork" transform="translate(148, 176)" fill="#737373" fill-rule="nonzero">
                <polygon id="Path" points="0 273 82.25 163.4 143.9375 245.6 226.1875 136 329 273"></polygon>
                <path d="M260,82 C248.155556,82 238.361111,78.1277778 230.616667,70.3833333 C222.872222,62.6388889 219,52.8444444 219,41 C219,29.1555556 222.872222,19.3611111 230.616667,11.6166667 C238.361111,3.87222222 248.155556,0 260,0 C271.844444,0 281.638889,3.87222222 289.383333,11.6166667 C297.127778,19.3611111 301,29.1555556 301,41 C301,52.8444444 297.127778,62.6388889 289.383333,70.3833333 C281.638889,78.1277778 271.844444,82 260,82 Z" id="Path"></path>
            </g>
        </g>
    </g>
</svg>`;

class SaveImageControl extends NavbarButton {
    constructor(parent, browser) {

        super(parent, browser, 'Save Image', buttonLabel, imageSaveImageSVG, imageSaveImageHoverSVG, false);

        this.button.addEventListener('mouseenter', () => this.setState(true));

        this.button.addEventListener('mouseleave', () => {

            for (const el of this.button.querySelectorAll('div')) {
                if('block' === el.style.display) {
                    return
                }
            }

            this.setState(false);
        });

        this.dropdown = new Dropdown(this.button.parentNode, { top:24, left:-88 });

        const items =
            [
                {
                    label: "Save as SVG",
                    click: e => {
                        this.browser.saveSVGtoFile("igvjs.svg");
                        this.dropdown.dismiss();
                    }
                },
                {
                    label: "Save as PNG",
                    click: e => {
                        this.browser.savePNGtoFile("igvjs.png");
                        this.dropdown.dismiss();
                    }
                },
            ];

        this.dropdown.configure(items);

        this.button.addEventListener('click', e => {

            let takeAction;
            if (e.target === this.button) {
                takeAction = true;
            } else if (e.target.closest('svg')) {
                const parentDiv = e.target.closest('div');
                if (parentDiv === this.button) {
                    takeAction = true;
                }
            }

            if (true === takeAction) {
                 'none' === this.dropdown.popover.style.display ? this.dropdown.present(e) : this.dropdown.dismiss();
            }

        });

        this.setVisibility(browser.config.showSVGButton);

    }

    navbarResizeHandler(navbarButtonCSSClass) {
        this.dropdown.dismiss();
        super.navbarResizeHandler(navbarButtonCSSClass);
    }

}

/**
 * User supplied button for the navbar
 */

const CustomButton = function (parent, browser, b) {

    const button = div$2({class: 'igv-navbar-button'});
    parent.appendChild(button);
    button.textContent = b.label;
    button.addEventListener('click', () => b.callback(browser));
};

const sliderMin = 0;
let sliderMax = 23;
let sliderValueRaw = 0;

class ZoomWidget {
    constructor(config, browser, parent) {

        this.browser = browser;

        this.zoomContainer = div$2({class: 'igv-zoom-widget'});
        parent.appendChild(this.zoomContainer);

        // zoom out
        this.zoomOutButton = div$2();
        this.zoomContainer.appendChild(this.zoomOutButton);
        this.zoomOutButton.appendChild(createIcon$1('minus-circle'));
        this.zoomOutButton.addEventListener('click', () => {
            // browser.zoomWithScaleFactor(2.0)
            browser.zoomOut();
        });

        // Range slider
        const el = div$2();
        this.zoomContainer.appendChild(el);
        this.slider = document.createElement('input');
        this.slider.type = 'range';

        this.slider.min = `${sliderMin}`;
        this.slider.max = `${sliderMax}`;

        el.appendChild(this.slider);

        this.slider.addEventListener('change', e => {

            e.preventDefault();
            e.stopPropagation();

            const referenceFrame = browser.referenceFrameList[0];
            const {bpLength} = referenceFrame.genome.getChromosome(referenceFrame.chr);
            const {end, start} = referenceFrame;

            const extent = end - start;

            // bpLength/(end - start)
            const scaleFactor = Math.pow(2, e.target.valueAsNumber);

            // (end - start) = bpLength/scaleFactor
            const zoomedExtent = bpLength / scaleFactor;

            // console.log(`zoom-widget - slider ${ e.target.value } scaleFactor ${ scaleFactor } extent-zoomed ${ StringUtils.numberFormatter(Math.round(zoomedExtent)) }`)

            browser.zoomWithScaleFactor(zoomedExtent / extent);

        });

        // zoom in
        this.zoomInButton = div$2();
        this.zoomContainer.appendChild(this.zoomInButton);
        this.zoomInButton.appendChild(createIcon$1('plus-circle'));
        this.zoomInButton.addEventListener('click', () => {
            // browser.zoomWithScaleFactor(0.5)
            browser.zoomIn();
        });

        browser.on('locuschange', (referenceFrameList) => {

            if (this.browser.isMultiLocusMode()) {
                this.disable();
            } else {
                this.enable();
                this.update(referenceFrameList);
            }

        });

    }

    update(referenceFrameList) {

        if (this.slider) {
            const referenceFrame = referenceFrameList[0];
            const {bpLength} = referenceFrame.genome.getChromosome(referenceFrame.chr);
            const {start, end} = referenceFrame;

            sliderMax = Math.ceil(Math.log2(bpLength / this.browser.minimumBases()));
            this.slider.max = `${sliderMax}`;

            const scaleFactor = bpLength / (end - start);
            sliderValueRaw = Math.log2(scaleFactor);
            this.slider.value = `${Math.round(sliderValueRaw)}`;
        }
    }


    enable() {

        // this.zoomInButton.style.color = appleCrayonPalette[ 'steel' ];
        // this.zoomInButton.style.pointerEvents = 'auto';
        //
        // this.zoomOutButton.style.color = appleCrayonPalette[ 'steel' ];
        // this.zoomOutButton.style.pointerEvents = 'auto';

        if (this.slider) this.slider.disabled = false;
    }

    disable() {

        // this.zoomInButton.style.color = appleCrayonPalette[ 'silver' ];
        // this.zoomInButton.style.pointerEvents = 'none';
        //
        // this.zoomOutButton.style.color = appleCrayonPalette[ 'silver' ];
        // this.zoomOutButton.style.pointerEvents = 'none';

        if (this.slider) this.slider.disabled = true;
    }

    hide() {
        this.zoomContainer.style.display = 'none';
    }

    show() {
        this.zoomContainer.style.display = 'block';
    }

    hideSlider() {
        if (this.slider) this.slider.style.display = 'none';
    }

    showSlider() {
        if (this.slider) this.slider.style.display = 'block';
    }
}

class ResponsiveNavbar {
    constructor(config, browser) {

        this.browser = browser;
        this.config = config;

        this.currentClass = 'igv-navbar-text-button';

        // Create nav bar element
        const navBar = document.createElement('div');
        navBar.className = 'igv-navbar';
        this.navigation = navBar;

        // Left container
        const navbarLeftContainer = document.createElement('div');
        navbarLeftContainer.className = 'igv-navbar-left-container';
        navBar.appendChild(navbarLeftContainer);
        this.navbarLeftContainer = navbarLeftContainer;

        // IGV logo
        const logoElement = document.createElement('div');
        logoElement.className = 'igv-logo';
        navbarLeftContainer.appendChild(logoElement);

        const logoSvg = logo();
        logoSvg.style.width = "34px";
        logoSvg.style.height = "32px";
        logoElement.appendChild(logoSvg);

        this.currentGenome = document.createElement('div');
        this.currentGenome.className = 'igv-current-genome';
        navbarLeftContainer.appendChild(this.currentGenome);
        this.currentGenome.textContent = '';

        const genomicLocation = document.createElement('div');
        genomicLocation.className = 'igv-navbar-genomic-location';
        navbarLeftContainer.appendChild(genomicLocation);

        // Chromosome select widget
        this.chromosomeSelectWidget = new ChromosomeSelectWidget(browser, genomicLocation);
        if (config.showChromosomeWidget !== false) {
            this.chromosomeSelectWidget.show();
        } else {
            this.chromosomeSelectWidget.hide();
        }

        const locusSizeGroup = document.createElement('div');
        locusSizeGroup.className = 'igv-locus-size-group';
        genomicLocation.appendChild(locusSizeGroup);

        const searchContainer = document.createElement('div');
        searchContainer.className = 'igv-search-container';
        locusSizeGroup.appendChild(searchContainer);

        // Search input
        this.searchInput = document.createElement('input');
        this.searchInput.className = 'igv-search-input';
        this.searchInput.type = 'text';
        this.searchInput.placeholder = 'Locus Search';
        searchContainer.appendChild(this.searchInput);

        // Stop event propagation
        this.searchInput.addEventListener('keyup', (event) => {
            event.stopImmediatePropagation();
        });

        this.searchInput.addEventListener('change', () => {
            this.doSearch(this.searchInput.value);
        });

        const searchIconContainer = document.createElement('div');
        searchIconContainer.className = 'igv-search-icon-container';
        searchContainer.appendChild(searchIconContainer);

        const searchIcon = createIcon$1("search");
        searchIconContainer.appendChild(searchIcon);

        searchIconContainer.addEventListener('click', () => {
            this.doSearch(this.searchInput.value);
        });

        this.windowSizePanel = new WindowSizePanel(locusSizeGroup, browser);

        // Right container
        const navbarRightContainer = document.createElement('div');
        navbarRightContainer.className = 'igv-navbar-right-container';
        navBar.appendChild(navbarRightContainer);
        this.navbarRightContainer = navbarRightContainer;

        const toggleButtonContainer = document.createElement('div');
        toggleButtonContainer.className = 'igv-navbar-toggle-button-container';
        navbarRightContainer.appendChild(toggleButtonContainer);
        this.toggleButtonContainer = toggleButtonContainer;

        this.overlayTrackButton = new OverlayTrackButton(toggleButtonContainer, browser);
        this.overlayTrackButton.setVisibility(false);

        const showMultiSelect = config.showMultiSelectButton !== false;
        this.multiTrackSelectButton = new MultiTrackSelectButton(toggleButtonContainer, browser, this, showMultiSelect);


        this.cursorGuideButton = new CursorGuideButton(toggleButtonContainer, browser);
        this.centerLineButton = new CenterLineButton(toggleButtonContainer, browser);
        this.trackLabelControl = new TrackLabelControl(toggleButtonContainer, browser);
        this.roiTableControl = new ROITableControl(toggleButtonContainer, browser);
        this.sampleInfoControl = new SampleInfoControl(toggleButtonContainer, browser);
        this.sampleNameControl = new SampleNameControl(toggleButtonContainer, browser);

        if (config.showSVGButton === true) {
            this.saveImageControl = new SaveImageControl(toggleButtonContainer, browser);
        }

        if (config.customButtons) {
            for (let b of config.customButtons) {
                new CustomButton(toggleButtonContainer, browser, b);
            }
        }

        this.zoomWidget = new ZoomWidget(config, browser, navbarRightContainer);

        if (config.showNavigation === false) {
            this.navigation.style.display = 'none';
        }
    }

    navbarDidResize() {

        const currentClass = this.currentNavbarButtonClass();
        if ('igv-navbar-text-button' === currentClass) {
            this.textButtonContainerWidth = this.navbarRightContainer.getBoundingClientRect().width;
        }
        const browser = this.browser;
        const isWGV =
            (browser.isMultiLocusWholeGenomeView()) ||
            (browser.referenceFrameList && GenomeUtils.isWholeGenomeView(browser.referenceFrameList[0].chr));

        isWGV ? this.windowSizePanel.hide() : this.windowSizePanel.show();

        const {
            x: leftContainerX,
            width: leftContainerWidth
        } = this.navbarLeftContainer.getBoundingClientRect();
        const leftContainerExtent = leftContainerX + leftContainerWidth;
        const {x: rightContainerX} = this.navbarRightContainer.getBoundingClientRect();

        const delta = rightContainerX - leftContainerExtent;

        let navbarButtonClass;
        const threshold = 8;
        if ('igv-navbar-text-button' === currentClass && delta < threshold) {
            navbarButtonClass = 'igv-navbar-icon-button';
        } else if (this.textButtonContainerWidth && 'igv-navbar-icon-button' === currentClass) {
            const length = this.navigation.offsetWidth - leftContainerExtent;
            if (length - this.textButtonContainerWidth > threshold) {
                navbarButtonClass = 'igv-navbar-text-button';
            }
        }
        // Update all the buttons (buttons are listeners)
        if(navbarButtonClass && currentClass !== navbarButtonClass) {
            this.currentClass = navbarButtonClass;
            this.browser.fireEvent('navbar-resize', [navbarButtonClass]);
        }

        let zoomContainerClass;
        if (isWGV) {
            zoomContainerClass = 'igv-zoom-widget-hidden';
        } else {
            zoomContainerClass = this.navigation.offsetWidth > 860 ? 'igv-zoom-widget' : 'igv-zoom-widget-900';
        }
        this.zoomWidget.zoomContainer.className = '';
        this.zoomWidget.zoomContainer.classList.add(zoomContainerClass);
    }

    updateGenome(genome) {
        let genomeLabel = (genome.id && genome.id.length < 20
            ? genome.id
            : `${genome.id.substring(0, 8)}...${genome.id.substring(genome.id.length - 8)}`);

        // Update genome label text
        this.currentGenome.textContent = genomeLabel;

        // Update genome description as a title attribute
        this.currentGenome.setAttribute('title', genome.description);

        // Chromosome select widget -- Show this if it's not explicitly hidden AND the genome has pre-loaded chromosomes
        const showChromosomeWidget =
            this.config.showChromosomeWidget !== false &&
            genome.showChromosomeWidget !== false &&
            genome.chromosomeNames &&
            genome.chromosomeNames.length > 1;

        if (showChromosomeWidget) {
            this.chromosomeSelectWidget.update(genome);
            this.chromosomeSelectWidget.show();
        } else {
            this.chromosomeSelectWidget.hide();
        }    }

    updateLocus(loc, chrName) {
        if (this.searchInput) {
            this.searchInput.value = loc;
        }
        if (this.chromosomeSelectWidget) {
            this.chromosomeSelectWidget.select.value = chrName;
        }
    }

    currentNavbarButtonClass() {
        return this.currentClass
    }

    setEnableTrackSelection(b) {
        this.multiTrackSelectButton.setMultiTrackSelection(b);
    }

    getEnableTrackSelection() {
        return this.multiTrackSelectButton.enableMultiTrackSelection
    }

    hide() {
        this.navigation.style.display = 'none';
    }

    show() {
        this.navigation.style.display = 'flex';
    }

    /**

     * Search for the locus string -- this function is called from the navbar search box, and is not part of the API.
     * Wraps ```search``` and presents an error dialog if false.
     *
     * @param locus
     * @param init
     * @returns {Promise<void>}
     */
    async doSearch(locus) {
        const success = await this.browser.search(locus);
        if (!success) {
            this.browser.alert.present(new Error(`Unrecognized locus: <b> ${locus} </b>`));
        }
    }

}

function logo() {
    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttribute("width", "690px");
    svg.setAttribute("height", "324px");
    svg.setAttribute("viewBox", "0 0 690 324");
    svg.setAttribute("version", "1.1");
    svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
    svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");

    const title = document.createElementNS("http://www.w3.org/2000/svg", "title");
    title.textContent = "IGV";
    svg.appendChild(title);

    const gPage = document.createElementNS("http://www.w3.org/2000/svg", "g");
    gPage.setAttribute("id", "Page-1");
    gPage.setAttribute("stroke", "none");
    gPage.setAttribute("stroke-width", "1");
    gPage.setAttribute("fill", "none");
    gPage.setAttribute("fill-rule", "evenodd");
    svg.appendChild(gPage);

    const gIGV = document.createElementNS("http://www.w3.org/2000/svg", "g");
    gIGV.setAttribute("id", "IGV");
    gIGV.setAttribute("fill", "#666666");
    gPage.appendChild(gIGV);

    const polygon1 = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
    polygon1.setAttribute("id", "Path");
    polygon1.setAttribute(
        "points",
        "379.54574 8.00169252 455.581247 8.00169252 515.564813 188.87244 532.884012 253.529506 537.108207 253.529506 554.849825 188.87244 614.833392 8.00169252 689.60164 8.00169252 582.729511 320.722144 486.840288 320.722144"
    );
    gIGV.appendChild(polygon1);

    const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
    path.setAttribute(
        "d",
        "M261.482414,323.793286 C207.975678,323.793286 168.339046,310.552102 142.571329,284.069337 C116.803612,257.586572 103.919946,217.158702 103.919946,162.784513 C103.919946,108.410325 117.437235,67.8415913 144.472217,41.0770945 C171.507199,14.3125977 212.903894,0.930550071 268.663545,0.930550071 C283.025879,0.930550071 298.232828,1.84616386 314.284849,3.6774189 C330.33687,5.50867394 344.839793,7.97378798 357.794056,11.072835 L357.794056,68.968378 C339.48912,65.869331 323.578145,63.5450806 310.060654,61.9955571 C296.543163,60.4460336 284.574731,59.6712835 274.154998,59.6712835 C255.850062,59.6712835 240.502308,61.4320792 228.111274,64.9537236 C215.720241,68.4753679 205.793482,74.2507779 198.330701,82.2801269 C190.867919,90.309476 185.587729,100.87425 182.48997,113.974767 C179.392212,127.075284 177.843356,143.345037 177.843356,162.784513 C177.843356,181.942258 179.251407,198.000716 182.067551,210.960367 C184.883695,223.920018 189.671068,234.41436 196.429813,242.443709 C203.188559,250.473058 212.059279,256.178037 223.042241,259.558815 C234.025202,262.939594 247.683295,264.629958 264.01693,264.629958 C268.241146,264.629958 273.098922,264.489094 278.590403,264.207362 C284.081883,263.925631 289.643684,263.50304 295.275972,262.939577 L295.275972,159.826347 L361.595831,159.826347 L361.595831,308.579859 C344.698967,313.087564 327.239137,316.750019 309.215815,319.567334 C291.192494,322.38465 275.281519,323.793286 261.482414,323.793286 L261.482414,323.793286 L261.482414,323.793286 Z"
    );
    path.setAttribute("id", "Path");
    gIGV.appendChild(path);

    const polygon2 = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
    polygon2.setAttribute("id", "Path");
    polygon2.setAttribute(
        "points",
        "0.81355666 5.00169252 73.0472883 5.00169252 73.0472883 317.722144 0.81355666 317.722144"
    );
    gIGV.appendChild(polygon2);

    return svg;
}

const viewportColumnManager =
    {
        createColumns: (columnContainer, count) => {

            for (let i = 0; i < count; i++) {
                if (0 === i) {
                    createColumn(columnContainer, 'igv-column');
                } else {
                    columnContainer.appendChild(div$2({class: 'igv-column-shim'}));
                    createColumn(columnContainer, 'igv-column');
                }
            }

        },

        removeColumnAtIndex: (i, column) => {
            const shim = 0 === i ? column.nextElementSibling : column.previousElementSibling;
            column.remove();
            shim.remove();
        },

        insertAfter: referenceElement => {

            const shim = div$2({class: 'igv-column-shim'});
            insertElementAfter(shim, referenceElement);

            const column = div$2({class: 'igv-column'});
            insertElementAfter(column, shim);

            return column
        },

        insertBefore: (referenceElement, count) => {

            for (let i = 0; i < count; i++) {

                const column = div$2({class: 'igv-column'});
                insertElementBefore(column, referenceElement);

                if (count > 1 && i > 0) {
                    const columnShim = div$2({class: 'igv-column-shim'});
                    insertElementBefore(columnShim, column);
                }

            }

        },

        indexOfColumn: (columnContainer, column) => {

            const allColumns = columnContainer.querySelectorAll('.igv-column');

            for (let i = 0; i < allColumns.length; i++) {
                const c = allColumns[ i ];
                if (c === column) {
                    return i
                }
            }

            return undefined
        },
    };

class ViewportCenterLine {

    constructor(browser, referenceFrame, column) {

        this.browser = browser;
        this.referenceFrame = referenceFrame;
        this.column = column;

        this.container = div$2({class: 'igv-center-line'});
        column.appendChild(this.container);

        if (browser.doShowCenterLine) {
            this.show();
        } else {
            this.hide();
        }
    }

    repaint() {

        if (this.referenceFrame) {

            const ppb = 1.0 / this.referenceFrame.bpPerPixel;
            if (ppb > 1) {
                const width = Math.floor(this.referenceFrame.toPixels(1));
                this.container.style.width = `${width}px`;
                this.container.classList.remove('igv-center-line-thin');
                this.container.classList.add('igv-center-line-wide');
            } else {
                this.container.style.width = '1px';
                this.container.classList.remove('igv-center-line-wide');
                this.container.classList.add('igv-center-line-thin');
            }
        }
    }

    show() {
        this.isVisible = true;
        this.container.style.display = 'block';
        this.repaint();
    }

    hide() {
        this.isVisible = false;
        this.container.style.display = 'none';
    }

    resize() {
        this.repaint();
    }
}

const CircularViewControl = function (parent, browser) {

    this.button = div$2({class: 'igv-navbar-button'});
    parent.appendChild(this.button);
    this.button.textContent = 'circular view';

    this.button.addEventListener('click', () => {
        browser.circularViewVisible = !browser.circularViewVisible;
        //this.setState(browser.circularViewVisible)
    });

    this.browser = browser;

    this.setVisibility(browser.config.showCircularViewButton);

    this.setState(browser.circularViewVisible);

};

CircularViewControl.prototype.setVisibility = function (showCircularViewButton) {
    if (true === showCircularViewButton) {
        this.show();
    } else {
        this.hide();
    }
};

CircularViewControl.prototype.setState = function (circularViewVisible) {
    if (true === circularViewVisible) {
        this.button.classList.add('igv-navbar-button-clicked');
    } else {
        this.button.classList.remove('igv-navbar-button-clicked');
    }
};

CircularViewControl.prototype.show = function () {
    this.button.style.display = 'block';
    this.setState(this.browser.circularViewVisible);
};

CircularViewControl.prototype.hide = function () {
    this.button.style.display = 'none';
};

const maxSequenceSize = 1000000;
const maxBlatSize = 25000;

class ROIMenu {
    constructor(browser, parent) {

        this.browser = browser;

        // container
        this.container = div$2({class: 'igv-roi-menu'});
        parent.appendChild(this.container);

        // header
        const header = div$2();
        this.container.appendChild(header);

        attachDialogCloseHandlerWithParent$1(header, () => this.container.style.display = 'none');

        // body
        this.body = div$2();
        this.container.appendChild(this.body);

        this.container.style.display = 'none';
    }

    async present(feature, roiSet, event, roiManager, columnContainer, regionElement) {
        const menuItems = this.menuItems(feature, roiSet, event, roiManager, columnContainer, regionElement);
        this.browser.menuPopup.presentTrackContextMenu(event, menuItems);
    }

    menuItems(feature, roiSet, event, roiManager, columnContainer, regionElement) {
        const items = feature.name ? [`<b>${feature.name}</b><br/>`]  : [];
        if ('name' in roiSet) items.push(`<b>ROI Set: ${roiSet.name}</b>`);
        if (items.length > 0) items.push(`<hr/>`);

        if (roiSet.isUserDefined) {
            this.#addDescriptionMenuItem(items, feature, event);
        }

        // sequence

        // copy
        if (isSecureContext() && feature.end - feature.start < maxSequenceSize) {
            this.#addCopySequenceMenuItem(items, feature);
        }

        if (feature.end - feature.start <= maxBlatSize) {
            this.#addBlatMenuItem(items, feature);
        }

        // Add sort menu items  -- disabled for now, its not clear what this means for tracks in general
        this.#addSortMenuItems(items, feature);


        if (roiSet.isUserDefined) {

            this.#addDeleteMenuItem(items, feature, roiSet, roiManager, columnContainer, regionElement);
        }

        return items
    }

    #addDeleteMenuItem(items, feature, roiSet, roiManager, columnContainer, regionElement) {

        items.push('<hr/>');
        items.push(
            {
                label: 'Delete',
                click: async () => {
                    roiSet.removeFeature(feature);
                    const userDefinedFeatures = await roiSet.getAllFeatures();

                    // Delete user defined ROI Set if it is empty
                    if (Object.keys(userDefinedFeatures).length === 0) {
                        roiManager.deleteUserDefinedROISet();
                    }
                    roiManager.deleteRegionWithKey(regionElement.dataset.region, columnContainer);
                    roiManager.repaintTable();
                }
            }
        );
    }

    #addCopySequenceMenuItem(items, feature) {
        items.push({
            label: 'Copy reference sequence',
            click: async () => {
                this.container.style.display = 'none';
                let sequence = await this.browser.genome.getSequence(feature.chr, feature.start, feature.end);
                if (!sequence) {
                    sequence = "Unknown sequence";
                }
                try {
                    await navigator.clipboard.writeText(sequence);
                } catch (e) {
                    console.error(e);
                    this.browser.alert.present(undefined, `Failed to copy the sequence to the clipboard. (${e.message})`);
                }
            }
        });
    }

    #addDescriptionMenuItem(items, feature, event) {
        items.push(
            {
                label: 'Set description ...',
                click: () => {
                    const callback = () => {
                        const value = this.browser.inputDialog.value || '';
                        feature.name = value.trim();
                        this.browser.roiManager.repaintTable();
                    };
                    const config = {
                        label: 'Description',
                        value: (feature.name || ''),
                        callback
                    };

                    this.browser.inputDialog.present(config, event);
                }
            }
        );
    }

    #addSortMenuItems(items, feature) {

        const found = this.browser.findTracks(track => typeof track.sortByValue === 'function');
        if (found.length > 0) {

            items.push(`<hr/>`);

            const { chr, start, end } = feature;
            items.push({
                    label: 'Sort by value (ascending)',
                    click: () => Promise.all(found.map(track => track.sortByValue({ option: 'VALUE', direction: 'ASC', chr, start, end })))
                });

            items.push('<hr style="border: none; height: 1px; background-color: white; margin-top: 1px; margin-bottom: 1px;" />');

            items.push({
                    label: 'Sort by value (descending)',
                    click: () => Promise.all(found.map(track => track.sortByValue({ option: 'VALUE', direction: 'DESC', chr, start, end })))
                });
        }
    }



    #addBlatMenuItem(items, feature) {
        items.push({
            label: 'BLAT reference sequence',
            click: async () => {
                this.container.style.display = 'none';
                const {chr, start, end} = feature;
                let sequence = await this.browser.genome.getSequence(chr, start, end);
                if (sequence) {
                    const name = `blat: ${chr}:${start + 1}-${end}`;
                    const title = `blat: ${chr}:${start + 1}-${end}`;
                    createBlatTrack({sequence, browser: this.browser, name, title});
                }
            }
        });
    }

    dispose() {
        this.container.innerHTML = '';
    }

}

class ROITable extends RegionTableBase {
    constructor(browser) {

        const config =
            {
                browser: browser,
                parent: browser.columnContainer,
                headerTitle: 'Regions of Interest',
                dismissHandler: () => browser.roiTableControl.buttonHandler(false),
                gotoButtonHandler: ROITable.gotoButtonHandler
            };
        config.columnFormat = ROITable.getColumnFormatConfiguration(true);

        super(Object.assign({ 'width':'640px' }, config));
    }

    tableRowDOM(record) {

        const dom = div$2({ class: 'igv-roi-table-row' });

        const { setName, feature } = record;
        dom.dataset.region = createRegionKey(feature.chr, feature.start, feature.end);

        let strings =
            [
                feature.chr,
                numberFormatter$1(feature.start),
                numberFormatter$1(feature.end),
                feature.name || '',
                setName
            ];

        if (4 === this.columnFormat.length) {
            strings = strings.slice(0, 4);
        }

        for (let i = 0; i < strings.length; i++) {
            const el = div$2();
            dom.appendChild(el);
            el.style.width = this.columnFormat[ i ].width;
            el.innerText = strings[ i ];
        }

        this.tableRowDOMHelper(dom);

        return dom
    }

    renderTable(records) {

        Array.from(this.tableRowContainer.querySelectorAll('.igv-roi-table-row')).forEach(el => el.remove());

        if (records.length > 0) {

            const sortedRecords = records.sort((a, b) => (a.feature.chr.localeCompare(b.feature.chr) || a.feature.start - b.feature.start || a.feature.end - b.feature.end));

            for (let record of sortedRecords) {
                const row = this.tableRowDOM(record);
                this.tableRowContainer.appendChild(row);
            }

        }
    }

    set footerDOM(gotoButtonHandler) {

        super.footerDOM = gotoButtonHandler;

        this.gotoButton.textContent = 'Go to selected region(s)';


        // Copy Sequence Button
        const copySequenceButton = div$2({ class: 'igv-roi-table-button' });
        this._footerDOM.appendChild(copySequenceButton);

        copySequenceButton.id = 'igv-roi-hide-show-button';
        copySequenceButton.textContent = 'Copy Sequence';
        copySequenceButton.title = 'One region only of max size 1 mb';
        this.copySequenceButton = copySequenceButton;

        enableButton(this.copySequenceButton, false);

        async function copySequenceButtonHandler(event) {
            event.preventDefault();
            event.stopPropagation();

            // capture loci
            const selected = this.tableDOM.querySelectorAll('.igv-roi-table-row-selected');

            if (selected.length > 0) {

                const loci = [];
                for (let el of selected) {
                    const { locus } = parseRegionKey(el.dataset.region);
                    loci.push(locus);
                }

                // unselect
                for (let el of this.tableDOM.querySelectorAll('.igv-roi-table-row')) {
                    el.classList.remove('igv-roi-table-row-selected');
                }

                this.setTableRowSelectionState(false);

                if (loci.length > 0) {
                    const { chr, start, end } = parseLocusString(loci[0], this.browser.isSoftclipped());
                    const seq = await this.browser.genome.getSequence(chr, start, end);
                    await navigator.clipboard.writeText(seq);

                }
            }


        }

        this.boundCopySequenceButtonHandler = copySequenceButtonHandler.bind(this);
        this.copySequenceButton.addEventListener('click', this.boundCopySequenceButtonHandler);

        // Hide/Show Button
        const toggleROIButton = div$2({ class: 'igv-roi-table-button' });
        this._footerDOM.appendChild(toggleROIButton);

        toggleROIButton.id = 'igv-roi-hide-show-button';
        toggleROIButton.textContent = 'Hide Overlays';
        this.toggleROIButton = toggleROIButton;

        function toggleROIButtonHandler(event) {
            event.preventDefault();
            event.stopPropagation();
            this.roiManager.toggleROIs();
        }

        this.boundToggleDisplayButtonHandler = toggleROIButtonHandler.bind(this);
        this.toggleROIButton.addEventListener('click', this.boundToggleDisplayButtonHandler);
    }

    // This is a rather roundabot way to get the manager
    get roiManager() {
        return this.browser.roiManager
    }


    setTableRowSelectionState(isTableRowSelected) {
        super.setTableRowSelectionState(isTableRowSelected);
        const selected = this.tableDOM.querySelectorAll('.igv-roi-table-row-selected');

        if (selected.length > 0 && selected.length < 2) {
            const { locus } = parseRegionKey(selected[ 0 ].dataset.region);
            const { chr, start, end } = parseLocusString(locus, this.browser.isSoftclipped());
            enableButton(this.copySequenceButton, (end - start) < 1e6);
        } else {
            enableButton(this.copySequenceButton, false);
        }


    }

    dispose() {

        document.removeEventListener('click', this.boundGotoButtonHandler);
        document.removeEventListener('click', this.boundCopySequenceButtonHandler);
        document.removeEventListener('click', this.boundToggleDisplayButtonHandler);

        this.browser.roiTableControl.buttonHandler(false);
        super.dispose();
    }

    static getColumnFormatConfiguration(doIncludeROISetNames) {

        if (true === doIncludeROISetNames) {

            return [
                    { label: 'Chr', width: '20%' },
                    { label: 'Start', width: '15%' },
                    { label: 'End', width: '15%' },
                    { label: 'Description', width: '30%' },
                    { label: 'ROI Set', width: '20%' }
                ]
        } else {
            return [
                    { label: 'Chr', width: '25%' },
                    { label: 'Start', width: '20%' },
                    { label: 'End', width: '20%' },
                    { label: 'Description', width: '35%' }
                ]
        }

    }

    static gotoButtonHandler (event) {

        event.stopPropagation();

        const selected = this.tableDOM.querySelectorAll('.igv-roi-table-row-selected');
        const loci = [];
        for (let el of selected) {
            const { locus } = parseRegionKey(el.dataset.region);
            loci.push(locus);
        }

        for (let el of this.tableDOM.querySelectorAll('.igv-roi-table-row')) {
            el.classList.remove('igv-roi-table-row-selected');
        }

        this.setTableRowSelectionState(false);

        if (loci.length > 0) {
            this.browser.search(loci.join(' '));
        }

    }

}

function enableButton(button, doEnable) {
    button.style.pointerEvents = doEnable ? 'auto' : 'none';
    button.style.color = doEnable ? appleCrayonRGB('licorice') : appleCrayonRGB('silver');
    button.style.borderColor = doEnable ? appleCrayonRGB('licorice') : appleCrayonRGB('silver');

}

class ROIManager {

    constructor(browser) {

        this.browser = browser;
        this.roiMenu = new ROIMenu(browser, browser.columnContainer);
        this.roiTable = new ROITable(browser, browser.columnContainer);
        this.top = 0;
        this.roiSets = [];
        this.showOverlays = true;

        this.boundLocusChangeHandler = locusChangeHandler.bind(this);
        browser.on('locuschange', this.boundLocusChangeHandler);

    }

    async reset() {

        if (this.roiSets.length > 0) {
            this.browser.roiTableControl.setVisibility(true);
        }

        const promises = this.roiSets.map(roiSet => this.renderROISet({
            browser: this.browser,
            pixelTop: this.top,
            roiSet
        }));

        if (promises.length > 0) {
            await Promise.all(promises);
        }

        const records = await this.getTableRecords();
        this.roiTable.renderTable(records);

        if (this.roiSets.length > 0) {
            this.setOverlayVisibility(this.showOverlays);
        }
    }

    setOverlayVisibility(isVisible) {

        const elements = this.browser.columnContainer.querySelectorAll('.igv-roi-region');
        for (let i = 0; i < elements.length; i++) {
            const el = elements[i];
            if (isVisible) {
                // Restore element background color to its original value
                el.style.backgroundColor = el.dataset.color;
            } else {
                // Hide overlay by setting its transparency to zero.  A bit of an unusual method to hide an element.
                el.style.backgroundColor = `rgba(0, 0, 0, 0)`;
            }
        }
        this.roiTable.toggleROIButton.textContent = false === isVisible ? 'Show Overlays' : 'Hide Overlays';
    }

    async loadROI(config, genome) {

        const configs = Array.isArray(config) ? config : [config];

        // Backward compatibility hack
        if (configs.length > 0 && configs[0].isVisible === false) {
            this.showOverlays = false;
        }

        for (let config of configs) {
            if (!config.name && config.url) {
                config.name = await getFilename$1(config.url);
            }
            if (config.url && !config.format) {
                config.format = await inferFileFormat(config);
            }
            this.roiSets.push(new ROISet(config, genome));
        }

        await this.reset();

    }

    clearROIs() {

        this.roiTable.clearTable();

        const elements = this.browser.columnContainer.querySelectorAll('.igv-roi-region');
        for (let el of elements) {
            el.remove();
        }

        for (let roiSet of this.roiSets) {
            roiSet.dispose();
        }

        this.roiSets = [];

    }

    async getTableRecords() {

        const records = [];

        for (let roiSet of this.roiSets) {
            const setName = (roiSet.name || '');
            const allFeatures = await roiSet.getAllFeatures();
            for (let chr of Object.keys(allFeatures)) {
                for (let feature of allFeatures[chr]) {
                    records.push({setName, feature});
                }
            }
        }

        return records
    }

    presentTable() {
        this.roiTable.present();
    }

    async repaintTable() {
        const records = await this.getTableRecords();
        this.roiTable.renderTable(records);
    }

    dismissTable() {
        this.roiTable.dismiss();
    }

    roiTableIsVisible() {
        return this.roiTable.isVisible()
    }

    async updateUserDefinedROISet(feature) {

        let userDefinedROISet = await this.getUserDefinedROISet();

        if (undefined === userDefinedROISet) {
            userDefinedROISet = this.initializeUserDefinedROISet();
        }

        userDefinedROISet.addFeature(feature);

        this.setROITableButtonVisibility(true);

        await this.renderROISet({browser: this.browser, pixelTop: this.top, roiSet: userDefinedROISet});

        const records = await this.getTableRecords();
        this.roiTable.renderTable(records);
    }

    setROITableButtonVisibility(isVisible) {
        this.browser.roiTableControl.setVisibility(isVisible);
    }

    toggleROIs() {
        this.showOverlays = !this.showOverlays;
        this.setOverlayVisibility(this.showOverlays);
    }

    async renderAllROISets() {

        for (let roiSet of this.roiSets) {
            await this.renderROISet({browser: this.browser, pixelTop: this.top, roiSet});
        }
    }

    async renderROISet({browser, pixelTop, roiSet}) {

        const columns = browser.columnContainer.querySelectorAll('.igv-column');

        for (let i = 0; i < columns.length; i++) {

            let {chr, start: viewStart, end: viewEnd, bpPerPixel} = browser.referenceFrameList[i];

            const elements = columns[i].querySelectorAll('.igv-roi-region');
            for (let el of elements) {
                const regionKey = el.dataset.region;
                const {chr: regionChr, start: regionStart, end: regionEnd} = parseRegionKey(regionKey);
                if (regionChr !== chr || regionEnd < viewStart || regionStart > viewEnd) {
                    el.remove();
                }
            }

            const features = await roiSet.getFeatures(chr, viewStart, viewEnd);

            if (features) {

                for (let feature of features) {
                    const regionKey = createRegionKey(chr, feature.start, feature.end);

                    const {
                        x: pixelX,
                        width: pixelWidth
                    } = screenCoordinates(Math.max(viewStart, feature.start), Math.min(viewEnd, feature.end), viewStart, bpPerPixel);


                    const el = columns[i].querySelector(createSelector(regionKey));

                    if (el) {
                        el.style.left = `${pixelX}px`;
                        el.style.width = `${pixelWidth}px`;

                    } else {
                        const element = this.createRegionElement(browser.columnContainer, pixelTop, pixelX, pixelWidth, roiSet, regionKey, feature);
                        columns[i].appendChild(element);
                    }
                }
            }
        }
    }

    createRegionElement(columnContainer, pixelTop, pixelX, pixelWidth, roiSet, regionKey, feature) {

        const regionElement = div$2({class: 'igv-roi-region'});

        regionElement.style.top = `${pixelTop}px`;
        regionElement.style.left = `${pixelX}px`;
        regionElement.style.width = `${pixelWidth}px`;

        const marginTop = `${this.getROIRegionTopMargin()}px`;
        regionElement.style.marginTop = marginTop;

        regionElement.dataset.color = roiSet.color;
        regionElement.dataset.region = regionKey;

        if (this.showOverlays) {
            // Restore element background color to its original value
            regionElement.style.backgroundColor = roiSet.color;
        } else {
            // Hide overlay by setting its transparency to zero.  A bit of an unusual method to hide an element.
            regionElement.style.backgroundColor = `rgba(0, 0, 0, 0)`;
        }

        const header = div$2();
        regionElement.appendChild(header);

        header.style.backgroundColor = roiSet.headerColor;

        header.addEventListener('click', event => {
            event.preventDefault();
            event.stopPropagation();

            translateMouseCoordinates(event, columnContainer);
            this.roiMenu.present(feature, roiSet, event, this, columnContainer, regionElement);
        });

        return regionElement
    }

    updateROIRegionPositions() {
        const top = `${this.getROIRegionTopMargin()}px`;
        const columns = this.browser.columnContainer.querySelectorAll('.igv-column');
        for (let i = 0; i < columns.length; i++) {
            const elements = columns[i].querySelectorAll('.igv-roi-region');
            for (let j = 0; j < elements.length; j++) {
                elements[j].style.marginTop = top;
            }
        }
    }

    getROIRegionTopMargin() {

        const browser = this.browser;

        const tracks = browser.findTracks(track => new Set(['ideogram', 'ruler']).has(track.type));

        const [rectA, rectB] = tracks
            .map(track => track.trackView.viewports[0].viewportElement)
            .map(element => getElementVerticalDimension(element));

        //Covers cases in which ruler and/or ideogram are hidden
        const heightA = rectA ? rectA.height : 0;
        const heightB = rectB ? rectB.height : 0;

        const fudge = -0.5;
        const roiRegionMargin = heightA + heightB + fudge;

        return roiRegionMargin
    }

    renderSVGContext(columnContainer, context, {deltaX, deltaY}) {

        for (const regionElement of columnContainer.querySelectorAll('.igv-roi-region')) {

            // body
            const {x, y, width, height} = regionElement.getBoundingClientRect();
            context.fillStyle = regionElement.style.backgroundColor;
            context.fillRect(x - deltaX, y + deltaY, width, height);

            // header
            const header = regionElement.querySelector('div');
            const {x: xx, y: yy, width: ww, height: hh} = header.getBoundingClientRect();
            context.fillStyle = header.style.backgroundColor;
            context.fillRect(xx - deltaX, yy + deltaY, ww, hh);
        }
    }

    getUserDefinedROISet() {
        return this.roiSets.find(roiSet => true === roiSet.isUserDefined)
    }

    deleteUserDefinedROISet() {
        this.roiSets = this.roiSets.filter(roiSet => roiSet.isUserDefined !== true);
    }

    initializeUserDefinedROISet() {

        const config =
            {
                name: 'user defined',
                isUserDefined: true,
                features: []
            };
        const userDefinedROISet = new ROISet(config, this.browser.genome);
        this.roiSets.push(userDefinedROISet);

        return userDefinedROISet
    }

    async deleteRegionWithKey(regionKey, columnContainer) {
        columnContainer.querySelectorAll(createSelector(regionKey)).forEach(node => node.remove());

        const records = await this.getTableRecords();

        if (0 === records.length) {
            this.browser.roiTableControl.buttonHandler(false);
            this.setROITableButtonVisibility(false);
        }

    }

    toJSON() {
        return this.roiSets.map(roiSet => roiSet.toJSON())
    }

    dispose() {

        this.browser.off('locuschange', this.boundLocusChangeHandler);

        const removable = this.browser.columnContainer.querySelectorAll('.igv-roi-region');

        for (let el of removable) {
            el.remove();
        }

        if (this.roiMenu) {
            this.roiMenu.dispose();
        }

        if (this.roiTable) {
            this.roiTable.dispose();
        }

        for (let roiSet of this.roiSets) {
            roiSet.dispose();
        }

        for (let key of Object.keys(this)) {
            this[key] = undefined;
        }

    }
}

function locusChangeHandler() {
    this.renderAllROISets();
}

function createRegionKey(chr, start, end) {
    return `${chr}-${start}-${end}`
}

function createSelector(regionKey) {
    return `[data-region="${regionKey}"]`
}

function parseRegionKey(regionKey) {
    let regionParts = regionKey.split('-');
    let ee = parseInt(regionParts.pop());
    let ss = parseInt(regionParts.pop());
    let chr = regionParts.join('-');

    return {chr, start: ss, end: ee, locus: `${chr}:${ss}-${ee}`, bedRecord: `${chr}\t${ss}\t${ee}`}
}

class TrackROISet {

    constructor(config, genome) {
        this.name = config.name;
        this.featureSource = config.featureSource || FeatureSource(config, genome);
        this.color = config.color || ROI_DEFAULT_COLOR;
    }

    async getFeatures(chr, start, end) {
        return this.featureSource.getFeatures({chr, start, end})
    }

    draw(drawConfiguration) {

        const { context, bpPerPixel, bpStart, pixelTop, pixelHeight, pixelWidth, features, } = drawConfiguration;

        if (!features) {
            return
        }

        const bpEnd = bpStart + (pixelWidth * bpPerPixel) + 1;
        for (let { start:regionStartBP, end:regionEndBP } of features) {

            if (regionEndBP < bpStart) {
                continue
            }

            if (regionStartBP > bpEnd) {
                break
            }

            const { x, width } = screenCoordinates(regionStartBP, regionEndBP, bpStart, bpPerPixel);
            IGVGraphics.fillRect(context, x, pixelTop, width, pixelHeight, { fillStyle: this.color });
        }
    }
}

/*
 * @author Jim Robinson Dec-2020
 */

class IGVRemoteFile {


    constructor(args) {
        this.config = args;
        this.url = args.path || args.url;
    }


    async read(position, length) {

        const range = {start: position, size: length};

        return igvxhr$1.loadArrayBuffer(this.url, {range});

    }
}

class BrowserLocalFile {

    constructor(blob) {
        this.file = blob;
    }

    async read(position, length) {
        const file = this.file;
        if (position !== undefined) {
            return file.slice(position, position + length).arrayBuffer()

        } else {
            return file.arrayBuffer()

        }
    }
}

class ThrottledFile {

    constructor(file, rateLimiter) {
        this.file = file;
        this.rateLimiter = rateLimiter;
    }


    async read(position, length) {

        const file = this.file;
        const rateLimiter = this.rateLimiter;

        return new Promise(function (fulfill, reject) {
            rateLimiter.limiter(async function (f) {
                try {
                    const result = await f.read(position, length);
                    fulfill(result);
                } catch (e) {
                    reject(e);
                }
            })(file);
        })
    }
}

class RateLimiter {

    constructor(wait) {
        this.wait = wait === undefined ? 100 : wait;

        this.isCalled = false;
        this.calls = [];
    }


    limiter(fn) {

        const self = this;

        let caller = function () {

            if (self.calls.length && !self.isCalled) {
                self.isCalled = true;
                self.calls.shift().call();
                setTimeout(function () {
                    self.isCalled = false;
                    caller();
                }, self.wait);
            }
        };

        return function () {
            self.calls.push(fn.bind(this, ...arguments));
            caller();
        };
    }

}

class BufferedFile {

    constructor(args) {
        this.file = args.file;
        this.size = args.size || 64000;
        this.position = 0;
        this.bufferStart = 0;
        this.bufferLength = 0;
        this.buffer = undefined;
    }


    async read(position, length) {

        const start = position;
        const end = position + length;
        const bufferStart = this.bufferStart;
        const bufferEnd = this.bufferStart + this.bufferLength;


        if (length > this.size) {
            // Request larger than max buffer size,  pass through to underlying file
            //console.log("0")
            this.buffer = undefined;
            this.bufferStart = 0;
            this.bufferLength = 0;
            return this.file.read(position, length)
        }

        if (start >= bufferStart && end <= bufferEnd) {
            // Request within buffer bounds
            //console.log("1")
            const sliceStart = start - bufferStart;
            const sliceEnd = sliceStart + length;
            return this.buffer.slice(sliceStart, sliceEnd)
        }

        else if (start < bufferStart && end > bufferStart) {
            // Overlap left, here for completness but this is an unexpected case in straw.  We don't adjust the buffer.
            //console.log("2")
            const l1 = bufferStart - start;
            const a1 = await this.file.read(position, l1);
            const l2 = length - l1;
            if (l2 > 0) {
                //this.buffer = await this.file.read(bufferStart, this.size)
                const a2 = this.buffer.slice(0, l2);
                return concatBuffers(a1, a2)
            } else {
                return a1
            }

        }

        else if (start < bufferEnd && end > bufferEnd) {
            // Overlap right
            // console.log("3")
            const l1 = bufferEnd - start;
            const sliceStart = this.bufferLength - l1;
            const a1 = this.buffer.slice(sliceStart, this.bufferLength);

            const l2 = length - l1;
            if (l2 > 0) {
                try {
                    this.buffer = await this.file.read(bufferEnd, this.size);
                    this.bufferStart = bufferEnd;
                    this.bufferLength = this.buffer.byteLength;
                    const a2 = this.buffer.slice(0, l2);
                    return concatBuffers(a1, a2)
                } catch (e) {
                    // A "unsatisfiable range" error is expected here if we overlap past the end of file
                    if (e.code && e.code === 416) {
                        return a1
                    }
                    else {
                        throw e
                    }
                }

            } else {
                return a1
            }

        }

        else {
            // No overlap with buffer
            // console.log("4")
            this.buffer = await this.file.read(position, this.size);
            this.bufferStart = position;
            this.bufferLength = this.buffer.byteLength;
            return this.buffer.slice(0, length)
        }

    }

}

/**
 * concatenates 2 array buffers.
 * Credit: https://gist.github.com/72lions/4528834
 *
 * @private
 * @param {ArrayBuffers} buffer1 The first buffer.
 * @param {ArrayBuffers} buffer2 The second buffer.
 * @return {ArrayBuffers} The new ArrayBuffer created out of the two.
 */
var concatBuffers = function (buffer1, buffer2) {
    var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
    tmp.set(new Uint8Array(buffer1), 0);
    tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
    return tmp.buffer;
};

// TODO -- big endian

const BinaryParser = function (dataView, littleEndian) {

    this.littleEndian = littleEndian !== undefined ? littleEndian : true;
    this.position = 0;
    this.view = dataView;
    this.length = dataView.byteLength;
};

BinaryParser.prototype.available = function () {
    return this.length - this.position;
};

BinaryParser.prototype.remLength = function () {
    return this.length - this.position;
};

BinaryParser.prototype.hasNext = function () {
    return this.position < this.length - 1;
};

BinaryParser.prototype.getByte = function () {
    var retValue = this.view.getUint8(this.position, this.littleEndian);
    this.position++;
    return retValue;
};

BinaryParser.prototype.getShort = function () {

    var retValue = this.view.getInt16(this.position, this.littleEndian);
    this.position += 2;
    return retValue;
};

BinaryParser.prototype.getUShort = function () {

    // var byte1 = this.getByte(),
    //     byte2 = this.getByte(),
    //     retValue = ((byte2 << 24 >>> 16) + (byte1 << 24 >>> 24));
    //     return retValue;

    //
    var retValue = this.view.getUint16(this.position, this.littleEndian);
    this.position += 2;
    return retValue;
};


BinaryParser.prototype.getInt = function () {

    var retValue = this.view.getInt32(this.position, this.littleEndian);
    this.position += 4;
    return retValue;
};


BinaryParser.prototype.getUInt = function () {
    var retValue = this.view.getUint32(this.position, this.littleEndian);
    this.position += 4;
    return retValue;
};

BinaryParser.prototype.getLong = function () {

    // DataView doesn't support long. So we'll try manually

    var b = [];
    b[0] = this.view.getUint8(this.position);
    b[1] = this.view.getUint8(this.position + 1);
    b[2] = this.view.getUint8(this.position + 2);
    b[3] = this.view.getUint8(this.position + 3);
    b[4] = this.view.getUint8(this.position + 4);
    b[5] = this.view.getUint8(this.position + 5);
    b[6] = this.view.getUint8(this.position + 6);
    b[7] = this.view.getUint8(this.position + 7);

    var value = 0;
    if (this.littleEndian) {
        for (var i = b.length - 1; i >= 0; i--) {
            value = (value * 256) + b[i];
        }
    } else {
        for (var i = 0; i < b.length; i++) {
            value = (value * 256) + b[i];
        }
    }


    this.position += 8;
    return value;
};

BinaryParser.prototype.getString = function (len) {

    var s = "";
    var c;
    while ((c = this.view.getUint8(this.position++)) != 0) {
        s += String.fromCharCode(c);
        if (len && s.length == len) break;
    }
    return s;
};

BinaryParser.prototype.getFixedLengthString = function (len) {

    var s = "";
    var i;
    var c;
    for (i = 0; i < len; i++) {
        c = this.view.getUint8(this.position++);
        if (c > 0) {
            s += String.fromCharCode(c);
        }
    }
    return s;
};

BinaryParser.prototype.getFixedLengthTrimmedString = function (len) {

    var s = "";
    var i;
    var c;
    for (i = 0; i < len; i++) {
        c = this.view.getUint8(this.position++);
        if (c > 32) {
            s += String.fromCharCode(c);
        }
    }
    return s;
};

BinaryParser.prototype.getFloat = function () {

    var retValue = this.view.getFloat32(this.position, this.littleEndian);
    this.position += 4;
    return retValue;


};

BinaryParser.prototype.getDouble = function () {

    var retValue = this.view.getFloat64(this.position, this.littleEndian);
    this.position += 8;
    return retValue;
};

BinaryParser.prototype.skip = function (n) {

    this.position += n;
    return this.position;
};


/**
 * Return a bgzip (bam and tabix) virtual pointer
 * TODO -- why isn't 8th byte used ?
 * @returns {*}
 */
BinaryParser.prototype.getVPointer = function () {

    var position = this.position,
        offset = (this.view.getUint8(position + 1) << 8) | (this.view.getUint8(position)),
        byte6 = ((this.view.getUint8(position + 6) & 0xff) * 0x100000000),
        byte5 = ((this.view.getUint8(position + 5) & 0xff) * 0x1000000),
        byte4 = ((this.view.getUint8(position + 4) & 0xff) * 0x10000),
        byte3 = ((this.view.getUint8(position + 3) & 0xff) * 0x100),
        byte2 = ((this.view.getUint8(position + 2) & 0xff)),
        block = byte6 + byte5 + byte4 + byte3 + byte2;
    this.position += 8;

    //       if (block == 0 && offset == 0) {
    //           return null;
    //       } else {
    return new VPointer(block, offset);
    //       }
};


function VPointer(block, offset) {
    this.block = block;
    this.offset = offset;
}

VPointer.prototype.isLessThan = function (vp) {
    return this.block < vp.block ||
        (this.block === vp.block && this.offset < vp.offset);
};

VPointer.prototype.isGreaterThan = function (vp) {
    return this.block > vp.block ||
        (this.block === vp.block && this.offset > vp.offset);
};

VPointer.prototype.print = function () {
    return "" + this.block + ":" + this.offset;
};

class MatrixZoomData {

    constructor(chr1, chr2) {

        this.chr1 = chr1;    // chromosome index
        this.chr2 = chr2;
    }

    getKey () {
        return this.chr1.name + "_" + this.chr2.name + "_" + this.zoom.unit + "_" + this.zoom.binSize;
    }

    getBlockNumbers(region1, region2, version) {

        // Verify region chromosomes and swap if neccessary
        if(region1.chr == this.chr2 && region2.chr === this.chr1) {
            const tmp = region1;
            region1 = region2;
            region2 = tmp;
        }

        const sameChr = this.chr1 === this.chr2;
        const binsize = this.zoom.binSize;
        const blockBinCount = this.blockBinCount;
        const blockColumnCount = this.blockColumnCount;
        return (version < 9 || !sameChr) ? getBlockNumbersV8() : getBlockNumbersV9();

        function getBlockNumbersV8()
        {
            const x1 = region1.start / binsize;
            const x2 = region1.end / binsize;
            const y1 = region2.start / binsize;
            const y2 = region2.end / binsize;

            const col1 = Math.floor(x1 / blockBinCount);
            const col2 = Math.floor((x2 - 1) / blockBinCount);
            const row1 = Math.floor(y1 / blockBinCount);
            const row2 = Math.floor((y2 - 1) / blockBinCount);

            const blockNumbers = [];
            for (let row = row1; row <= row2; row++) {
                for (let column = col1; column <= col2; column++) {
                    let blockNumber;
                    if (sameChr && row < column) {
                        blockNumber = column * blockColumnCount + row;
                    } else {
                        blockNumber = row * blockColumnCount + column;
                    }
                    if (!blockNumbers.includes(blockNumber)) {  // possible from transposition
                        blockNumbers.push(blockNumber);
                    }
                }
            }
            return blockNumbers
        }

        function getBlockNumbersV9()
        {

            const binX1 = region1.start / binsize;
            const binX2 = region1.end / binsize;
            const binY1 = region2.start / binsize;
            const binY2 = region2.end / binsize;

            // PAD = positionAlongDiagonal (~projected)
            // Depth is axis perpendicular to diagonal; nearer means closer to diagonal
            const translatedLowerPAD = Math.floor((binX1 + binY1) / 2 / blockBinCount);
            const translatedHigherPAD = Math.floor((binX2 + binY2) / 2 / blockBinCount);
            const translatedNearerDepth = Math.floor(Math.log2(1 + Math.abs(binX1 - binY2) / Math.sqrt(2) / blockBinCount));
            const translatedFurtherDepth = Math.floor(Math.log2(1 + Math.abs(binX2 - binY1) / Math.sqrt(2) / blockBinCount));

            // because code above assume above diagonal; but we could be below diagonal
            const containsDiagonal = (binX2 - binY1) * (binX1 - binY2) < 0;   // i.e. sign of (x-y) opposite on 2 corners
            const nearerDepth = containsDiagonal ? 0 : Math.min(translatedNearerDepth, translatedFurtherDepth);
            const furtherDepth = Math.max(translatedNearerDepth, translatedFurtherDepth);

            const blockNumbers = [];
            for (let depth = nearerDepth; depth <= furtherDepth; depth++) {
                for (let pad = translatedLowerPAD; pad <= translatedHigherPAD; pad++) {
                    const block_number = depth * blockColumnCount + pad;
                    blockNumbers.push(block_number);
                }
            }
            return blockNumbers
        }
    }

    static parseMatrixZoomData(chr1, chr2, dis) {

        const zd = new MatrixZoomData(chr1, chr2);

        const unit = dis.getString();
        const zoomIndex = dis.getInt();
        const sumCounts = dis.getFloat();
        const occupiedCellCount = dis.getFloat();
        const stdDev = dis.getFloat();
        const percent95 = dis.getFloat();
        const binSize = dis.getInt();
        zd. blockBinCount = dis.getInt();
        zd. blockColumnCount = dis.getInt();
        const nBlocks = dis.getInt();

        zd. zoom = {index: zoomIndex, unit: unit, binSize: binSize};

        zd.blockIndex = new StaticBlockIndex(nBlocks, dis);

        const nBins1 = (chr1.size / binSize);
        const nBins2 = (chr2.size / binSize);
        const avgCount = (sumCounts / nBins1) / nBins2;   // <= trying to avoid overflows

        zd.averageCount = avgCount;
        zd.sumCounts = sumCounts;
        zd.stdDev = stdDev;
        zd.occupiedCellCount = occupiedCellCount;
        zd.percent95 = percent95;

        return zd;
    }
}


class StaticBlockIndex {

    constructor(nBlocks, dis) {
        this.blockIndex = {};
        while (nBlocks-- > 0) {
            const blockNumber = dis.getInt();
            const filePosition = dis.getLong();
            const size = dis.getInt();
            this.blockIndex[blockNumber] = {filePosition, size};
        }
    }

    getBlockIndexEntry(blockNumber) {
        return this.blockIndex[blockNumber];
    }
}

class Matrix {

    constructor(chr1, chr2, zoomDataList) {
        this.chr1 = chr1;
        this.chr2 = chr2;
        this.bpZoomData = [];
        this.fragZoomData = [];
        for (let zd of zoomDataList) {
            if (zd.zoom.unit === "BP") {
                this.bpZoomData.push(zd);
            } else {
                this.fragZoomData.push(zd);
            }
        }
    }

    /**
     * Find the best zoom level for the given bin size
     * @param binSize
     * @param unit
     * @returns {number}
     */
    findZoomForResolution(binSize, unit) {
        const zdArray = "FRAG" === unit ? this.fragZoomData : this.bpZoomData;
        for (let i = 1; i < zdArray.length; i++) {
            var zd = zdArray[i];
            if (zd.zoom.binSize < binSize) {
                return i - 1
            }
        }
        return zdArray.length - 1
    }

    /**
     * Fetch zoom data by bin size.  If no matching level exists return undefined.
     *
     * @param unit
     * @param binSize
     * @param zoom
     * @returns {undefined|*}
     */
    getZoomData(binSize, unit) {
        unit = unit || "BP";
        const zdArray = unit === "BP" ? this.bpZoomData : this.fragZoomData;
        for (let i = 0; i < zdArray.length; i++) {
            var zd = zdArray[i];
            if (binSize === zd.zoom.binSize) {
                return zd
            }
        }
        return undefined
    }

    /**
     * Return zoom data by resolution index.
     * @param index
     * @param unit
     * @returns {*}
     */
    getZoomDataByIndex(index, unit) {
        const zdArray = "FRAG" === unit ? this.fragZoomData : this.bpZoomData;
        return zdArray[index]
    }

    static getKey(chrIdx1, chrIdx2) {
        if (chrIdx1 > chrIdx2) {
            const tmp = chrIdx1;
            chrIdx1 = chrIdx2;
            chrIdx2 = tmp;
        }
        return `${chrIdx1}_${chrIdx2}`;
    }

    static parseMatrix(data, chromosomes) {

        const dis = new BinaryParser(new DataView(data));
        const c1 = dis.getInt();     // Should equal chrIdx1
        const c2 = dis.getInt();     // Should equal chrIdx2

        // TODO validate this
        const chr1 = chromosomes[c1];
        const chr2 = chromosomes[c2];

        // # of resolution levels (bp and frags)
        let nResolutions = dis.getInt();
        const zdList = [];

        while (nResolutions-- > 0) {
            const zd = MatrixZoomData.parseMatrixZoomData(chr1, chr2, dis);
            zdList.push(zd);
        }
        return new Matrix(c1, c2, zdList);
    }

}

class ContactRecord {

    constructor(bin1, bin2, counts) {
        this.bin1 = bin1;
        this.bin2 = bin2;
        this.counts = counts;
    };

    getKey() {
        return "" + this.bin1 + "_" + this.bin2;
    }
}

class LRU {
    constructor(max = 10) {
        this.max = max;
        this.map = new Map();
    }

    get(key) {
        let item = this.map.get(key);
        if (item) {
            // refresh key
            this.map.delete(key);
            this.map.set(key, item);
        }
        return item;
    }

    set(key, val) {
        // refresh key
        if (this.map.has(key)) this.map.delete(key);
        // evict oldest
        else if (this.map.size === this.max) {
            this.map.delete(this.first());
        }
        this.map.set(key, val);
    }

    has(key) {
        return this.map.has(key);
    }

    clear() {
        this.map.clear();
    }

    first() {
        return this.map.keys().next().value;
    }
}

const DOUBLE$1 = 8;

class NormalizationVector {

    constructor(file, filePosition, nValues, dataType) {
        this.file = file;
        this.filePosition = filePosition;
        this.nValues = nValues;
        this.dataType = dataType;
        this.cache = undefined;
    }

    async getValues(start, end) {

        if(!this.cache || start < this.cache.start || end > this.cache.end) {
            const adjustedStart = Math.max(0, start - 1000);
            const adjustedEnd = Math.min(this.nValues, end + 1000);
            const startPosition = this.filePosition + adjustedStart * this.dataType;
            const n = adjustedEnd - adjustedStart;
            const sizeInBytes = n  * this.dataType;
            const data = await this.file.read(startPosition, sizeInBytes);
            if (!data) {
                return undefined;
            }
            const parser = new BinaryParser(new DataView(data));

            const values = [];
            for (let i = 0; i < n; i++) {
                values[i] = this.dataType === DOUBLE$1 ? parser.getDouble() : parser.getFloat();

            }
            this.cache = {
                start: adjustedStart,
                end: adjustedEnd,
                values: values
            };
        }

        const sliceStart = start - this.cache.start;
        const sliceEnd = sliceStart + (end - start);
        return this.cache.values.slice(sliceStart, sliceEnd);
    }

    getKey() {
        return NormalizationVector.getKey(this.type, this.chrIdx, this.unit, this.resolution);
    }


    static getNormalizationVectorKey(type, chrIdx, unit, resolution) {
        return type + "_" + chrIdx + "_" + unit + "_" + resolution;
    }
}

// Contains "normalization vector index" (nvi values) for legacy (v8) hosted files from aidenlab, ENCODE, and 4DN.
// These values can speed up initialization by a few seconds.
var nvi = {
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2Fcombined.hic": "54386046426,55860",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2Fprimary.hic": "33860030033,37504",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2Freplicate.hic": "30849652794,55832",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2Fmaternal.hic": "1380542661,17185",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2Fpaternal.hic": "1389001777,17185",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fimr90%2Fin-situ%2Fcombined.hic": "13433880319,35723",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC001.hic": "3409347253,35975",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC002.hic": "4529947083,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC003.hic": "6894962500,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC004.hic": "3395593338,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC005.hic": "5204128636,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC006.hic": "3278363811,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC007.hic": "3585547340,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC008.hic": "4134197273,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC009.hic": "2851728310,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC010.hic": "1754216102,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC011.hic": "1716829574,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC012.hic": "4367980375,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC013.hic": "2309654671,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC014.hic": "5324950266,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC015.hic": "3132333594,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC016.hic": "3159154704,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC017.hic": "3262554627,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC018.hic": "3014216364,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC019.hic": "5025753246,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC020.hic": "5861584507,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC021.hic": "3542263275,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC022.hic": "5061949378,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC023.hic": "4816423919,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC024.hic": "2683781104,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC025.hic": "6522701781,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC026.hic": "6748846520,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC027.hic": "5642693007,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC028.hic": "2098921691,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC029.hic": "3886454027,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fagar%2FHIC030.hic": "2288984204,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fagar%2FHIC031.hic": "2115342419,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fpellet%2FHIC032.hic": "2063314324,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fsupernatant%2FHIC033.hic": "1933908457,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fdilution%2FHIC034.hic": "13555514595,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fdilution%2FHIC035.hic": "6129650900,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fdilution%2FHIC036.hic": "4307445019,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fdilution%2FHIC037.hic": "9375139867,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fdilution%2Fcombined.hic": "11664249584,33929",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2Fcombined_DpnII.hic": "7003537290,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC038.hic": "2099825544,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC039.hic": "2096900138,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC040.hic": "2593642141,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC041.hic": "2263919098,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC042.hic": "2684774693,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2Fcombined_noXlink.hic": "5574807456,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC043.hic": "2986114347,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC044.hic": "1916378621,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC045.hic": "746463126,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC046.hic": "2338476164,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC047.hic": "1167496250,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC048.hic": "2231690794,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fin-situ%2FHIC049.hic": "2782308678,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fimr90%2Fin-situ%2FHIC050.hic": "3633531346,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fimr90%2Fin-situ%2FHIC051.hic": "4063486444,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fimr90%2Fin-situ%2FHIC052.hic": "929756278,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fimr90%2Fin-situ%2FHIC053.hic": "2489657204,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fimr90%2Fin-situ%2FHIC054.hic": "4036419444,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fimr90%2Fin-situ%2FHIC055.hic": "4284185549,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fimr90%2Fin-situ%2FHIC056.hic": "4410546240,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fimr90%2Fdilution%2FHIC057.hic": "1491998259,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhmec%2Fin-situ%2Fcombined.hic": "7266425111,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhmec%2Fin-situ%2FHIC058.hic": "6532635593,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhmec%2Fin-situ%2FHIC059.hic": "400684530,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhmec%2Fin-situ%2FHIC060.hic": "645352187,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhmec%2Fin-situ%2FHIC061.hic": "736291871,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhmec%2Fin-situ%2FHIC062.hic": "762731037,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhmec%2Fin-situ%2FHIC063.hic": "682917431,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhmec%2Fdilution%2FHIC064.hic": "4550171307,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fnhek%2Fin-situ%2Fcombined.hic": "11897184911,35789",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fnhek%2Fin-situ%2FHIC065.hic": "5425943567,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fnhek%2Fin-situ%2FHIC066.hic": "5942869818,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fnhek%2Fin-situ%2FHIC067.hic": "5191049261,35647",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fnhek%2Fdilution%2FHIC068.hic": "2713318801,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fk562%2Fin-situ%2Fcombined.hic": "12641555389,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fk562%2Fin-situ%2FHIC069.hic": "5630177461,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fk562%2Fin-situ%2FHIC070.hic": "5926611269,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fk562%2Fin-situ%2FHIC071.hic": "2486191351,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fk562%2Fin-situ%2FHIC072.hic": "2281343366,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fk562%2Fin-situ%2FHIC073.hic": "2173074239,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fk562%2Fin-situ%2FHIC074.hic": "2463700999,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fkbm7%2Fin-situ%2Fcombined.hic": "14661922519,35639",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fkbm7%2Fin-situ%2FHIC075.hic": "3685310515,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fkbm7%2Fin-situ%2FHIC076.hic": "5833658487,35891",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fkbm7%2Fin-situ%2FHIC077.hic": "2406346486,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fkbm7%2Fin-situ%2FHIC078.hic": "5241454987,35919",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fkbm7%2Fin-situ%2FHIC079.hic": "6297234263,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhuvec%2Fin-situ%2Fcombined.hic": "9193858096,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhuvec%2Fin-situ%2FHIC080.hic": "5171405536,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhuvec%2Fin-situ%2FHIC081.hic": "3489767688,35891",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhuvec%2Fin-situ%2FHIC082.hic": "4243339375,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhuvec%2Fdilution%2FHIC083.hic": "1497495406,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhela%2Fin-situ%2Fcombined.hic": "7923332224,35593",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhela%2Fin-situ%2FHIC084.hic": "867699123,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhela%2Fin-situ%2FHIC085.hic": "3669518514,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhela%2Fin-situ%2FHIC086.hic": "5306474712,35595",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhela%2Fin-situ%2FHIC087.hic": "1490963755,36397",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fin-situ%2Fcombined.hic": "8956382853,50248",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fin-situ%2FHIC088.hic": "1874185366,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fin-situ%2FHIC089.hic": "1301165236,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fin-situ%2FHIC090.hic": "1105246282,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fin-situ%2FHIC091.hic": "1338325770,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fin-situ%2FHIC092.hic": "1307918730,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fin-situ%2FHIC093.hic": "755136918,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fin-situ%2FHIC094.hic": "1606583694,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fin-situ%2FHIC095.hic": "2039711245,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fin-situ%2FHIC096.hic": "1909573053,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fin-situ%2FHIC097.hic": "1826710712,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fin-situ%2FHIC098.hic": "1998793575,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fin-situ%2FHIC099.hic": "1007740962,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fin-situ%2FHIC100.hic": "736464120,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fin-situ%2FHIC101.hic": "980505082,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fin-situ%2FHIC102.hic": "1116083646,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fdilution%2FHIC103.hic": "1096994310,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fch12-lx-b-lymphoblasts%2Fdilution%2FHIC104.hic": "841063882,32102",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC105.hic": "107742864,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC106.hic": "134295912,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC107.hic": "110228014,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC108.hic": "102761401,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC109.hic": "100461491,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC110.hic": "88828472,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC111.hic": "81471064,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC112.hic": "92297035,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC113.hic": "65503916,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC114.hic": "49181188,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC115.hic": "46471102,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC116.hic": "64284432,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC117.hic": "60434076,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC118.hic": "45248563,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC119.hic": "60288782,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC120.hic": "136888414,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC121.hic": "125009003,33681",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC122.hic": "154011688,34127",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC123.hic": "177663372,35293",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC124.hic": "382186226,35245",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC125.hic": "158160717,35271",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC127.hic": "110261177,34659",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC128.hic": "246131875,35509",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC129.hic": "260160119,35667",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC130.hic": "117479562,34681",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC131.hic": "81508929,34673",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC132.hic": "105959102,34895",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC133.hic": "91504812,33451",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC134.hic": "52222546,33737",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC135.hic": "57763085,34159",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC136.hic": "50618489,33731",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC137.hic": "193163606,33363",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC138.hic": "229876152,33777",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC139.hic": "201783666,33571",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC140.hic": "281163862,34125",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC141.hic": "275969727,33935",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC142.hic": "275020431,34125",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC144.hic": "223834810,34177",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC145.hic": "296632653,33937",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC146.hic": "290945216,33961",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC147.hic": "234459964,34231",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC148.hic": "295457850,34039",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC149.hic": "113569632,35695",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC150.hic": "304381897,33963",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC151.hic": "289314545,33805",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC152.hic": "284320182,33803",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC153.hic": "401393947,34659",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC154.hic": "188065488,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC155.hic": "299157119,33967",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC156.hic": "279289244,33787",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC157.hic": "254852119,33917",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC158.hic": "195969660,33477",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC159.hic": "185720388,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC160.hic": "227709279,33599",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC161.hic": "157225076,33209",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC162.hic": "105703444,32033",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC163.hic": "122665658,32121",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC164.hic": "228048231,33761",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC165.hic": "114440819,32011",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC166.hic": "285147558,33891",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC167.hic": "363116754,33943",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC168.hic": "119592158,31937",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC169.hic": "236853872,33785",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC170.hic": "195187639,33395",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC171.hic": "208044058,33309",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC172.hic": "144645916,32877",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC173.hic": "119115941,32203",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC175.hic": "178030481,33525",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC176.hic": "117220136,32169",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC177.hic": "128586196,32659",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC178.hic": "191419141,33629",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC179.hic": "141556748,32959",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC180.hic": "199825102,33185",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC181.hic": "301022688,33783",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC182.hic": "185384453,33205",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC183.hic": "202189264,36367",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC184.hic": "37464475,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC185.hic": "161938278,36099",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC186.hic": "133853166,36179",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC188.hic": "128068030,36237",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC189.hic": "148053886,36177",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC190.hic": "198946958,35803",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC191.hic": "226584564,36063",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC192.hic": "194412797,36341",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC193.hic": "176408194,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC194.hic": "207215649,36055",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC195.hic": "175185530,36315",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC196.hic": "168969337,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC197.hic": "121231252,36339",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC198.hic": "92713752,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC199.hic": "105130500,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC200.hic": "114526350,36479",
    "hicfiles.s3.amazonaws.com%2Fmiseq%2Frh2014%2FHIC201.hic": "122689717,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fhap1%2Fin-situ%2Fcombined.hic": "9614983328,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fexternal%2Fctbp_8_4_17%2Fall_intra_megabase_michrom.hic": "154205644,10960",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2Fdel-Xa-combined.hic": "8843305329,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2FdelXa-chrX-diploid.hic": "11173520,1498",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2FWT-combined.hic": "13365182403,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2FWT-chrX-diploid.hic": "17343459,1498",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2FHIC002.hic": "4279298739,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2FHIC003.hic": "3027365532,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2FHIC004.hic": "2896130835,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2FHIC005.hic": "4717830071,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2FHIC006.hic": "5066210154,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2FHIC007.hic": "3069516128,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2Fdel-Xi-combined.hic": "14551096423,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2FdelXi-chrX-diploid.hic": "17362477,1498",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2FHIC008.hic": "2572386655,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2FHIC009.hic": "2935931663,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2FHIC010.hic": "2019085070,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2FHIC011.hic": "6087610814,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2FHIC012.hic": "2809618194,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frpe1%2FDarrowHuntley-2015%2FHIC013.hic": "2572386655,36479",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fpatski%2FDarrowHuntley-2015%2Fcombined.hic": "6950423609,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fpatski%2FDarrowHuntley-2015%2Fpaternal.hic": "220592749,15691",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fpatski%2FDarrowHuntley-2015%2Fmaternal.hic": "323233095,15691",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fpatski%2FDarrowHuntley-2015%2FHIC014.hic": "3183235800,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fpatski%2FDarrowHuntley-2015%2FHIC015.hic": "1739539801,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fpatski%2FDarrowHuntley-2015%2FHIC016.hic": "1105912758,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fpatski%2FDarrowHuntley-2015%2FHIC017.hic": "1060773319,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fpatski%2FDarrowHuntley-2015%2FHIC018.hic": "1203679874,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fpatski%2FDarrowHuntley-2015%2FHIC019.hic": "3321955269,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Frhesus%2FDarrowHuntley-2015%2FHIC020.hic": "7108980626,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fgm12878%2Fcola%2FDarrowHuntley-2015%2FHIC021.hic": "3906448676,34955",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fassembly%2Faedes%2Fcombined.hic": "390780490,2245",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fassembly%2Fhs2-hic.hic": "1960284186,33561",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2Fassembly%2FCpipJ3.hic": "766279097,4381",
    "s3.amazonaws.com%2Fhicfiles%2Fexternal%2Fphanstiel%2Fupdated_O%2FSnyder_O.hic": "8046980555,18679",
    "s3.amazonaws.com%2Fhicfiles%2Fexternal%2Fphanstiel%2FA_inter.hic": "10986005145,18679",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Funtreated%2Funsynchronized%2Fcombined.hic": "30439217680,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftreated_6hr%2Funsynchronized%2Fcombined.hic": "33255540403,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2Fdeep%2F20min_withdraw_combined.hic": "11078436933,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2Fdeep%2F40min_withdraw_combined.hic": "11031830038,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2Fdeep%2F60min_withdraw_combined.hic": "10312725340,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2Fdeep%2F180min_withdraw_combined.hic": "10038610214,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Funtreated%2Funsynchronized%2FHIC001.hic": "8004335785,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Funtreated%2Funsynchronized%2FHIC002.hic": "8453215933,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Funtreated%2Funsynchronized%2FHIC003.hic": "9118406189,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Funtreated%2Funsynchronized%2FHIC004.hic": "5973342894,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Funtreated%2Funsynchronized%2FHIC005.hic": "6846576837,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Funtreated%2Funsynchronized%2FHIC006.hic": "6075148017,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Funtreated%2Funsynchronized%2FHIC007.hic": "8192366992,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftreated_6hr%2Funsynchronized%2FHIC008.hic": "6844159653,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftreated_6hr%2Funsynchronized%2FHIC009.hic": "7282063059,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftreated_6hr%2Funsynchronized%2FHIC010.hic": "10410578833,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftreated_6hr%2Funsynchronized%2FHIC011.hic": "8192366992,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftreated_6hr%2Funsynchronized%2FHIC012.hic": "7223422850,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftreated_6hr%2Funsynchronized%2FHIC013.hic": "7435986997,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftreated_6hr%2Funsynchronized%2FHIC014.hic": "9120113605,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Funtreated%2Fsynchronized%2Fcombined.hic": "4170577904,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Funtreated%2Fsynchronized%2FHIC015.hic": "1504084990,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Funtreated%2Fsynchronized%2FHIC016.hic": "1686244500,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Funtreated%2Fsynchronized%2FHIC017.hic": "1804802410,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Funtreated%2Fsynchronized%2FHIC018.hic": "1533942067,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftreated_6hr%2Fsynchronized%2Fcombined.hic": "4572390198,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftreated_6hr%2Fsynchronized%2FHIC019.hic": "1543609666,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftreated_6hr%2Fsynchronized%2FHIC020.hic": "1814133288,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftreated_6hr%2Fsynchronized%2FHIC021.hic": "1942698243,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftreated_6hr%2Fsynchronized%2FHIC022.hic": "1873587413,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2Funtreated%2FRao-2017-HIC049.hic": "405127557,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2Funtreated%2FRao-2017-HIC060.hic": "326681741,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2Funtreated%2FRao-2017-HIC061.hic": "300908673,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_20min%2FRao-2017-HIC050.hic": "481758242,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_20min%2FRao-2017-HIC051.hic": "417577401,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_40min%2FRao-2017-HIC052.hic": "419353227,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_40min%2FRao-2017-HIC053.hic": "411554953,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_60min%2FRao-2017-HIC054.hic": "386655891,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_60min%2FRao-2017-HIC055.hic": "402170701,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_240min%2FRao-2017-HIC057.hic": "735906308,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min%2FRao-2017-HIC058.hic": "443789827,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min%2FRao-2017-HIC059.hic": "491422615,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min%2FRao-2017-HIC062.hic": "320261375,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min%2FRao-2017-HIC063.hic": "306590499,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min_withdraw_20min%2FRao-2017-HIC032S.hic": "367051742,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min_withdraw_20min%2FRao-2017-HIC033S.hic": "344555339,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min_withdraw_40min%2FRao-2017-HIC036S.hic": "349392947,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min_withdraw_40min%2FRao-2017-HIC037S.hic": "317714554,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min_withdraw_60min%2FRao-2017-HIC040S.hic": "266786397,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min_withdraw_60min%2FRao-2017-HIC041S.hic": "279445530,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min_withdraw_180min%2FRao-2017-HIC044S.hic": "898418269,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min_withdraw_180min%2FRao-2017-HIC045S.hic": "961006347,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min_withdraw_360min%2FRao-2017-HIC064.hic": "833576408,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min_withdraw_360min%2FRao-2017-HIC065.hic": "840881418,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min_withdraw_1080min%2FRao-2017-HIC066.hic": "980958864,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min_withdraw_1080min%2FRao-2017-HIC067.hic": "882023129,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min_withdraw_1440min%2FRao-2017-HIC068.hic": "1110292630,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fdegron%2Ftime_course%2FAuxin_treated_360min_withdraw_1440min%2FRao-2017-HIC069.hic": "1001747023,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fexternal%2Frowley_nichols_mol_cell_2017%2FCP190_HiChIP.hic": "144107588,6976",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fexternal%2Frowley_nichols_mol_cell_2017%2FH3K27ac_HiChIP_combined.hic": "43171370,5233",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fexternal%2Frowley_nichols_mol_cell_2017%2FH3K27me3_HiChIP_combined.hic": "42906472,5233",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fexternal%2Frowley_nichols_mol_cell_2017%2FPol2_ChIAPET.hic": "41992430,6976",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fexternal%2Frowley_nichols_mol_cell_2017%2FPol2S2_HiChIP.hic": "145842743,6976",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fexternal%2Frowley_nichols_mol_cell_2017%2FArabidopsis_Wang2015_Liu2016.hic": "1134391078,4984",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fexternal%2Frowley_nichols_mol_cell_2017%2FElegans_Crane2015.hic": "387127568,8758",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fexternal%2Frowley_nichols_mol_cell_2017%2FNcrassa_Galazka2016.hic": "26555511,27725",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fexternal%2Frowley_nichols_mol_cell_2017%2FPfalciparum_trophozoite_Ay2014.hic": "15640893,13948",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fduan%2FE-Me-A.hic": "6897251,23348",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fduan%2FE-Me-B.hic": "4959088,23348",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fduan%2FE-Mp-A.hic": "5232286,23348",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fduan%2FE-Mp-B.hic": "5105292,23348",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fduan%2FH-Me-A.hic": "6900072,23348",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fduan%2FH-Me-B.hic": "8653258,23348",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fduan%2FH-Mp-A.hic": "7999058,23348",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fduan%2FH-Mp-B.hic": "8774657,23348",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdekker%2F4dn%2Fh1hesc.hic": "23671753603,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdekker%2F4dn%2Fh1hesc_rep1.hic": "23671754721,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdekker%2F4dn%2Fh1hesc_rep2.hic": "22269909910,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdekker%2F4dn%2Fhffc6_rep1.hic": "19016661622,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Flieberman_aiden_van_berkum%2FGM06990_NcoI.hic": "480725203,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Flieberman_aiden_van_berkum%2F302NH.hic": "99948617,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Flieberman_aiden_van_berkum%2FK562.hic": "470263605,36151",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Flieberman_aiden_van_berkum%2FGM06990_HindIII.hic": "293706559,36289",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Flieberman_aiden_van_berkum%2FGM06990_rep_HindIII.hic": "309673862,36087",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Frudan%2Fcanis-lupus-rep1.hic": "1531151371,56905",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Frudan%2Fcanis-lupus-rep2.hic": "2851437676,56905",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Frudan%2Fmouse-rep1.hic": "582052544,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Frudan%2Fmacaque-rep1.hic": "698509182,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Frudan%2Fmacaque-rep2.hic": "461410393,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Frudan%2Frabbit-rep1.hic": "691045338,33561",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Frudan%2Frabbit-rep2.hic": "668888336,33561",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fsexton%2Fcombined.hic": "50650087,4486",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fkalhor%2Ftcc-hindiii.hic": "1071142023,16500",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fkalhor%2Ftcc-mboi.hic": "1508851093,16448",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fkalhor%2Fnon-tcc-hindiii.hic": "395179229,36093",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fkalhor%2Ftcc-combined.hic": "1050423847,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdixon%2Fmm9-hindiii%2Fsplit-read-run.hic": "9599984876,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdixon%2Fhesc-hindiii.hic": "1952555632,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdixon%2Fimr90-hindiii.hic": "9691260435,18679",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdixon%2Fmm9-cortex.hic": "3712072953,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fzhang%2Fatm-i-scei-chr18.hic": "2083350075,16438",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fzhang%2Fatm-i-scei-chr2.hic": "3362049644,16360",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fzhang%2Fatm-i-scei-chr7.hic": "1658399854,16438",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fzhang%2Fwt-i-scei-chr15.hic": "866691249,16438",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fzhang%2Fwt-i-scei-chr2.hic": "3304782795,16386",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnagano%2Fcell-1.hic": "23673506,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnagano%2Fcell-2.hic": "26873280,30643",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnagano%2Fcell-3.hic": "31066362,30643",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnagano%2Fcell-4.hic": "16807724,30643",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnagano%2Fcell-5.hic": "28665065,30643",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnagano%2Fcell-6.hic": "13124158,30643",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnagano%2Fcell-7.hic": "15118945,30643",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnagano%2Fcell-8.hic": "68316814,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnagano%2Fcell-9.hic": "18965101,30643",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnagano%2Fcell-10.hic": "17020666,30643",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fjin%2Fimr90%2Fcombined.hic": "6526601242,36427",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fjin%2Fimr90%2Frep1.hic": "1861562891,36401",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fjin%2Fimr90%2Frep2.hic": "3061013635,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fjin%2Fimr90%2Frep3.hic": "1564588211,35969",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fjin%2Fimr90%2Frep4.hic": "1255543175,36153",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fjin%2Fimr90%2Frep5.hic": "974683253,36179",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fjin%2Fimr90%2Frep6.hic": "1040805629,36053",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fjin%2Fimr90-tnf-alpha%2Fcombined.hic": "8424080116,36401",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fjin%2Fimr90-tnf-alpha%2Frep1.hic": "2697976702,36295",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fjin%2Fimr90-tnf-alpha%2Frep3.hic": "1718610467,36159",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fjin%2Fimr90-tnf-alpha%2Frep4.hic": "2029519405,36101",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fjin%2Fimr90-tnf-alpha%2Frep5.hic": "985310855,36069",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fjin%2Fimr90-tnf-alpha%2Frep6.hic": "1418750480,35967",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fcheng%2Fmhh-call-4-cell-line.hic": "538380253,35351",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fcheng%2Fprimary-b-all.hic": "773764191,35793",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fcheng%2Fnormal-b-cell-line.hic": "336271459,35005",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fcheng%2Frl-cell-line.hic": "484807998,35351",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fhou%2Fkc167-biorep.hic": "135932435,8758",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fhou%2Fkc167-techrep1.hic": "160355961,8758",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fhou%2Fkc167-techrep2.hic": "188809887,8758",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Flin%2Fpre-prob-egs.hic": "2237792858,15483",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Flin%2Fprob-egs.hic": "5744720377,16438",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Flin%2Fprob-fa.hic": "2174866215,16438",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fli%2Fhct116-rnapii-pilot.hic": "396366166,35999",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fli%2Fhela-rnapii-pilot.hic": "882516759,35107",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fli%2Fk562-rnapii-pilot.hic": "1131576189,35969",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fli%2Fk562-rnapii-saturated.hic": "571583828,36291",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fli%2Fmcf7-rnapii-saturated.hic": "1282547841,36035",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fli%2Fnb4-rnapii-pilot.hic": "761614686,34861",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fjung%2Fmouse_sperm.hic": "1730231642,29184",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Ftang%2Fgm12878.hic": "1953518212,20505",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Frutledge%2FSRR1791299.hic": "5723484,11956",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Frutledge%2FSRR1791297.hic": "16534743,23348",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fzuin%2Fhek293t-rad21cv-hrv.hic": "5094525579,36289",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fzuin%2Fhek293t-rad21cv-hrv-rep1.hic": "2574358912,36373",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fzuin%2Fhek293t-rad21cv-hrv-rep2.hic": "3417099433,36243",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fzuin%2Fhek293t-rad21cv-tev.hic": "4785157552,36323",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fzuin%2Fhek293t-rad21cv-tev-rep1.hic": "2648268465,36321",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fzuin%2Fhek293t-rad21cv-tev-rep2.hic": "2994727980,36107",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fzuin%2Fhek293t-sirna-ctcf.hic": "5391791815,36063",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fzuin%2Fhek293t-sirna-ctcf-rep1.hic": "3325842095,36269",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fzuin%2Fhek293t-sirna-ctcf-rep2.hic": "3096215920,36167",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fzuin%2Fhek293t-sirna-ctrl.hic": "3830902383,36167",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fzuin%2Fhek293t-sirna-ctrl-rep1.hic": "1346712252,36079",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fzuin%2Fhek293t-sirna-ctrl_rep2.hic": "3098067367,36271",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fay%2Fp-falciparum-3d7-0h-hiseq.hic": "38174042,21889",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fay%2Fp-falciparum-3d7-18h-hiseq.hic": "26506099,21889",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fay%2Fp-falciparum-3d7-18h-hiseq-non-crosslinked_control.hic": "3545496,21889",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fay%2Fp-falciparum-3d7-36h-hiseq.hic": "70754194,21889",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnaumova%2Fhelas3%2Fg1mid-r1.hic": "200863476,17185",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnaumova%2Fhelas3%2Fm-r1.hic": "249021190,17185",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnaumova%2Fhelas3%2Fm-r2.hic": "212341157,17185",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnaumova%2Fhelas3ccl2p2%2Fg1-0-25fa.hic": "583572265,17185",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnaumova%2Fhelas3ccl2p2%2Fg1-1fa.hic": "354502306,17185",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnaumova%2Fhelas3ccl2p2%2Fm-0-25fa.hic": "149308791,17185",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnaumova%2Fhelas3ccl2p2%2Fm-98percent.hic": "778175225,17185",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnaumova%2Fhelas3ccl2p2%2Fm-1fa.hic": "158091047,17185",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnaumova%2Fhff1%2Fcchic-hff1-ns-r1.hic": "801670039,17185",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnaumova%2Fhff1%2Fcchic-hff1-m-r1.hic": "875922086,17185",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnaumova%2Fk562%2Fm-r1.hic": "130959943,17185",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnaumova%2Fk562%2Fm-r2.hic": "237861106,17185",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fsofueva%2FmAST-WT.hic": "496104039,30643",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fsofueva%2FmAST-WT-adv-cre.hic": "438608517,30643",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fsofueva%2FmAST-floxed-rep1.hic": "446205712,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fsofueva%2FmAST-floxed-rep2.hic": "386354767,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fsofueva%2FmAST-deleted-rep1.hic": "621998722,30643",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fsofueva%2FmAST-deleted-rep2.hic": "583840829,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fsofueva%2FmNSC-floxed-rep1.hic": "801290988,30643",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fsofueva%2FmNSC-floxed-rep2.hic": "819406658,30643",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fsofueva%2FmNSC-deleted-rep1.hic": "792825269,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fsofueva%2FmNSC-deleted-rep2.hic": "746189884,30643",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fsofueva%2FmNSC-WT.hic": "309353372,30643",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fsofueva%2FmNSC-WT-OHT.hic": "274229679,30643",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fseitan%2FTcell-Rad21KO-R1.hic": "1490362251,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fseitan%2FTcell-Rad21KO-R3.hic": "2403664421,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fseitan%2FTcell-Rad21WT-R1.hic": "1377124538,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fseitan%2FTcell-Rad21WT-R3.hic": "2646201369,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fmoissiard%2Fcrh6-mutant.hic": "105261071,10217",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fmoissiard%2FWT.hic": "127506388,10217",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fgrob%2FCol.hic": "153638500,10217",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fgrob%2Fcrwn1.hic": "211213220,10217",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fgrob%2Fcrwn4.hic": "134236837,10217",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fxie%2Fprimary.hic": "92936173,10217",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Ffeng%2F6wt-control6wt337.hic": "318737891,10217",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Ffeng%2F6wt-control6wt67.hic": "348123956,10217",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Ffeng%2F6wt337.hic": "251107279,10217",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Ffeng%2F6wt67.hic": "351840994,10217",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Ffeng%2FCol0.hic": "345969497,10217",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Ffeng%2Fclf28-swn7.hic": "299585025,10217",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Ffeng%2Fcmt3-11.hic": "253427706,10217",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Ffeng%2Fddm1-2.hic": "303626924,10217",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Ffeng%2Fmet1-3.hic": "347697973,10217",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Ffeng%2Fsuvh4-suvh5-suvh6.hic": "263122019,10217",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fma%2FDNase-HiC-WG-K562.hic": "892955840,18679",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fma%2FDNaseHiC-WG-H1.hic": "696922695,18679",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fma%2FtargetedDNaseHiC-lincRNA-K562-rep1.hic": "341917716,18679",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fma%2FtargeredDNaseHiC-lincRNA-K562-rep2.hic": "214841185,18679",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fma%2FtargetedDNaseHiC-lincRNA-H1-rep1.hic": "616326137,18679",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fma%2FtargetedDNaseHiC-lincRNA-H1-rep2.hic": "579654013,18679",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fma%2FtargetedDNaseHiC-pe-H1-rep1.hic": "519810544,18679",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fma%2FtargetedDNaseHiC-pe-K562-rep1.hic": "199553123,18679",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Frowley%2FKc167_DpnII.hic": "1319373594,6976",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Frowley%2FKc167_HinfI.hic": "1465493873,6976",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Frowley%2FKc167_DpnII_HinfI_combo.hic": "753062558,5814",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fbarutcu%2FMCF-10A.hic": "2784956115,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fbarutcu%2FMCF-7.hic": "2700457411,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fbatullin%2Fsperm.hic": "501052169,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fminajigi%2FWT_hiC_rep1.hic": "1450697530,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fminajigi%2FWT_hiC_rep2.hic": "1189371558,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fminajigi%2Fcombined.hic": "2262007875,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fminajigi%2Finactive.hic": "4444564,751",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fminajigi%2Factive.hic": "3993495,751",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdeng%2Fpatski_rep1.hic": "1650838357,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdeng%2Fpatski_rep2.hic": "1258038606,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdeng%2Fpatski.hic": "499827296,16438",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdeng%2Fpatski_active.hic": "79175714,15691",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdeng%2Fpatski_inactive.hic": "98947881,15691",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdeng%2Fbrain_dnase.hic": "4298088401,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdeng%2Fbrain_in_situ_dnase.hic": "609321852,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdeng%2Fbrain.hic": "1028196506,16438",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdeng%2Fbrain_active.hic": "79175714,15691",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fdeng%2Fbrain_inactive.hic": "98947881,15691",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fle%2FBglII_rep1.hic": "1283306,1463",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fle%2FBglII_rep2.hic": "1263227,1463",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fle%2FNcoI_rep1.hic": "4380906,1463",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Feagen%2FGSE89112_Kc167combined.hic": "1829549643,25624",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Feagen%2FGSE89112_Kc167combined_randomized.hic": "1859742654,25624",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fmumbach%2FGSE80820_HiChIP_GM_cohesin.hic": "1231264781,17932",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fmumbach%2FGSE80820_HiChIP_mES_cohesin_all.hic": "1152008628,16438",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fmumbach%2FGSE80820_HiChIP_mES_Oct4.hic": "912828146,16438",
    "s3.amazonaws.com%2Fhicfiles%2Fexternal%2Fwapl_hic%2FSCC4KO.hic": "1767906138,18679",
    "s3.amazonaws.com%2Fhicfiles%2Fexternal%2Fwapl_hic%2FDKO.hic": "1487228787,18679",
    "s3.amazonaws.com%2Fhicfiles%2Fexternal%2Fwapl_hic%2FWaplKO_1.14.hic": "1189751862,18679",
    "s3.amazonaws.com%2Fhicfiles%2Fexternal%2Fwapl_hic%2FWaplKO_3.3.hic": "1155057696,18679",
    "s3.amazonaws.com%2Fhicfiles%2Fexternal%2Fwapl_hic%2FWT.hic": "1522401568,18679",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fskfuwi4fnsjkdf9jekrw2%2Fmega_Liverpool%2Fmega.hic": "2612171975,4381",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fskfuwi4fnsjkdf9jekrw2%2FHIC2836_Liverpool1%2FLiverpool1.hic": "191729195,2245",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fskfuwi4fnsjkdf9jekrw2%2FHIC2861_Liverpool2%2FLiverpool2.hic": "1637096030,4381",
    "s3.amazonaws.com%2Fhicfiles%2Fhiseq%2Fskfuwi4fnsjkdf9jekrw2%2FHIC2862_Liverpool3%2FLiverpool3.hic": "1160045602,4381",
    "s3.amazonaws.com%2Fhicfiles%2Fexternal%2Fgoodell%2FHSPC.hic": "11857786247,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fexternal%2Fgoodell%2FHSPC_CanyonDel.hic": "8139168836,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fexternal%2Fgoodell%2Ftcell.hic": "9871547081,36479",
    "s3.amazonaws.com%2Fhicfiles%2Fexternal%2Fgoodell%2Fep.hic": "17479335373,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fnagano%2FTh1_ensemble.hic": "1690592868,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fbonev%2FES_mapq30.hic": "32930498831,32386",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fbonev%2FNPC_mapq30.hic": "55053730986,32386",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fbonev%2FCN_mapq30.hic": "53138031154,32386",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2FKieffer-Kwon_et_al_2017%2FMain_maps%2FKieffer-Kwon-2017-activated_B_cells_72_hours_WT.hic": "16492991184,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2FKieffer-Kwon_et_al_2017%2FMain_maps%2FKieffer-Kwon-2017-resting_B_cells_WT.hic": "23057795398,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2FKieffer-Kwon_et_al_2017%2FMain_maps%2FKieffer-Kwon-2017-activated_B_cells_24_hours_MYC_KO.hic": "21245554876,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2FKieffer-Kwon_et_al_2017%2FMain_maps%2FKieffer-Kwon-2017-resting_B_cells_TSA.hic": "13933346059,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2FKieffer-Kwon_et_al_2017%2FMain_maps%2FKieffer-Kwon-2017-activated_B_cells_24_hours_oligomycin.hic": "21902506832,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2FKieffer-Kwon_et_al_2017%2FMain_maps%2FKieffer-Kwon-2017-mES_WT.hic": "9451969795,32102",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2FVian_et_al_2018%2FMain_maps%2FVian-2018-activated_B_cells_24_hours_WT.hic": "3600188564,16438",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2FVian_et_al_2018%2FMain_maps%2FVian-2018-activated_B_cells_30_hours_WT.hic": "1309503359,16438",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2FVian_et_al_2018%2FMain_maps%2FVian-2018-activated_B_cells_30_hours_HU_treated.hic": "1477252396,16438",
    "hicfiles.s3.amazonaws.com%2Fhiseq%2FVian_et_al_2018%2FMain_maps%2FVian-2018-activated_B_cells_24_hours_flavopiridol.hic": "15711773490,32102",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fpgp%2FPGP1F_mega_090118%2FPGP1F_mega_090118_30.hic": "24786676166,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fpgp%2FHIC7145%2FHIC7145_30.hic": "4554644616,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fpgp%2FHIC7146%2FHIC7146_30.hic": "4174848944,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fpgp%2FHIC7147%2FHIC7147_30.hic": "5062229182,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fpgp%2FHIC7862%2FHIC7862_30.hic": "4520990620,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fpgp%2FHIC7863%2FHIC7863_30.hic": "4330354773,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fpgp%2FHIC7864%2FHIC7864_30.hic": "5880640268,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fpgp%2FHIC7865%2FHIC7865_30.hic": "4431632469,36479",
    "hicfiles.s3.amazonaws.com%2Fexternal%2Fpgp%2FHIC7866%2FHIC7866_30.hic": "4686000222,36479",
    "encode-public.s3.amazonaws.com%2F2018%2F10%2F19%2Fb122542b-306c-4631-bdc3-2e155e89af55%2FENCFF718AWL.hic": "39544267907,58932",
    "encode-public.s3.amazonaws.com%2F2019%2F02%2F15%2F8e787cd8-e388-4bc2-a236-7a5e6f39e0c0%2FENCFF999YXX.hic": "9255069996,56883",
    "encode-public.s3.amazonaws.com%2F2019%2F02%2F08%2Ffc1d9d5d-8fa0-4e29-9080-3da674d9490d%2FENCFF543USQ.hic": "5005263062,36479",
    "s3.us-east-1.wasabisys.com%2Fhicfiles%2Finternal%2FLCL_mega_42B_500bp_30.hic": "106482584851,22829",
    "www.encodeproject.org%2Ffiles%2FENCFF925QIF%2F%40%40download%2FENCFF925QIF.hic": "51712684396,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF259YUS%2F%40%40download%2FENCFF259YUS.hic": "24640030444,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF783KQI%2F%40%40download%2FENCFF783KQI.hic": "27209249856,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF318JAP%2F%40%40download%2FENCFF318JAP.hic": "28636979242,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF512PQA%2F%40%40download%2FENCFF512PQA.hic": "28124104361,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF591MHA%2F%40%40download%2FENCFF591MHA.hic": "37373387202,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF197OWW%2F%40%40download%2FENCFF197OWW.hic": "41826180893,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF317OIA%2F%40%40download%2FENCFF317OIA.hic": "34741636038,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF522YLZ%2F%40%40download%2FENCFF522YLZ.hic": "24064692329,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF420JTA%2F%40%40download%2FENCFF420JTA.hic": "21202405177,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF871ZDJ%2F%40%40download%2FENCFF871ZDJ.hic": "10124815680,18679",
    "www.encodeproject.org%2Ffiles%2FENCFF349RZY%2F%40%40download%2FENCFF349RZY.hic": "4446702705,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF738YON%2F%40%40download%2FENCFF738YON.hic": "3338049584,35947",
    "www.encodeproject.org%2Ffiles%2FENCFF406KJN%2F%40%40download%2FENCFF406KJN.hic": "4132875306,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF081NPN%2F%40%40download%2FENCFF081NPN.hic": "34495587443,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF946RZW%2F%40%40download%2FENCFF946RZW.hic": "34571955193,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF602CHT%2F%40%40download%2FENCFF602CHT.hic": "30674798032,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF150DKS%2F%40%40download%2FENCFF150DKS.hic": "12618769864,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF614IPZ%2F%40%40download%2FENCFF614IPZ.hic": "42450060331,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF571ZQR%2F%40%40download%2FENCFF571ZQR.hic": "15230068871,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF705MKK%2F%40%40download%2FENCFF705MKK.hic": "38686554702,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF658KTB%2F%40%40download%2FENCFF658KTB.hic": "13195884405,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF435JYN%2F%40%40download%2FENCFF435JYN.hic": "17192139674,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF016JKX%2F%40%40download%2FENCFF016JKX.hic": "37266736642,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF294GFP%2F%40%40download%2FENCFF294GFP.hic": "36389843399,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF700CYI%2F%40%40download%2FENCFF700CYI.hic": "60482012152,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF962EDB%2F%40%40download%2FENCFF962EDB.hic": "39394328182,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF467OGP%2F%40%40download%2FENCFF467OGP.hic": "35572788855,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF556RLR%2F%40%40download%2FENCFF556RLR.hic": "34826921187,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF309UNV%2F%40%40download%2FENCFF309UNV.hic": "48510618174,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF653HCO%2F%40%40download%2FENCFF653HCO.hic": "41079007620,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF120DSV%2F%40%40download%2FENCFF120DSV.hic": "12790195576,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF284OEA%2F%40%40download%2FENCFF284OEA.hic": "46007570339,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF035BLF%2F%40%40download%2FENCFF035BLF.hic": "63868871985,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF094KKX%2F%40%40download%2FENCFF094KKX.hic": "42151419971,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF965BPU%2F%40%40download%2FENCFF965BPU.hic": "42717376107,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF842DUO%2F%40%40download%2FENCFF842DUO.hic": "39553892515,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF417GBZ%2F%40%40download%2FENCFF417GBZ.hic": "45544785452,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF499BVX%2F%40%40download%2FENCFF499BVX.hic": "40900674170,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF136XCV%2F%40%40download%2FENCFF136XCV.hic": "47700392974,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF224HKR%2F%40%40download%2FENCFF224HKR.hic": "40426005932,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF711XSR%2F%40%40download%2FENCFF711XSR.hic": "41434581949,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF660JWA%2F%40%40download%2FENCFF660JWA.hic": "54201632697,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF081PMO%2F%40%40download%2FENCFF081PMO.hic": "53428285130,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF799QGA%2F%40%40download%2FENCFF799QGA.hic": "1228682189,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF473CAA%2F%40%40download%2FENCFF473CAA.hic": "1077514950,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF663FRL%2F%40%40download%2FENCFF663FRL.hic": "39843938245,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF341WOY%2F%40%40download%2FENCFF341WOY.hic": "34792598619,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF515ZBF%2F%40%40download%2FENCFF515ZBF.hic": "41779230923,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF545GBW%2F%40%40download%2FENCFF545GBW.hic": "38351980358,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF252ANV%2F%40%40download%2FENCFF252ANV.hic": "37668839771,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF912NDK%2F%40%40download%2FENCFF912NDK.hic": "21196851956,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF683PLM%2F%40%40download%2FENCFF683PLM.hic": "21849338763,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF355NFJ%2F%40%40download%2FENCFF355NFJ.hic": "47880627268,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF507WEW%2F%40%40download%2FENCFF507WEW.hic": "35777782924,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF241MSL%2F%40%40download%2FENCFF241MSL.hic": "22507408288,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF135MUT%2F%40%40download%2FENCFF135MUT.hic": "24168734040,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF459FRB%2F%40%40download%2FENCFF459FRB.hic": "37166770103,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF705YZH%2F%40%40download%2FENCFF705YZH.hic": "44679182442,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF395INO%2F%40%40download%2FENCFF395INO.hic": "56727346538,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF736ITL%2F%40%40download%2FENCFF736ITL.hic": "49036286185,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF546TZN%2F%40%40download%2FENCFF546TZN.hic": "33195126348,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF706SFK%2F%40%40download%2FENCFF706SFK.hic": "3271831702,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF922ERE%2F%40%40download%2FENCFF922ERE.hic": "12476647122,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF876OWE%2F%40%40download%2FENCFF876OWE.hic": "1538692704,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF227XJZ%2F%40%40download%2FENCFF227XJZ.hic": "1925617685,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF053BXY%2F%40%40download%2FENCFF053BXY.hic": "1032237652,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF014VMM%2F%40%40download%2FENCFF014VMM.hic": "1564021703,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF563XES%2F%40%40download%2FENCFF563XES.hic": "1489180508,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF482LGO%2F%40%40download%2FENCFF482LGO.hic": "1208778887,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF216QQM%2F%40%40download%2FENCFF216QQM.hic": "6293163571,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF256UOW%2F%40%40download%2FENCFF256UOW.hic": "6293163571,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF791UZC%2F%40%40download%2FENCFF791UZC.hic": "37029615181,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF911AHQ%2F%40%40download%2FENCFF911AHQ.hic": "34246649165,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF491AOR%2F%40%40download%2FENCFF491AOR.hic": "558663570,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF307PDL%2F%40%40download%2FENCFF307PDL.hic": "525321519,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF773ITV%2F%40%40download%2FENCFF773ITV.hic": "295464771,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF198SSL%2F%40%40download%2FENCFF198SSL.hic": "500142476,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF251UEF%2F%40%40download%2FENCFF251UEF.hic": "567443575,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF942LTN%2F%40%40download%2FENCFF942LTN.hic": "4430960222,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF543USQ%2F%40%40download%2FENCFF543USQ.hic": "5005263062,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF434XQU%2F%40%40download%2FENCFF434XQU.hic": "25506511674,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF318GOM%2F%40%40download%2FENCFF318GOM.hic": "44750824314,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF004TKY%2F%40%40download%2FENCFF004TKY.hic": "20705661833,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF493YNC%2F%40%40download%2FENCFF493YNC.hic": "53407154984,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF334XXU%2F%40%40download%2FENCFF334XXU.hic": "14341194962,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF569RJM%2F%40%40download%2FENCFF569RJM.hic": "1922066634,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF076LWH%2F%40%40download%2FENCFF076LWH.hic": "39406255845,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF977XWK%2F%40%40download%2FENCFF977XWK.hic": "20563512962,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF127TPS%2F%40%40download%2FENCFF127TPS.hic": "21032070512,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF480KLP%2F%40%40download%2FENCFF480KLP.hic": "33115569647,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF281ILS%2F%40%40download%2FENCFF281ILS.hic": "28236353584,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF129LMU%2F%40%40download%2FENCFF129LMU.hic": "35247096013,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF759YCW%2F%40%40download%2FENCFF759YCW.hic": "16058537743,32102",
    "www.encodeproject.org%2Ffiles%2FENCFF045YOM%2F%40%40download%2FENCFF045YOM.hic": "19775445828,32102",
    "www.encodeproject.org%2Ffiles%2FENCFF013TGD%2F%40%40download%2FENCFF013TGD.hic": "4208420532,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF929RPW%2F%40%40download%2FENCFF929RPW.hic": "1974260276,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF996XEO%2F%40%40download%2FENCFF996XEO.hic": "2072918865,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF097SKJ%2F%40%40download%2FENCFF097SKJ.hic": "1983524001,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF464KRA%2F%40%40download%2FENCFF464KRA.hic": "1957300131,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF406HHC%2F%40%40download%2FENCFF406HHC.hic": "3854527235,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF080DPJ%2F%40%40download%2FENCFF080DPJ.hic": "19953768024,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF621AIY%2F%40%40download%2FENCFF621AIY.hic": "26781700010,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF366ERB%2F%40%40download%2FENCFF366ERB.hic": "1049184239,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF997RGL%2F%40%40download%2FENCFF997RGL.hic": "2521167549,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF029MPB%2F%40%40download%2FENCFF029MPB.hic": "1854711185,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF043EEE%2F%40%40download%2FENCFF043EEE.hic": "2924133032,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF920CJR%2F%40%40download%2FENCFF920CJR.hic": "3011577886,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF928NJV%2F%40%40download%2FENCFF928NJV.hic": "2778936244,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF894GLR%2F%40%40download%2FENCFF894GLR.hic": "2831733099,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF303PCK%2F%40%40download%2FENCFF303PCK.hic": "688704435,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF999YXX%2F%40%40download%2FENCFF999YXX.hic": "9255069996,56883",
    "www.encodeproject.org%2Ffiles%2FENCFF685BLG%2F%40%40download%2FENCFF685BLG.hic": "16797022992,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF188SSH%2F%40%40download%2FENCFF188SSH.hic": "16797023050,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF465ESX%2F%40%40download%2FENCFF465ESX.hic": "13489465953,32102",
    "www.encodeproject.org%2Ffiles%2FENCFF493SFI%2F%40%40download%2FENCFF493SFI.hic": "30033046818,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF235LCO%2F%40%40download%2FENCFF235LCO.hic": "38572174589,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF625VNK%2F%40%40download%2FENCFF625VNK.hic": "45202258438,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF304HMS%2F%40%40download%2FENCFF304HMS.hic": "36994445315,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF605CEN%2F%40%40download%2FENCFF605CEN.hic": "40358346301,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF086ORS%2F%40%40download%2FENCFF086ORS.hic": "22650781304,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF237UKR%2F%40%40download%2FENCFF237UKR.hic": "41516707246,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF004YZQ%2F%40%40download%2FENCFF004YZQ.hic": "71900665326,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF104THR%2F%40%40download%2FENCFF104THR.hic": "15172410632,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF807IRK%2F%40%40download%2FENCFF807IRK.hic": "17346637451,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF193CQL%2F%40%40download%2FENCFF193CQL.hic": "55726903245,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF700DEX%2F%40%40download%2FENCFF700DEX.hic": "20231399798,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF234MDO%2F%40%40download%2FENCFF234MDO.hic": "24635312640,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF181ROW%2F%40%40download%2FENCFF181ROW.hic": "26883606497,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF896OFN%2F%40%40download%2FENCFF896OFN.hic": "36952196833,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF573OPJ%2F%40%40download%2FENCFF573OPJ.hic": "36685546919,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF028RXH%2F%40%40download%2FENCFF028RXH.hic": "31053919206,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF156GGD%2F%40%40download%2FENCFF156GGD.hic": "35284134289,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF496GEU%2F%40%40download%2FENCFF496GEU.hic": "44659660933,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF945TUH%2F%40%40download%2FENCFF945TUH.hic": "3898570191,35639",
    "www.encodeproject.org%2Ffiles%2FENCFF698KFV%2F%40%40download%2FENCFF698KFV.hic": "4427896684,35751",
    "www.encodeproject.org%2Ffiles%2FENCFF239BHZ%2F%40%40download%2FENCFF239BHZ.hic": "1718552021,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF277LAN%2F%40%40download%2FENCFF277LAN.hic": "4542391336,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF397CMD%2F%40%40download%2FENCFF397CMD.hic": "2832016430,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF778OYA%2F%40%40download%2FENCFF778OYA.hic": "14067187484,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF775VSU%2F%40%40download%2FENCFF775VSU.hic": "14067187484,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF812THZ%2F%40%40download%2FENCFF812THZ.hic": "9667720429,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF777KBU%2F%40%40download%2FENCFF777KBU.hic": "5058284320,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF688KOY%2F%40%40download%2FENCFF688KOY.hic": "4198695833,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF223UBX%2F%40%40download%2FENCFF223UBX.hic": "1206064037,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF273XBU%2F%40%40download%2FENCFF273XBU.hic": "44085607579,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF952JZV%2F%40%40download%2FENCFF952JZV.hic": "44728845246,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF643NDM%2F%40%40download%2FENCFF643NDM.hic": "35877886780,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF586MQY%2F%40%40download%2FENCFF586MQY.hic": "71158037547,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF251VFA%2F%40%40download%2FENCFF251VFA.hic": "40089479399,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF033WGK%2F%40%40download%2FENCFF033WGK.hic": "30802023728,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF649OHR%2F%40%40download%2FENCFF649OHR.hic": "28203184265,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF782WVZ%2F%40%40download%2FENCFF782WVZ.hic": "37248221674,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF579CAR%2F%40%40download%2FENCFF579CAR.hic": "39662025395,54779",
    "www.encodeproject.org%2Ffiles%2FENCFF514XWQ%2F%40%40download%2FENCFF514XWQ.hic": "1319613701,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF666USK%2F%40%40download%2FENCFF666USK.hic": "15051670107,32102",
    "www.encodeproject.org%2Ffiles%2FENCFF047SDP%2F%40%40download%2FENCFF047SDP.hic": "1357076705,16438",
    "www.encodeproject.org%2Ffiles%2FENCFF584LEP%2F%40%40download%2FENCFF584LEP.hic": "9314176858,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF671SOE%2F%40%40download%2FENCFF671SOE.hic": "3273732256,16438",
    "www.encodeproject.org%2Ffiles%2FENCFF702LAP%2F%40%40download%2FENCFF702LAP.hic": "1800427500,31682",
    "www.encodeproject.org%2Ffiles%2FENCFF339PON%2F%40%40download%2FENCFF339PON.hic": "5394351199,31514",
    "www.encodeproject.org%2Ffiles%2FENCFF741QPR%2F%40%40download%2FENCFF741QPR.hic": "4271510019,30139",
    "www.encodeproject.org%2Ffiles%2FENCFF103ANX%2F%40%40download%2FENCFF103ANX.hic": "4439745804,30083",
    "www.encodeproject.org%2Ffiles%2FENCFF395SBC%2F%40%40download%2FENCFF395SBC.hic": "1596732351,31850",
    "www.encodeproject.org%2Ffiles%2FENCFF289WNN%2F%40%40download%2FENCFF289WNN.hic": "8632980535,32102",
    "www.encodeproject.org%2Ffiles%2FENCFF381EAU%2F%40%40download%2FENCFF381EAU.hic": "1702476311,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF976FEY%2F%40%40download%2FENCFF976FEY.hic": "1448279040,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF485LVS%2F%40%40download%2FENCFF485LVS.hic": "1593610856,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF143VJV%2F%40%40download%2FENCFF143VJV.hic": "1422312076,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF588NHX%2F%40%40download%2FENCFF588NHX.hic": "3943637009,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF768UBD%2F%40%40download%2FENCFF768UBD.hic": "24786676166,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF965PEE%2F%40%40download%2FENCFF965PEE.hic": "29387092778,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF702IFC%2F%40%40download%2FENCFF702IFC.hic": "29387092778,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF288DNV%2F%40%40download%2FENCFF288DNV.hic": "10313429504,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF698HWZ%2F%40%40download%2FENCFF698HWZ.hic": "10962849269,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF355OWW%2F%40%40download%2FENCFF355OWW.hic": "3219571601,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF177TYX%2F%40%40download%2FENCFF177TYX.hic": "10325679706,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF542BHD%2F%40%40download%2FENCFF542BHD.hic": "11085397879,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF684IFW%2F%40%40download%2FENCFF684IFW.hic": "11085397879,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF592UHE%2F%40%40download%2FENCFF592UHE.hic": "21363948089,32102",
    "www.encodeproject.org%2Ffiles%2FENCFF779XIS%2F%40%40download%2FENCFF779XIS.hic": "9517383271,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF518PSL%2F%40%40download%2FENCFF518PSL.hic": "7500581463,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF275GBB%2F%40%40download%2FENCFF275GBB.hic": "7889407162,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF634KXI%2F%40%40download%2FENCFF634KXI.hic": "6430702966,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF337QPM%2F%40%40download%2FENCFF337QPM.hic": "8529733887,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF162KMP%2F%40%40download%2FENCFF162KMP.hic": "5615896908,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF992VTE%2F%40%40download%2FENCFF992VTE.hic": "7328975027,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF464WXY%2F%40%40download%2FENCFF464WXY.hic": "5715339664,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF752EGH%2F%40%40download%2FENCFF752EGH.hic": "1733758743,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF474UJM%2F%40%40download%2FENCFF474UJM.hic": "222309548,18679",
    "www.encodeproject.org%2Ffiles%2FENCFF531UPZ%2F%40%40download%2FENCFF531UPZ.hic": "1827756846,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF022VHA%2F%40%40download%2FENCFF022VHA.hic": "244983451,18679",
    "www.encodeproject.org%2Ffiles%2FENCFF732NGR%2F%40%40download%2FENCFF732NGR.hic": "1679484567,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF513VSF%2F%40%40download%2FENCFF513VSF.hic": "28397932940,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF750AOC%2F%40%40download%2FENCFF750AOC.hic": "36471430712,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF219YOB%2F%40%40download%2FENCFF219YOB.hic": "2859767211,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF604YDD%2F%40%40download%2FENCFF604YDD.hic": "3549694085,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF746AMV%2F%40%40download%2FENCFF746AMV.hic": "2954867135,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF845ZEB%2F%40%40download%2FENCFF845ZEB.hic": "2763039282,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF570LWS%2F%40%40download%2FENCFF570LWS.hic": "2861588289,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF883YVR%2F%40%40download%2FENCFF883YVR.hic": "2433679887,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF056VLK%2F%40%40download%2FENCFF056VLK.hic": "2502986910,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF532DUQ%2F%40%40download%2FENCFF532DUQ.hic": "3172615899,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF723PYJ%2F%40%40download%2FENCFF723PYJ.hic": "10292839095,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF991SGJ%2F%40%40download%2FENCFF991SGJ.hic": "13072407683,32102",
    "www.encodeproject.org%2Ffiles%2FENCFF885UZI%2F%40%40download%2FENCFF885UZI.hic": "1201881201,16438",
    "www.encodeproject.org%2Ffiles%2FENCFF287KXA%2F%40%40download%2FENCFF287KXA.hic": "14705801988,32102",
    "www.encodeproject.org%2Ffiles%2FENCFF335HYI%2F%40%40download%2FENCFF335HYI.hic": "20366756545,32102",
    "www.encodeproject.org%2Ffiles%2FENCFF121YPY%2F%40%40download%2FENCFF121YPY.hic": "2760104016,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF675SJE%2F%40%40download%2FENCFF675SJE.hic": "3751779995,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF876LAW%2F%40%40download%2FENCFF876LAW.hic": "4032376256,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF740KVX%2F%40%40download%2FENCFF740KVX.hic": "3202490322,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF977OQV%2F%40%40download%2FENCFF977OQV.hic": "2919735477,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF525EFN%2F%40%40download%2FENCFF525EFN.hic": "2394748073,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF690QRC%2F%40%40download%2FENCFF690QRC.hic": "3183023394,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF452FWS%2F%40%40download%2FENCFF452FWS.hic": "2951805127,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF089KBG%2F%40%40download%2FENCFF089KBG.hic": "2531936700,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF939ARM%2F%40%40download%2FENCFF939ARM.hic": "3066385212,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF401ZAN%2F%40%40download%2FENCFF401ZAN.hic": "3008677144,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF378RZT%2F%40%40download%2FENCFF378RZT.hic": "2830232168,26979",
    "www.encodeproject.org%2Ffiles%2FENCFF304AVD%2F%40%40download%2FENCFF304AVD.hic": "6164439305,50358",
    "www.encodeproject.org%2Ffiles%2FENCFF453DBX%2F%40%40download%2FENCFF453DBX.hic": "6775246670,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF590VOM%2F%40%40download%2FENCFF590VOM.hic": "6443478922,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF246DOF%2F%40%40download%2FENCFF246DOF.hic": "6837914659,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF125FXX%2F%40%40download%2FENCFF125FXX.hic": "9736232550,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF241RAY%2F%40%40download%2FENCFF241RAY.hic": "7686758397,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF415XWQ%2F%40%40download%2FENCFF415XWQ.hic": "6973092068,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF374EBH%2F%40%40download%2FENCFF374EBH.hic": "8557459058,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF263YWR%2F%40%40download%2FENCFF263YWR.hic": "331586820,18679",
    "www.encodeproject.org%2Ffiles%2FENCFF280CGS%2F%40%40download%2FENCFF280CGS.hic": "1743569614,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF553ZNS%2F%40%40download%2FENCFF553ZNS.hic": "1717253636,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF451VOI%2F%40%40download%2FENCFF451VOI.hic": "1626927468,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF420MQO%2F%40%40download%2FENCFF420MQO.hic": "31055961677,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF301BWY%2F%40%40download%2FENCFF301BWY.hic": "38902429246,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF606XNW%2F%40%40download%2FENCFF606XNW.hic": "1009699327,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF439ZOU%2F%40%40download%2FENCFF439ZOU.hic": "12381714313,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF499SYK%2F%40%40download%2FENCFF499SYK.hic": "13182654663,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF532LFI%2F%40%40download%2FENCFF532LFI.hic": "8286030286,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF020DPP%2F%40%40download%2FENCFF020DPP.hic": "23521952420,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF306VTV%2F%40%40download%2FENCFF306VTV.hic": "23521952480,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF399IHE%2F%40%40download%2FENCFF399IHE.hic": "10216571232,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF944AII%2F%40%40download%2FENCFF944AII.hic": "12567308552,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF230HVV%2F%40%40download%2FENCFF230HVV.hic": "8629849711,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF174LAF%2F%40%40download%2FENCFF174LAF.hic": "9446506633,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF898HRO%2F%40%40download%2FENCFF898HRO.hic": "9446506633,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF429MOR%2F%40%40download%2FENCFF429MOR.hic": "1455757398,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF061NXV%2F%40%40download%2FENCFF061NXV.hic": "1710915891,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF434PYS%2F%40%40download%2FENCFF434PYS.hic": "1832274903,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF704HHX%2F%40%40download%2FENCFF704HHX.hic": "1768142834,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF349DPM%2F%40%40download%2FENCFF349DPM.hic": "4316041260,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF718AWL%2F%40%40download%2FENCFF718AWL.hic": "39544267907,58932",
    "www.encodeproject.org%2Ffiles%2FENCFF065LSP%2F%40%40download%2FENCFF065LSP.hic": "24929805283,55874",
    "www.encodeproject.org%2Ffiles%2FENCFF632MFV%2F%40%40download%2FENCFF632MFV.hic": "22333105467,55798",
    "www.encodeproject.org%2Ffiles%2FENCFF179HVU%2F%40%40download%2FENCFF179HVU.hic": "2659219603,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF791SPZ%2F%40%40download%2FENCFF791SPZ.hic": "2537491657,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF211ZWF%2F%40%40download%2FENCFF211ZWF.hic": "3512141227,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF614BNU%2F%40%40download%2FENCFF614BNU.hic": "5252155462,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF891DLM%2F%40%40download%2FENCFF891DLM.hic": "2549095612,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF056EXT%2F%40%40download%2FENCFF056EXT.hic": "2219014704,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF776DSS%2F%40%40download%2FENCFF776DSS.hic": "3920308353,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF814LQF%2F%40%40download%2FENCFF814LQF.hic": "1349480197,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF513ZNA%2F%40%40download%2FENCFF513ZNA.hic": "2638868195,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF692IOL%2F%40%40download%2FENCFF692IOL.hic": "3106768498,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF294WZY%2F%40%40download%2FENCFF294WZY.hic": "2211089761,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF339UWS%2F%40%40download%2FENCFF339UWS.hic": "2142759055,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF835FSK%2F%40%40download%2FENCFF835FSK.hic": "3919821739,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF328BMB%2F%40%40download%2FENCFF328BMB.hic": "2550686152,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF897RTY%2F%40%40download%2FENCFF897RTY.hic": "1323294397,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF306MWH%2F%40%40download%2FENCFF306MWH.hic": "1665386199,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF039YUJ%2F%40%40download%2FENCFF039YUJ.hic": "2335679939,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF468QIN%2F%40%40download%2FENCFF468QIN.hic": "3352516594,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF364WXU%2F%40%40download%2FENCFF364WXU.hic": "3959043175,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF373MSH%2F%40%40download%2FENCFF373MSH.hic": "4429550624,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF959EGQ%2F%40%40download%2FENCFF959EGQ.hic": "2475218228,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF624XMK%2F%40%40download%2FENCFF624XMK.hic": "3527855681,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF445OKS%2F%40%40download%2FENCFF445OKS.hic": "3369125589,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF239QDV%2F%40%40download%2FENCFF239QDV.hic": "1960069888,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF645AVM%2F%40%40download%2FENCFF645AVM.hic": "4969006589,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF494IPI%2F%40%40download%2FENCFF494IPI.hic": "1565483792,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF753IXE%2F%40%40download%2FENCFF753IXE.hic": "2892400648,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF731GXX%2F%40%40download%2FENCFF731GXX.hic": "5131381303,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF369CAQ%2F%40%40download%2FENCFF369CAQ.hic": "4236629448,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF053VBX%2F%40%40download%2FENCFF053VBX.hic": "54305946375,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF555ISR%2F%40%40download%2FENCFF555ISR.hic": "54305946434,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF964RSP%2F%40%40download%2FENCFF964RSP.hic": "12065485229,36479",
    "www.encodeproject.org%2Ffiles%2FENCFF234MZQ%2F%40%40download%2FENCFF234MZQ.hic": "17507142760,47429",
    "www.encodeproject.org%2Ffiles%2FENCFF994CEG%2F%40%40download%2FENCFF994CEG.hic": "17507142760,47429",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fb068e787-bbdd-49e2-9f6d-7a0e9f065d6e%2F4DNFIHSUIRFY.hic": "2701182442,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3768b89a-e124-40eb-8acd-94a4fdd35807%2F4DNFIXTAS6EE.hic": "3283740601,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F9c513602-5abb-4088-8415-c827c381ab0d%2F4DNFIC3JD6O2.hic": "4896075767,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fde954dad-1849-4c23-beb4-bf893b7d76c9%2F4DNFIGY85S4W.hic": "3364151996,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fe1befa6e-8067-4296-8db2-67d2a2458985%2F4DNFI2LTNXME.hic": "3699291418,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fbb3307fd-7162-477a-87c5-52f12d03befc%2F4DNFID162B9J.hic": "3576428773,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F8597cdae-8319-42d9-9175-3f77aaee34e1%2F4DNFIQWS3632.hic": "4022561190,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F327f091d-6a63-47c4-9752-2dff303a13d9%2F4DNFI6GFHB6G.hic": "4239331168,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fd70dc3b8-48a7-42c2-bd23-80bc44fc2266%2F4DNFIS5F6EBL.hic": "4404671861,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F77df7bf8-6a3d-407a-8968-4ca1e78d2269%2F4DNFIF2D3TL9.hic": "4793989394,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fc8f7cc35-d2ef-4717-9009-c02154050fbf%2F4DNFI6EIFUZI.hic": "3370273763,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F98449d79-30d1-4e32-801d-07435819a42f%2F4DNFIRKGMZRQ.hic": "4669813207,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fd4fe1844-2493-4722-8b98-34a93a4bb7d8%2F4DNFIQWD8ZYD.hic": "5379889057,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F1ee40499-1de4-47ff-b540-d9a65943c6b8%2F4DNFIX7H3CE2.hic": "4857679239,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fac016170-23a7-4111-8773-353d6c150f34%2F4DNFIO21YDCV.hic": "4695989644,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F1faea4e7-9a32-466e-a826-0bfdab6cd915%2F4DNFIUE5RAS6.hic": "4788044799,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fcbd396ed-11bb-404f-aed9-ef10abc258e6%2F4DNFIFI6NIKJ.hic": "2069414927,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Faf3f78db-ae92-4215-bcae-76d30c617e5d%2F4DNFIL5K3MA1.hic": "2585598247,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F66fc2893-224e-48b4-bdb0-25e022dedd03%2F4DNFIPV5KFMK.hic": "2060965679,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F2b39781d-aadc-434f-abe3-562d61308fdd%2F4DNFIW8TIFI5.hic": "1694424195,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fb6d28f7b-27d3-486f-b5ac-32567416560d%2F4DNFIA6NSSBJ.hic": "2219532611,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F08785616-722e-46bb-ade2-2fdd21324e11%2F4DNFIBKZK63V.hic": "2429136232,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F904b13d3-1203-42a4-812c-963e80c159b1%2F4DNFI3CFMRID.hic": "1233390166,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fb0ed4afe-5ecf-451d-bb1f-444697e3f558%2F4DNFIYIAUEPW.hic": "1753567534,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F4bb9a9b3-12a8-4d2f-8071-4722294c88ea%2F4DNFIC3HAU11.hic": "2271375254,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F5abf9c36-c1d1-44dc-8197-74d860d896c0%2F4DNFIM2BMJ33.hic": "1770471113,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fa68475db-7816-4cec-b2da-d4ce4a0c7a0e%2F4DNFIBY3UHJB.hic": "2095528423,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F07c40943-0d0b-4b3e-94a0-8583b0e00b47%2F4DNFIWZEN3FF.hic": "1558559508,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fd8a39553-2718-4866-9b2a-45d8e9c5ac6f%2F4DNFIE5TEYK7.hic": "1323063102,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F294b61ef-5565-4f04-ade9-f921a6686cce%2F4DNFI7H4II2V.hic": "1436947982,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F87a70ad7-7d34-4302-9c03-eb824bd4e633%2F4DNFIAS8LV1C.hic": "1263105698,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F9db804c6-5a91-4cf5-8ae9-e054223afb77%2F4DNFIBXWIH8J.hic": "1587059951,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fd1e55906-ec8f-444b-8ce1-2de22a431a76%2F4DNFIO2HBDAI.hic": "1666436393,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F7a4b9b37-8e38-470c-8190-fd4d513c0bbb%2F4DNFIXWCLD4M.hic": "1513511880,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F6c7615b2-5310-4bbf-a605-98761d3a3079%2F4DNFINASC4NQ.hic": "1420358914,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F20454014-49cd-4e2d-93da-f40ad52776d5%2F4DNFIZF1KI43.hic": "1377041630,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F5df04e86-ee2a-45f2-b878-bf1648e2cca5%2F4DNFIQBHR9XC.hic": "1179502734,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F9d1cc910-bb3f-4106-802a-73184c58d5ba%2F4DNFIQVMVRP6.hic": "1811976991,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fe431986b-0732-460f-92ff-0abd1694d2d0%2F4DNFIJ7OAIFQ.hic": "1685503856,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F56d0c575-4962-4ea3-92c3-55bd0e17c096%2F4DNFIE8E4ANZ.hic": "1598369564,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fdb1502fb-a989-4ef9-b800-7eaa7be172ae%2F4DNFIOUSETUP.hic": "1493294709,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Facc2f1c3-8c96-4d21-814a-3a8ba81e7741%2F4DNFIVJ3IIQL.hic": "1472376655,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F037fb4c3-8641-4953-a977-cc71f2137582%2F4DNFIHN2QSMX.hic": "1179632606,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F16d2c3c0-6d29-4815-b074-b142af90b615%2F4DNFIILDWK8M.hic": "2280059989,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F31789849-e4db-45d8-9730-66a048c02d8a%2F4DNFI6XD6JAQ.hic": "2253961696,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fff04947e-e6e8-4d62-8374-ef2ee4104809%2F4DNFIALNLR78.hic": "1914687914,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F53e6c1e7-9410-4158-84e6-57c2e683b095%2F4DNFIBT4EAKN.hic": "2296378356,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F0127900a-258f-4815-97d2-6fd2b043d853%2F4DNFIDJ2JZ7Q.hic": "2558945393,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F89f95693-3d28-44eb-9d45-14932f8e258c%2F4DNFI8JFOAJT.hic": "1726440253,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fd803d818-b0d4-4796-8d61-a9f3943f3b10%2F4DNFIRZA6EV6.hic": "1805017504,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff9165fb1-0782-4757-a740-5b929e0252bb%2F4DNFIB7QTIMH.hic": "2541020707,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff8d916b2-b58e-4cdf-894c-ffcb3bf6ebd9%2F4DNFIJ5DQZVK.hic": "2445734043,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F14066a5b-e01a-43c2-a468-b18826174df3%2F4DNFISTWNV9C.hic": "2499319890,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fd0692151-7f9a-4475-b059-f8802616dded%2F4DNFIBN9K4V9.hic": "1591570769,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F70a7ef36-a514-4e45-918f-47c4e9e30ef4%2F4DNFIR8OU6QJ.hic": "1689882108,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F180306cd-8593-413a-ae83-24f19209c5d2%2F4DNFIDSWKFF5.hic": "1596696630,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F6e7b9be7-f465-4434-8e9c-68d1e44a76af%2F4DNFIV239W4L.hic": "2345867406,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F55df948c-941d-4930-854c-6a07557a0c1d%2F4DNFIREVU1Z5.hic": "2254683002,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3f590cb7-df3a-498f-81c3-d5ed4179b8ad%2F4DNFIFHU75I5.hic": "2497670883,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F8f064770-6008-4f74-bfca-268d4a22d745%2F4DNFIMROE6N4.hic": "9850583704,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fa93d0588-76b7-4ccf-933c-4325876b0a53%2F4DNFI5SUHCGZ.hic": "10364705187,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F2f5f0d6b-0b22-4936-93c9-0944d49c037b%2F4DNFII84FBKM.hic": "8836986129,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F12e90fc5-0b76-4e42-b346-58d267f9ad6e%2F4DNFI2J1ODWB.hic": "13410858286,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff2702a45-3e8b-4980-bcd5-6bb0e291614f%2F4DNFIJWBWE41.hic": "20515676755,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fa0859349-5f06-4ad3-b56f-b1166b34a9eb%2F4DNFIIMZB6Y9.hic": "15350325963,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F12685c57-d54a-46fa-9efb-acf34ed77574%2F4DNFICJECID5.hic": "1525002266,26979",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fd101a58e-89aa-44fd-96c5-6bf1e2476272%2F4DNFIR6UW2QA.hic": "1264999913,26979",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3d8c73bf-1335-4af4-b124-e3622d48c5f6%2F4DNFIGOEKBUZ.hic": "1905947578,26979",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fcd2477cf-6ed0-4d7b-b4e9-88fb38b0df48%2F4DNFIMDNAEW4.hic": "996444147,26979",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F0f03953e-29da-4f2e-8c05-572d0edf4525%2F4DNFI5OSZ3JC.hic": "369361754,22829",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F29d95dbb-005a-4ae1-8b9c-0fec9ec0bbd0%2F4DNFI7KOUGFM.hic": "1227434049,26979",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fa441ab7b-afbe-437d-bf98-b6b916558642%2F4DNFIGKX4A32.hic": "883933242,26979",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F941b2f94-2fff-4d3c-aceb-fee4a5ae41a9%2F4DNFI1ORHOTA.hic": "1140970677,26979",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F8720ede9-093f-4c00-912f-373283260a16%2F4DNFIWSVLZED.hic": "458091151,26979",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fe9e17155-2b62-4a68-afb2-31dfa674e169%2F4DNFI6H926RO.hic": "3552863515,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F22f22778-987a-4dc0-97bc-f26eae0156d9%2F4DNFIKTYSPOF.hic": "2939475400,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F40f40192-0acb-44cb-8778-8d21afe593fb%2F4DNFIZ4S52BV.hic": "3067438113,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fd8767374-7018-4e11-9226-bf5612d39ebb%2F4DNFI3APMXHF.hic": "3258874744,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F8c5140ec-a65a-4186-8c59-2927233eecaa%2F4DNFIQLIKDSD.hic": "2746398416,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F8cdedda6-d7d8-4231-a85b-d56fcd962366%2F4DNFIKYXMF1E.hic": "14442148774,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Faa4d4497-7c6c-4dad-8040-be13ba07ea32%2F4DNFI9815EEJ.hic": "17960817603,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fe9b7b95f-190b-49fe-8047-ef4a8fc59746%2F4DNFIQ2687RT.hic": "15954277087,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F59863d3a-8d7c-49c7-9a41-7f2a58ad24d1%2F4DNFIYDBBSE3.hic": "16499534862,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3e80ca41-b3e0-4f4f-a05e-8f95fe96d938%2F4DNFIMOCN1YS.hic": "863725312,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F96f97a01-0020-4f09-93e8-fc8a9fd07218%2F4DNFI6558CM8.hic": "1597967345,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fce9e9334-ee0b-4549-a845-110470ba5986%2F4DNFIQZRKS7Y.hic": "907626411,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fdaac8209-1015-48da-9e31-2e252dfd8fe0%2F4DNFIX4DLXSE.hic": "1369766288,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F268b7d52-9655-474c-9467-8ba31bb2195c%2F4DNFII3JV8I1.hic": "856656441,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F9681f9b5-335a-4f56-afa1-15b58bbb41e8%2F4DNFI5IAH9H1.hic": "1495185175,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3e082eab-95aa-4677-88ed-9d7adcc1effb%2F4DNFIK5HY1GP.hic": "2135337789,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F31af1ead-4520-4669-957e-424b4b197c33%2F4DNFIFA89L5B.hic": "1975593640,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fcc2b78fd-52d4-4e0c-ac93-b84ee82daf7e%2F4DNFICXCFGEI.hic": "2628202920,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff54efece-280f-46ca-95e3-1f8a3cd18820%2F4DNFIK4CECUH.hic": "1901129375,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F1181c0c4-afb7-4b6a-9fdc-d868fb2253fc%2F4DNFI1EYIGOC.hic": "1947135384,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F1d3e2905-b9b9-4b91-bcb4-8d34746e78f6%2F4DNFI7TLEWUI.hic": "1185758726,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Faba3d42f-39cd-4a85-9d3f-835aefb5c803%2F4DNFIVCJKHMN.hic": "687240974,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F27f54fcb-54fe-41a4-b25a-2f8944c89044%2F4DNFIN8F14CS.hic": "1304082029,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fb75fedb5-88f4-4bc0-8e38-298df7847a7f%2F4DNFI5LCW273.hic": "12497258313,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F2f22b2a0-3712-496e-92d2-63775ec0e421%2F4DNFIVSCH2CH.hic": "12594733778,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F07e47261-5863-44f5-9198-76e263081bea%2F4DNFIZ268JGC.hic": "1499059135,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F682b1ae1-3ac3-40e4-a330-c49f63946952%2F4DNFIJ1P4HBI.hic": "1190211725,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F197e7c89-ea73-41de-8cb2-8c0b1e3bb945%2F4DNFITEGKBIW.hic": "2061501971,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F191f6928-c9a8-44b2-8516-ee4d6005f24b%2F4DNFIE1P6RVH.hic": "1711447310,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F94435086-3701-4b28-a6d0-4a59b24d7615%2F4DNFICJ6I4DO.hic": "1557899940,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Faa9ac173-d233-41c7-8985-ca91949664e4%2F4DNFIKUG8MEW.hic": "1339072088,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fba6d8ab6-ee18-4ba2-8da9-d631888dd50a%2F4DNFICFZGFAV.hic": "1261083478,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F87a3ab04-7094-4673-81e4-ac8c9add8eb5%2F4DNFIQXCZVVA.hic": "1084913802,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fbc1ff488-82af-4579-9424-959184edb139%2F4DNFIB6PJFJ3.hic": "1114571343,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F1f7026ee-987a-41e7-8a61-45510ee3f86b%2F4DNFIX97731O.hic": "1246860402,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F6186787c-2e72-4925-9729-02121e2e4597%2F4DNFIYQYZOTO.hic": "1212210849,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3b98a946-0109-4efa-a13e-cc7a36772125%2F4DNFIPXU7V25.hic": "1696288217,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fea17d041-c6ed-43a1-8047-535f3d19ad44%2F4DNFIL39PR76.hic": "1719284710,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F72774afe-f166-42ac-9849-4070b0dadf6d%2F4DNFIYLJ3R3B.hic": "1106419640,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F4a45e733-55f8-4a8e-be24-2ef34c976e25%2F4DNFIL51WBN6.hic": "1327651164,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F086c62b0-a26f-41ae-a5ff-88b8be2bd2aa%2F4DNFI6SFPUDA.hic": "1336964405,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F4d9b79c7-af62-4070-84a9-b953903f2732%2F4DNFI2KM22QR.hic": "953067310,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3c9889d1-f49e-4639-8e15-2d99d6cd22e7%2F4DNFIVF8Q45U.hic": "984193565,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fa080dd7b-b7bb-4eae-a275-572a8b45b2d0%2F4DNFI2RN3WFP.hic": "1046525527,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fb81860ca-c227-4b68-a298-1907fd32300a%2F4DNFI4TJTL7A.hic": "1160373401,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F74ab47dc-7b52-421c-845e-6feb33457adf%2F4DNFIYJ4TWB8.hic": "1268416204,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fc6582128-a930-4bcc-941e-5c0733bdd4d5%2F4DNFIM6RGKPV.hic": "863347067,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F11a75514-4450-4295-9eda-18e3678a575a%2F4DNFIL2VWWL5.hic": "860731581,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F5eb52a8c-bf14-4225-bf34-14a0aa47c369%2F4DNFIX6ZXCA8.hic": "893462267,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F66aaa4c0-1f62-4040-8ecc-4e95da23cd18%2F4DNFIEVR81FS.hic": "879736207,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F821c6e7f-96f7-47de-b7d9-4970562b3c8e%2F4DNFIAUI6BBI.hic": "996313881,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F9252c362-31ab-413d-a47a-cc97bdbf243a%2F4DNFIAFEE9G2.hic": "1103265750,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fc1a0bf52-2e65-4deb-871f-ede736babd41%2F4DNFIPZBEXCP.hic": "1081257762,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F729f5ce3-d7e0-466d-b781-d18049e63bfa%2F4DNFIWPKRZGU.hic": "973779283,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F80b769ee-a717-4825-9db0-dca86f913173%2F4DNFIMD9QNDX.hic": "1158688208,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F266290c0-2767-4158-85ab-7274368905b9%2F4DNFIATA1HD5.hic": "1073361089,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fc46f18a8-eb24-4d35-b112-36db72bde2f5%2F4DNFIH9U4I7I.hic": "1111124316,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fec91a79e-7de7-49f7-9b3d-d28313aeddc8%2F4DNFIZ95S6TR.hic": "1242494051,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F9e9390b9-da20-43bb-a8f4-9830f354ddf9%2F4DNFI5ERM9J7.hic": "1271051397,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fee8ea82e-b9e4-41ad-bced-5f4fbdaa4fec%2F4DNFI15FPSQL.hic": "1216714120,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fbed7eb86-8990-42f2-b3ee-5992d6361f7c%2F4DNFIJ3DZPGM.hic": "1035089437,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fdaa8a324-3bf8-424c-8a86-b8ad325cc20d%2F4DNFINW9P6XF.hic": "1215712740,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F70631758-6bf8-47a0-92b6-132574886faa%2F4DNFITS16HW8.hic": "1105283847,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fe473304b-7bc8-49b8-bba1-ecb9f06914ee%2F4DNFIS3DIH4V.hic": "1036056660,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F10f9224a-111c-402a-ac25-e221ad0af9ef%2F4DNFIY3KDHP6.hic": "1071761024,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F17519b96-2bdd-4294-98f0-1535f1aef28e%2F4DNFIO8HVKOL.hic": "953793750,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ffb18f896-4730-4909-86a3-867fc8e76cd6%2F4DNFIMF6CFOM.hic": "708717889,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fcfee4d87-0873-40b0-b79f-35dcc8669c1c%2F4DNFIZZ77KD2.hic": "732668723,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fb8b5d909-8a1b-41bb-bcdd-d108a0bde762%2F4DNFIOLO226X.hic": "951271932,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F9ce81f6e-b9cf-4ec9-8bd3-517539e4b662%2F4DNFIJMS2ODT.hic": "932509067,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F506aa952-5d41-49ff-b658-916bcf88a395%2F4DNFI49F3LJ4.hic": "1208494817,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fe4dc392d-ca36-4bdf-a27d-173cae34f520%2F4DNFI65MQOIJ.hic": "854630155,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F149ef8d7-e0b4-4e4f-b7e5-7226e2603746%2F4DNFIM4KEPRD.hic": "882790622,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fa436f0a8-4f9a-4524-90ac-795c5b8491cf%2F4DNFIIXBIZFC.hic": "848244425,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fbc8a05dc-7449-4b01-8330-a55db37ca8e6%2F4DNFIWDOOBVE.hic": "1116239020,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Faef8b881-28fe-4694-9a95-1d31940e6726%2F4DNFIDT9EB5M.hic": "888289053,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff66f3162-a53d-46f2-8800-3752935d5400%2F4DNFIX2VUNV8.hic": "1226661451,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F36c782e6-2c48-4298-aa43-bd6624da003d%2F4DNFIEQHTV1R.hic": "804665819,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff15d571e-605d-4bcc-a359-0350b57a6815%2F4DNFIFW7GA64.hic": "901568712,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fc2aa2e8b-ac7f-4acb-b4b5-ac5f845840fe%2F4DNFIXGXD67I.hic": "970995551,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F71b6fd7d-b249-4d65-a597-b80b95845399%2F4DNFIA7GB1NB.hic": "956785837,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F6cbb1418-34db-427f-9273-5174eac81529%2F4DNFIVOJGWNP.hic": "890415052,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fc69c2ee7-b6c3-4a62-8d4f-913491c9dab8%2F4DNFIW22BNB5.hic": "1075499540,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fa268f28f-c6d9-4324-8989-75d2d317af1b%2F4DNFIIFBC8WN.hic": "1045459440,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F6c7361d4-60a6-440b-b5be-072cada98663%2F4DNFI9ZBEBJH.hic": "759395398,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fcceaad1a-e01e-4430-8bd6-24ffa6891802%2F4DNFID4SLU53.hic": "921288444,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F9eee5854-b33e-4d5a-9ba8-ceffd47e7488%2F4DNFIODI1NUJ.hic": "733159777,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F5e1089b0-42e8-441c-99bb-4be99bcec28b%2F4DNFIJL26LFN.hic": "723567970,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F298a429e-ea29-49d3-bfed-fa851e1e7c4c%2F4DNFI5KTF553.hic": "2213661745,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F965d29f0-e4a3-477c-8ae4-faf4a654e69a%2F4DNFIYSPGFGV.hic": "2274989349,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fd88acbc1-37b9-4266-9f97-d8fed576d0f3%2F4DNFILOR6D2D.hic": "2595459358,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F7b6ddbce-0ed1-4272-b8ae-32793e4ea4e4%2F4DNFI1WMMPJQ.hic": "2262433890,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fedc0c8c4-97d8-4372-9a4f-5e0cdf09d7d0%2F4DNFI47P5978.hic": "2146314403,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F0edcc389-43a4-44d7-b946-fe3496f93d56%2F4DNFI7XGW6IH.hic": "2302805723,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F0a802926-e235-4c80-b521-83a1774dfb46%2F4DNFIK1EJG8I.hic": "2374943035,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3c36aedb-6a58-4aa2-9b11-bb440ec174f1%2F4DNFIDQ3THN9.hic": "2341188249,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F846123cd-8828-4e51-8b8a-27deaf293858%2F4DNFI3WPNFWT.hic": "775145629,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fbd06e3ad-0e37-48a3-8367-c7d5912cab03%2F4DNFI9DCUOQ1.hic": "870487824,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fc2eaf9bf-9584-4cec-8685-bd74038a6c01%2F4DNFIU8AF5ZY.hic": "1276224366,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F219497b5-3f35-473b-9d4e-e1cf21c69561%2F4DNFIHW8NTQX.hic": "28864787856,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F4be13558-114a-4f2d-abca-7598e2ff4757%2F4DNFI4OUMWZ8.hic": "17061013588,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F49aed637-87a1-4d4d-ae68-e5da82aec05b%2F4DNFIEYQ5ZIR.hic": "13555272651,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fa2f48779-4eff-44a6-8a9d-96435aca1ddc%2F4DNFIDZT7PR8.hic": "13425047093,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fb6ec6b71-27b8-4891-b10a-ab06a5270468%2F4DNFIEJEFCAM.hic": "15394960208,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F68b9a7b5-8413-44fc-8c2d-59a67b1c4166%2F4DNFIJ8JKKWJ.hic": "17012681779,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F294d45a4-8b5e-456f-a9cb-1cbbbad3a7d2%2F4DNFINEQY95T.hic": "13966775978,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fbd7f3a48-b822-41e2-bd9c-f52b3aff7b6a%2F4DNFII6AN691.hic": "12367300362,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F7da44594-0dc8-4e74-8e10-a957aeaea51a%2F4DNFIDBIEXI7.hic": "2015474981,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F085a0453-3784-4eba-80b2-3556e53e2f52%2F4DNFIDKJFK2S.hic": "1983111659,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fc370c78c-0a99-4c3f-a29b-2dfafc6a3e86%2F4DNFIPAF4PYT.hic": "3041434294,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F907ce45f-baa5-416d-92de-e86db4bacff1%2F4DNFIWIYYNQ5.hic": "7082321112,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff13afc6b-cf69-4c9d-881f-eed4dd3c28ee%2F4DNFIHMCILEN.hic": "1739157603,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F7575d891-0e3d-4a77-896a-3e1b3a8d330b%2F4DNFIJBJ6QVH.hic": "5213499748,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff0daa830-60d5-4c7c-a2e1-a4ee70b3fee7%2F4DNFIH2A7IKF.hic": "4099503507,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff4970a25-91d8-49a7-9a4a-fe0f12cb2b0f%2F4DNFIOL8KGQ2.hic": "2256042055,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F879f1850-d175-4a86-8a97-12658482e3d1%2F4DNFIWILLD6C.hic": "5476286356,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F787f538e-d619-4c8f-985b-78753fb7e57c%2F4DNFIZBXXJAF.hic": "5946467856,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fb8ee785f-1016-4989-9e64-78352621d32f%2F4DNFI8PZOJHN.hic": "8307204961,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3d8997fa-528c-4531-b25b-a412de6bd472%2F4DNFIGXAMSJY.hic": "6577588285,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F87aede55-7610-4283-af0e-c6840429a31e%2F4DNFIWNNO89O.hic": "2871061417,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F93b8e020-b337-4685-81dc-40cc8a12b5e9%2F4DNFIXU2KPNQ.hic": "3401717686,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ffb5cf1bd-59d6-46fe-857d-b86d9756029d%2F4DNFIHBGD6N6.hic": "840028421,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F2d1bc088-d838-49b8-85fc-fea78a7353c4%2F4DNFI7J8NI4A.hic": "675705256,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F21a27b63-fd67-401b-9aa2-2798e6588d21%2F4DNFISFM9VFK.hic": "800145922,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F656c0a9d-7058-4c4b-a3ce-701d929561c1%2F4DNFIJU5XBK7.hic": "966593603,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F2efacfbd-9c81-4e62-8df7-847d1ab774d5%2F4DNFIPKY8ED1.hic": "3401629450,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fa50b6a26-56b5-43f2-b525-fa219fd716f1%2F4DNFIQRTP7NM.hic": "5358852759,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff5dc7a59-b998-4490-8a76-afbf3b258a49%2F4DNFIBCIA62Q.hic": "5779049277,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F62b2b237-cb02-45f7-89d9-616cf9ffb678%2F4DNFIV56OFE3.hic": "5818844506,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F8ac79b4f-cfbf-4b2a-9fc2-898ed6c3e5e1%2F4DNFILIM6FDL.hic": "19637469163,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F1cff8afa-5a13-4ad4-bcad-91fc3ced759c%2F4DNFIP71EWXC.hic": "17934647326,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F000789fe-9a22-4280-a71c-7ad228fd4cac%2F4DNFIM7ZVQOQ.hic": "700134697,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F8d78457f-0c2b-4358-b08c-4d4ef4d9f59d%2F4DNFIS6EYHCX.hic": "628314484,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F8ca3b4a0-2478-49c9-b4a2-a28430417a32%2F4DNFIKKCMS1Q.hic": "584050634,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff5041473-18ec-439d-92b1-8ad98c9b25c0%2F4DNFIBIV8OUN.hic": "522632731,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fcf474ec6-abd4-431e-a21c-252fcf5ee6b3%2F4DNFIAUMRM2S.hic": "313395531,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F2bb1c73e-fb0f-41c4-8620-a938cc8d24a0%2F4DNFI9ZUXG61.hic": "332217525,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F26dfa41e-7e4f-457a-a3ac-72daf8059f3c%2F4DNFIDBFENL7.hic": "359604894,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F8ed2e68b-189e-4988-bb6d-94e63240a231%2F4DNFI7XAZNNQ.hic": "5312422266,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F6f2b1ce0-5205-4680-981e-d91b694dc3e4%2F4DNFISNGDB53.hic": "2008546759,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F73c8147f-bc7d-4d33-8a10-a9e7b413ecc6%2F4DNFI1CIGHYC.hic": "2221847625,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F526ae3e3-32cc-4670-970f-1faa9cfafd4d%2F4DNFIQD2DP2F.hic": "5295591503,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F6e803530-47f8-4c56-bb19-fbdcd578d2f0%2F4DNFINHT8P7C.hic": "3159499486,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ffe705f38-903f-47b7-9b38-f80ebd18a479%2F4DNFIGF8EM7M.hic": "3490552542,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Faa197328-d6da-4299-a0fd-409ac9892668%2F4DNFIUPG2ZBJ.hic": "5382003751,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ffe5d52c4-2be7-4812-bca0-cd4bb3025e6b%2F4DNFIH3OTR14.hic": "3224693110,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F21935f61-daa8-4df2-aa69-fcd27953b405%2F4DNFIUATRW3Z.hic": "3835865655,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F72147527-e3e8-4967-bc45-8251249d76c9%2F4DNFI6V7ZQAE.hic": "4382708138,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F589f0919-f54d-49ff-a482-e2f453ef7f34%2F4DNFICKMT1CY.hic": "4479068191,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F861946ca-bad8-4a87-96e1-b2eb20b9ef10%2F4DNFIF9BDCNI.hic": "3698151402,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff76346ac-97ec-4893-8984-2b75ce1cdc51%2F4DNFIG5O1OQS.hic": "5704288175,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fe8b43826-f049-4c3f-b83a-e75fa7aacc85%2F4DNFIHS2SVBQ.hic": "990025510,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F46316260-f555-4dd4-9e40-8ae06ca52822%2F4DNFIU95B69H.hic": "1275878544,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff62ef491-a3ae-4f66-ab33-c5673c9e0b65%2F4DNFIYRROFCR.hic": "1522530503,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F1cd617e1-fad4-4fdc-8b4e-cb277650123c%2F4DNFIF7JN3IU.hic": "1463443128,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fdb20fe2a-0078-4d4a-8a52-d76043d06f40%2F4DNFIB2RYULQ.hic": "1269529700,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3d386fcf-1d22-47de-a0d6-0c3a641ca174%2F4DNFIMVDK3SI.hic": "1185489599,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F00e470bd-c533-41b1-923a-4b471824da12%2F4DNFIRLOTXSC.hic": "978217499,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fb8774fb3-d0c5-43cc-a6c9-eb0ca39841fb%2F4DNFIZAPX6J8.hic": "1100356082,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fab3b826e-614c-448b-860f-a3ebef68a82f%2F4DNFIQD5VCSD.hic": "1322319877,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F66c78d2b-993a-4937-95ed-9e2d302ceb3e%2F4DNFIIF6VAGN.hic": "868885799,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F5d106fe1-2d80-466e-87aa-46b38ba142c5%2F4DNFI86IEZXH.hic": "751443267,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fc99490b6-faf9-413f-9c38-3f53018089d6%2F4DNFIP881SR2.hic": "754427927,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F39c7e387-e710-4b1d-b757-357dec03f6ad%2F4DNFI42NNCW8.hic": "1166392978,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F4742a211-43b1-4325-af6f-82490aeb766b%2F4DNFIEV4PKP6.hic": "1102227476,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Feaa73842-1c86-4ad9-8a9e-c189c0b4c01d%2F4DNFIF1DUQHE.hic": "1206699207,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fd25f24df-0298-4079-bb6f-1c8e1e32392b%2F4DNFIGW55EAY.hic": "1033358104,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fc57a7ab5-2778-4fe2-9d64-9a4e95135f12%2F4DNFISZRU6I4.hic": "1539570262,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fd89f494a-27ca-46b6-89be-7583f3f6d18c%2F4DNFIBKKDTAP.hic": "350372180,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F06725280-cb24-4f22-a267-18b434bfa79e%2F4DNFIJSRQW24.hic": "314361467,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F8fe5c3c5-006f-425b-ac6d-ea475e29be78%2F4DNFI3REN89E.hic": "299665258,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fc8b6ac5c-13b4-427e-9158-cec0b890b6d8%2F4DNFIF5XLHKM.hic": "235668364,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F01ff33fc-fc23-48aa-a6c6-b720d6fe0768%2F4DNFIB6Q538N.hic": "316494853,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F552978c8-abbc-424a-9187-3e34cf802a97%2F4DNFI3ST3F7Y.hic": "296298411,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F72e66520-5170-4f2e-844a-fbfd0e23b480%2F4DNFIQT4M6UI.hic": "307691265,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fb3eb3953-ffe8-4d5a-970c-a16f3b7b9ec3%2F4DNFIFNCC7M2.hic": "364160436,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F37d413fb-afab-41ff-99aa-1678830f7dde%2F4DNFITM9MDJL.hic": "252412426,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Faf5db92c-aaf1-4086-a0d2-79b28648f172%2F4DNFI3NTD7B3.hic": "227586640,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff6892f7b-1f8a-4d7d-9605-c67932cd5654%2F4DNFIBED48O1.hic": "328344868,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fd64afb07-5ab1-4c4f-8438-2fee37afa86a%2F4DNFISATK9PF.hic": "379669025,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fad11bb63-8692-461c-b698-89c601451321%2F4DNFISRP84FE.hic": "298491537,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F63710ec5-35d2-47dd-bf9a-08d3daa83d67%2F4DNFI2LY7B73.hic": "303150299,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F76077d9e-ca75-4301-ab79-88aa49bda495%2F4DNFIXEB4UZO.hic": "711757090,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F34dd4c0e-3487-4fb4-b95f-c4bb66e0fa31%2F4DNFIQWWATGK.hic": "221304476,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F48a8cf86-3248-4a8e-8e85-472ec219d49c%2F4DNFI44JLUSL.hic": "284772724,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fee399fbf-2a9e-46e2-8364-457713bff508%2F4DNFIM7Q2FQQ.hic": "330729464,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F124d191a-c5c7-49b6-b84b-0d1bad8bef2b%2F4DNFIZK7W8GZ.hic": "293166018,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fe351f7cc-7a2c-4515-ae0b-3bb2f91c986a%2F4DNFIMIMLMD3.hic": "240094740,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ffd1ae249-57b1-41d6-81a6-16b21dcd8f12%2F4DNFI7QUSU5J.hic": "681762505,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F46c92c5c-9ad8-4d49-beba-f35c5cd99208%2F4DNFINXUOI5H.hic": "375594313,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fd7350d40-4e9e-4131-9151-bb539d39b431%2F4DNFIIJR2296.hic": "331322302,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fbfacf37b-1665-45f3-a046-e29e1544e9b0%2F4DNFIJMZUTXR.hic": "361313244,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F234e18fb-9990-43bf-9a48-f72722cfa232%2F4DNFIZQPT9EL.hic": "540421873,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F4b35ab09-2e79-4876-b2bc-d3a8faa26257%2F4DNFIZIXSZ9J.hic": "545066813,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F2ae5118e-807a-46e0-8e92-143d4b282b3d%2F4DNFIDD9IF9T.hic": "237409149,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fb34d7f6f-73f7-4b6b-80c7-c8af0f991549%2F4DNFIVC8OQPG.hic": "355121712,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F9ab169fd-cbdd-4f60-8553-a4c6912ad89e%2F4DNFITUPI4HA.hic": "313085022,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F4e53712b-d0d9-40c5-afe1-ffa0fad71e59%2F4DNFI5IZNXIO.hic": "341863906,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F7a91477f-45eb-4a16-affb-fc131f9326b8%2F4DNFII16KXA7.hic": "248904495,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F153729de-0eca-43cc-9b61-61aa8d775b38%2F4DNFIAAH19VM.hic": "668047244,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F879fd78a-7165-43d9-a0de-ca158b53df30%2F4DNFIVKIY3KV.hic": "247571318,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F67f901a4-040b-4b04-9fff-94a1780423cd%2F4DNFIVVCWL6C.hic": "198355985,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fd5b30c5a-654a-463c-9474-39bdc3ce4500%2F4DNFIJRF2273.hic": "343971277,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F7070280a-8f03-40d0-9fef-e9d9f4d3dae7%2F4DNFI6FCC9ZA.hic": "312013061,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F47049719-9407-4d76-9048-26231045f642%2F4DNFI1CPQ4O3.hic": "352923267,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F862c2b5f-44c2-4bd7-9264-b3ca032d3d8d%2F4DNFISXFEDE9.hic": "271745977,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Feffbe052-39ff-4217-9871-44199ed90818%2F4DNFIF9L6QEQ.hic": "668409334,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fa7791854-1b35-4258-90fb-603c55f24642%2F4DNFIPKASX1Q.hic": "312727920,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F79cccad6-a78e-4de9-9fe7-08f7e8df663d%2F4DNFIWXZ4JQZ.hic": "151808553,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F7e86e0cc-d6f1-4ce0-aba4-c36c6ebb3c4c%2F4DNFIEMQ1O8V.hic": "700533280,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F4a1b58d3-9ae6-43e4-91cf-49f1fcbbab33%2F4DNFIYWONU7A.hic": "729050356,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F67bff264-0b0c-4c5c-9d4d-ff5abef2bbe4%2F4DNFISNIE5FH.hic": "7987657997,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3a6a943f-2cc0-4433-a27d-c89f3fdd3309%2F4DNFIV8MLWJB.hic": "6646216171,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F9e235711-ba90-404d-b4ac-b15990ea8e33%2F4DNFIXSTDPHL.hic": "3146468238,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F0c89f4bc-e9fc-4ecf-b868-26108dbb7bb2%2F4DNFIQJNOH8U.hic": "2919703928,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F1856ff9e-de7a-4fa1-ae0b-3e04b837b5e1%2F4DNFI57XB4A8.hic": "13417594377,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F099bb5b9-6315-4650-8f78-d603f5a046b3%2F4DNFIQ7UKVZV.hic": "9016468637,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Faf3c7a30-0fc4-410d-8d41-247da3eb105d%2F4DNFIWJY8GHD.hic": "7921314766,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fda99f429-20ab-4d91-bbfe-f8bdea8ed7b1%2F4DNFIN8NKRNF.hic": "4247121358,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F0ca0f4e3-30ca-4332-b735-e32dce08e1a4%2F4DNFIZYGEHNZ.hic": "4910404663,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F243f48b0-6b59-4f37-ad39-a12e0986f530%2F4DNFIRPYGAV8.hic": "4220073636,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F49f1a890-d458-42a6-9c78-99c38b6c7a8f%2F4DNFI6MZTDM1.hic": "375035361,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F76bd4768-9343-44ba-a99d-5500e184e39d%2F4DNFIG3BCGPL.hic": "2422923206,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F9d1ed866-eaf6-4906-aa8c-9e39616dde38%2F4DNFIS9YPJ3L.hic": "1593271574,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F1e7a5636-bdb6-45fb-85d8-87a7e908de43%2F4DNFIOWGA9AX.hic": "1632314387,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F5ecc552a-0a6b-49a9-9aa5-9f3fd9b535fd%2F4DNFINKEDSBF.hic": "1458761024,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F6c03f1d2-bb25-4c94-b5f5-d5cdd01c14b6%2F4DNFIVIXVUK7.hic": "1504818271,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F1a4616ff-6d04-4a6a-9773-b80e81da2471%2F4DNFIANHFY16.hic": "1850724180,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fd2231870-a78d-4c8b-9cfd-084f2468acb1%2F4DNFIJ67ZFBI.hic": "1473864703,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F859c2dd3-df8a-42b7-86ea-bf6524f75e80%2F4DNFIG3YQQ4V.hic": "1539822829,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F8bad667e-6003-4828-9a20-07585fa9c811%2F4DNFIOLKEOME.hic": "3036353747,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F92ea551b-5779-4d31-a0f5-621e4bd0eb21%2F4DNFIVLN8WJE.hic": "2710426318,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F61ee79fa-0617-4083-b121-f92ef3a8905c%2F4DNFI2111XQA.hic": "2589549183,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F83ebb98d-e5db-403d-aa23-875293156c0e%2F4DNFI88HXX7D.hic": "2462367030,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F6d63671a-8f1d-4159-82c0-931b43464772%2F4DNFIUBM6QK8.hic": "2486020469,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fcccb9014-ce15-4032-81ac-222b3d621cb4%2F4DNFIVILEERC.hic": "2052537210,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F21a07867-4007-4ca5-b066-25d3168208dc%2F4DNFIF7Q43N4.hic": "2091773428,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff738de0f-358b-44bf-97dd-644c8d9b1b1c%2F4DNFID56VJGW.hic": "878913568,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fda97c1d4-bb92-4a77-92a7-f7960f88b9dc%2F4DNFIYUR29KO.hic": "983915575,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F54ea363a-99e7-4f55-861e-1644acab7939%2F4DNFIUDPGHO8.hic": "905253594,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fdcd66a07-55ee-48d9-a157-419cf4ee3eef%2F4DNFISWC2N8T.hic": "1233576358,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F40363837-7466-4a1d-b20d-a66d1670b700%2F4DNFICMPHDMI.hic": "516590777,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F0b3fa4e6-caeb-40b4-8942-e9c0b03dfe4f%2F4DNFIKMKN736.hic": "634310295,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fb6a34050-d52b-4880-91cf-d3fe4b0ce982%2F4DNFIN5NLS49.hic": "650451131,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F8e63a47b-9fd4-4772-8b0c-10ee589ffb16%2F4DNFI4P8UBRL.hic": "705655033,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F7d92b666-30cc-4838-9d46-c1f11cee3d3c%2F4DNFISNC3LAI.hic": "176146197,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F17f7dc63-a1bc-4970-b389-87d9ee93b753%2F4DNFIVBWPFD7.hic": "749678512,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff8897333-4791-4a83-9444-a653e24aee13%2F4DNFITMQRXB7.hic": "254477361,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F8712bf31-33d0-4562-ab0b-a1e9cf4fd30c%2F4DNFIUYD3HBY.hic": "995625724,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fbfaf6383-9cc6-4b5c-9b20-d3bf2e8c3495%2F4DNFIRRSC7P9.hic": "1149985362,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fb0b70598-5d17-4e0a-8831-6cbe697e69e2%2F4DNFIWKR8FP6.hic": "1151628407,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fccc094a8-23ce-4fd8-9c4a-41f07c045c8f%2F4DNFIQ74RSUY.hic": "931695969,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F8eda16f3-af5e-4f1f-b46d-c74df44b76aa%2F4DNFI9IIO5PW.hic": "1077740364,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff283637b-cf2c-4412-ba00-0e3d7e279fad%2F4DNFI8WGJG19.hic": "812086456,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F7ad8438f-d3d9-42d8-88bf-42845a0dc2d0%2F4DNFIQ5CCESJ.hic": "652559720,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F70e3bdcc-caf4-4822-b9c5-85234873db7a%2F4DNFIWTZHB4H.hic": "760694196,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F56ed767d-cd8a-4d38-8047-ed1e8bc8cd19%2F4DNFIENYQCDP.hic": "1159428005,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fa912fff9-5f90-42b1-a3a6-cbb9eb2cf861%2F4DNFIFATZWPD.hic": "1033325746,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F21338ea5-fdee-4ad8-b61c-608fc6cec4bc%2F4DNFI7YRXKEO.hic": "388142539,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff0f88647-5631-437b-b48a-4603915e3714%2F4DNFIUY3J7EW.hic": "407344788,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F16926af8-ac3b-4f32-97d5-6c0dc6f1596a%2F4DNFIRKUYKP4.hic": "893352368,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F93e39aec-6cde-4c5b-bcdf-283faa3163a2%2F4DNFIAQDBJZP.hic": "489922022,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F0dc0b1ba-5509-4464-9814-dfe103ff09a0%2F4DNFIC1CLPK7.hic": "565748333,37769",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fb50964c7-b025-49d9-b1ed-289bedb59678%2F4DNFI3A3VYWU.hic": "4597459343,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff4390cea-b330-4022-88c1-9ff8c0a786f7%2F4DNFI6JJQKFJ.hic": "3678393678,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F70c0f730-5da1-4863-9b7a-6cdd31066cff%2F4DNFIK61ILGM.hic": "3324300994,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F14b14ac4-ab59-4754-9c43-eacd9d0638e2%2F4DNFIYD8LO9Z.hic": "3227227120,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F7386f953-8da9-47b0-acb2-931cba810544%2F4DNFIOTPSS3L.hic": "253942497,7557",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff6b5d8bd-27dc-4415-a320-53405a16db85%2F4DNFI525PKU5.hic": "305089653,7557",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F1f922dbc-637b-4c43-b816-f604f0e79ad1%2F4DNFI8Y9SRP2.hic": "1438589609,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F6a36d686-716c-4ba8-a9c3-fa8311791d3c%2F4DNFIP25GZGA.hic": "1419100109,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F0e535542-2cf2-46e4-ba10-7a0b946a7fb2%2F4DNFITPO1WTY.hic": "3885296184,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F0c17d228-5052-4c83-880e-af05118e6929%2F4DNFIG4X5Z5F.hic": "2159245890,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fc3977c00-525b-4556-9a27-061707f8cb76%2F4DNFIXLYR32S.hic": "587852728,21916",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3f856506-cbc5-4211-a29d-48a7530c622e%2F4DNFIWI3V5LI.hic": "2318237793,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F38f77979-5eb7-4b6f-9a60-c2451185db98%2F4DNFI2DGL7LH.hic": "2068150664,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fe0992a75-7727-405e-95ac-aa97141b51c1%2F4DNFI1VSO8JP.hic": "1807949651,21916",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fe925f1aa-2e7b-44e7-bfa8-2084a613ab13%2F4DNFIV5O2ZYO.hic": "2242134377,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F803a6d3c-3972-4939-8ff2-d195c5192090%2F4DNFIEWEWQ8D.hic": "2981341325,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F25104375-a588-46e6-a382-663cee6c332f%2F4DNFICSTCJQZ.hic": "14592996888,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F927aa8a5-24a5-43b7-807a-1e99ac321459%2F4DNFICEGAHRC.hic": "15431307603,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F8c380474-12cd-41f4-b86f-2a29faf12eec%2F4DNFIY3XPKPO.hic": "2550999291,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F77da562e-d0ec-45a8-8764-14fa16d995a1%2F4DNFIK9PS9GM.hic": "3401011629,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F11b26222-d480-40a7-8d53-3ad8500396cb%2F4DNFIF4Y74JR.hic": "2127593378,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F93df02c6-1047-43f6-afae-d3598b61eeea%2F4DNFIBBKG9KD.hic": "5724446278,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F6cd4378a-8f51-4e65-99eb-15f5c80abf8d%2F4DNFIT4I5C6Z.hic": "5419655766,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F067881e5-563d-40de-a9e3-caccad887fbc%2F4DNFI2U38HCE.hic": "14749043471,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fcb3da9ad-7afe-4d0c-9c2d-261181e7a95c%2F4DNFIETP228R.hic": "9401474797,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F185b7bcc-7bb7-41b7-9ebe-92d4fe159431%2F4DNFIXKC48TK.hic": "11624295594,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F18459b84-dd43-4af6-a05f-bcb773096322%2F4DNFIFPGOWJ6.hic": "2843730862,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ffde1e48c-4a0d-4bb1-b84a-e0e45f9a0aa3%2F4DNFIIRMBE8Y.hic": "5820677927,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fbabed539-1f0f-4e87-8815-fde596e6024c%2F4DNFIVUGNDD7.hic": "5170782436,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F7d00531a-e616-469b-af52-5b028270e2ce%2F4DNFIFLJLIS5.hic": "18917549985,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F7a9dad53-b8af-4cb6-9ce3-5c38e4a855c7%2F4DNFIOX3BGNE.hic": "5482775936,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F82fd7b7e-5f47-48d5-bf0d-daaa48a4674d%2F4DNFISPUVQHA.hic": "2177864938,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F4c6680f3-9cf6-452f-9dc5-ecebf376775d%2F4DNFI8649RQJ.hic": "3288850368,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F2395f001-19d9-4000-9f00-dae3dea708d6%2F4DNFIE8T8JZ8.hic": "1383191843,21916",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F24464fa8-bf96-483c-8e9a-c2f626ad6e19%2F4DNFIBM714BP.hic": "1765204569,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F918fc048-fa27-41d2-a63a-d50c511fbb9a%2F4DNFI6CWNUFL.hic": "2431120602,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F7ff5a0d1-8e54-4cf0-9fe7-339f147c78da%2F4DNFIZ4F74QR.hic": "3211825957,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F355f0412-cb17-452a-b15c-aba16c5a742c%2F4DNFIOUVOX3R.hic": "5547401083,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3cac91c5-160a-4b91-a91e-edc309318dd5%2F4DNFIVTDXW6X.hic": "4797344498,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fe9ac9ec5-6221-4f39-951a-1c5054fc9f1c%2F4DNFICA8Y9TN.hic": "4277557869,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F46f62222-b50a-428b-938b-5444951600eb%2F4DNFIDWM3HN5.hic": "4374808431,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F50e4f2a1-9cb6-4192-8199-64d354bfdf96%2F4DNFIBK5TB3N.hic": "3757387253,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fe4cdd5a8-1a45-4c6a-906b-a9603260d4c8%2F4DNFI3JYF9VS.hic": "5260534916,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fc8164e99-5778-427d-be26-f0e1b62400f0%2F4DNFIP1GNKUO.hic": "7589365689,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F0d72e78a-fc87-4716-8b8e-6dc5650ff2ef%2F4DNFIQYQWPF5.hic": "22022259901,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fac58fc15-48c2-4eec-a689-23b677b4b6e7%2F4DNFIW6H9U3S.hic": "6810897169,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F31811e40-e3ca-4456-8ba6-2ee66d6c7104%2F4DNFI7J8BQ4P.hic": "7188744676,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F62d81a52-d2cb-4bbd-8510-0fe120e786e0%2F4DNFIA3AKFSU.hic": "1763730692,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fae0b7513-d1a0-4f27-a9d3-348837f42cb6%2F4DNFID68JQY9.hic": "2422880133,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fae6a1a7e-1d2d-4fd2-a89f-1799abac8a9c%2F4DNFIL1FQDXE.hic": "1543814834,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F80d343c9-bd60-468a-803d-c3e351a958bc%2F4DNFITSIPCSK.hic": "2344682115,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff2d61195-a3a6-4d50-b928-546ca67546cb%2F4DNFIJ88LYHR.hic": "3362742962,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F31052aba-773a-4500-961b-e0a518245b0c%2F4DNFIJIPWD63.hic": "2529548812,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fc93d0710-3fbf-40cc-b0eb-878e2aa659e4%2F4DNFIZUP2BBI.hic": "491996147,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F8660baeb-5be1-4ba5-93e4-66499e9a4587%2F4DNFIEBC4AF3.hic": "2993048734,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff6d08fbc-b836-4776-82ad-8b1b277ae837%2F4DNFI5VO3E1W.hic": "1949322679,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3af004a2-a6fa-46dc-8be8-898b847a77be%2F4DNFIFDGVWLU.hic": "2456400803,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fbd613b4b-2422-492c-8d8f-8afba9c8a182%2F4DNFITVGFNYZ.hic": "1411204423,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fc340694f-14f3-4638-b09b-98220c20039a%2F4DNFIOW9TTC1.hic": "2611370588,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3effa71b-6aa3-4265-87ba-f8d709d778d7%2F4DNFIOAA3ZEQ.hic": "3123195980,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fde75cf8b-9ce1-44fc-8b7b-ca0adea6bae9%2F4DNFIA5ID1S6.hic": "3496246968,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F714f775d-a5a4-4314-83f8-016b9d73200b%2F4DNFIZYU7V81.hic": "4245592042,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F5ca518af-6e1e-4777-b67b-0fe7f039ce58%2F4DNFIRV6PVUX.hic": "3981520466,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fc29b57d5-c263-4372-b396-ab4f55682f5d%2F4DNFIMEANFBY.hic": "3999623376,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F4196d3ef-161a-4c18-a57e-005165c6b32a%2F4DNFIGLGQXLC.hic": "4105683321,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fe10828b5-9519-4ceb-81e7-7228bfd890de%2F4DNFILS2HLXC.hic": "3804942111,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Feaac88c7-9bdc-4b1e-aa9f-e675520dfffa%2F4DNFIQS8853L.hic": "5287726781,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Faab371b1-bc8d-4e83-998e-5bb977c443ca%2F4DNFIVBYCYGS.hic": "4657988157,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fdb116add-af8c-4a3b-b106-9f704ba0a175%2F4DNFIE4WWHMF.hic": "5116727268,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F033366e2-05a2-4381-9d48-90122892a9e0%2F4DNFIKDUUC3C.hic": "45638237,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F7284a446-442d-4a46-aae5-84987e75a7af%2F4DNFIJ5HIRA2.hic": "62201052,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F876649a3-6dd3-463a-8b17-5752a98a89c5%2F4DNFIXDFC7EM.hic": "69878143,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3cb4ce4d-d474-459f-8b8a-2159d8df8068%2F4DNFIO35EOVR.hic": "70153114,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F5c5612fa-18c2-47fb-8074-693b1054e254%2F4DNFI57QGL6U.hic": "54605055,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F70c1472d-cf3a-41d7-8682-cd03b7cc978d%2F4DNFI2AGEBE5.hic": "67930121,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F2bf4cce8-6e03-422b-a190-e51d4a07d501%2F4DNFI1E6NJQJ.hic": "4893181511,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F0eb3aa56-8a23-435d-a07b-8bb545e8d708%2F4DNFI2DNFM1A.hic": "60466977,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F1201682a-a223-482d-913d-3c3972b8eb65%2F4DNFIIRIHBR2.hic": "48961678,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F6aee7913-6356-43cf-affa-f2b60dd478f6%2F4DNFIKXNMT46.hic": "39095246,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F357139a3-c9a5-4343-b0a4-b41c45fd03fd%2F4DNFIRV7IRJP.hic": "70916718,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F76be3061-6259-42be-a917-19dcba41517b%2F4DNFIFX1XYKZ.hic": "229197441,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ff60c348a-a62c-43c3-ac85-f6c28b5f823f%2F4DNFI64CJUZ5.hic": "112428610,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fbd0b0a14-9703-49af-bb36-2359f96d4d0e%2F4DNFIBPL2HL1.hic": "73972428,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F79ac278d-83f7-4c1f-91b2-05c8018c1426%2F4DNFIBRSIPE4.hic": "415430378,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F0c3b8141-8e67-4211-80dd-3bae88f9e537%2F4DNFIDLDVY14.hic": "112747303,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F2ae703d2-d85c-4ac4-9ce1-14d653927675%2F4DNFI4R1II4P.hic": "210565448,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F76e641af-90be-49f8-b126-12c20f856159%2F4DNFINMLRY95.hic": "1590865292,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3b416382-ae5d-469f-9e9a-85a3e2ce9793%2F4DNFI8KBXYNL.hic": "4626496956,22663",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fa27589a6-86b2-43a2-8218-1f2232d551e6%2F4DNFI53R5IKA.hic": "1536016455,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fdd3999f5-b36e-4154-a6b9-446656959a3a%2F4DNFIAWVDQ8C.hic": "2210273411,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fc377953b-0e61-426b-a905-abab2d5a3002%2F4DNFI9VXTBE2.hic": "3384365692,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F2dbe0377-1db2-4aae-af2b-d87dff728035%2F4DNFIT96Z365.hic": "7692311974,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fdcfcb009-f006-4ab8-a4c7-af72be58c12c%2F4DNFITUOMFUQ.hic": "6592120312,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F67f9073d-b2c5-474e-b3d5-7d72cde555cd%2F4DNFIHLQKDN8.hic": "2647703964,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F76968cc8-fb9f-485b-85b2-c9289067194b%2F4DNFIL9M97T2.hic": "6186798917,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F87d9330b-8d2c-4e9d-85bb-02e572de7a68%2F4DNFIDWGZLHX.hic": "4379708168,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Ffc01b381-3665-4225-88ee-2e5e9ecb7324%2F4DNFIMEG1QIC.hic": "3587146533,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3144a03c-15c3-4bf9-95f8-ffad2640a1bf%2F4DNFIWSB66VX.hic": "1529739069,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fd7d3aac0-ba66-494b-ba0c-147631084b98%2F4DNFIH7TH4MF.hic": "6896300679,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F4b13bd19-fbd1-4c68-8855-4f0158d1ce47%2F4DNFIHKWIZ9T.hic": "1954878455,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F3d04e286-30d4-4add-a806-7a929d36419e%2F4DNFIZBBX795.hic": "1350142414,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Faa26f261-a88a-4cac-9118-ff8e90ab6f61%2F4DNFI9ZWZ5BS.hic": "3410552807,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F24e516ce-0b49-4476-b818-cc96471d8765%2F4DNFIPAI8XB5.hic": "4168518737,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fac3b06b5-46d4-4c1e-97e9-9a4f353473a7%2F4DNFI1OUWFSC.hic": "10766406904,21916",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F76c6aa27-895f-4e82-8ed8-7a85aca84490%2F4DNFIX4PJKXZ.hic": "1463157350,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fe6fbe03a-997b-4d72-a3cc-f1ed7bd88a92%2F4DNFIZGCA8AH.hic": "1597664180,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F61d99a87-4ec6-4116-be99-b483999641c4%2F4DNFI5CGQ46V.hic": "2870576870,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F271ea8cd-53b9-44c9-9fff-1bb5c444b54c%2F4DNFIOCW5AEM.hic": "2463600138,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2Fa98ca64a-861a-4a8c-92e9-586af457b1fb%2F4DNFI1UEG1HD.hic": "22480709268,25900",
    "4dn-open-data-public.s3.amazonaws.com%2Ffourfront-webprod%2Fwfoutput%2F5809b32e-0aea-4cf5-a174-cf162d591a35%2F4DNFI9YAVTI1.hic": "3141182061,25900"
};

const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
const Short_MIN_VALUE = -32768;
const DOUBLE = 8;
const FLOAT = 4;
const INT = 4;
const GoogleRateLimiter = new RateLimiter(100);


class HicFile {

    constructor(args) {

        if (args.alert) {
            this.alert = args.alert;
        }

        this.config = args;

        this.loadFragData = args.loadFragData;
        this.fragmentSitesCache = {};
        this.normVectorCache = new LRU(10);
        this.normalizationTypes = ['NONE'];
        this.matrixCache = new LRU(10);
        this.blockCache = new BlockCache();

        // args may specify an io.File object, a local path (Node only), or a url
        if (args.file) {
            this.file = args.file;
        } else if (args.blob) {
            this.file = new BrowserLocalFile(args.blob);
        } else if (args.url || (args.path && !isNode)) {
            this.url = args.url || this.path;
            this.remote = true;

            // Google drive must be rate limited.  Perhaps all remote files should be rate limited?
            const remoteFile = new IGVRemoteFile(args);
            if (isGoogleDrive(this.url)) {
                this.file = new ThrottledFile(remoteFile, GoogleRateLimiter);
            } else {
                this.file = remoteFile;
            }
        } else if (args.path) {
            // path argument, assumed local file
            throw Error(`path property is deprecated, use NodeLocalFile`)
        } else {
            throw Error("Arguments must include file, blob, url, or path")
        }
    }

    async init() {

        if (this.initialized) {
            return;
        } else {
            await this.readHeaderAndFooter();
            // Footer is read with header
            //await this.readFooter()
            this.initialized = true;
        }
    }

    async getVersion() {
        if (this.version === undefined) {
            const data = await this.file.read(0, 128);
            if (!data) {
                return undefined;
            }
            const binaryParser = new BinaryParser(new DataView(data));
            this.magic = binaryParser.getString();
            this.version = binaryParser.getInt();
            return this.version
        } else {
            return this.version
        }
    }

    async getMetaData() {
        await this.init();
        return this.meta
    }

    async readHeaderAndFooter() {

        // Read initial fields magic, version, and footer position
        let data = await this.file.read(0, 16);
        if (!data || data.byteLength === 0) {
            throw Error("File content is empty")
        }
        let binaryParser = new BinaryParser(new DataView(data));
        this.magic = binaryParser.getString();
        this.version = binaryParser.getInt();
        if (this.version < 5) {
            throw Error("Unsupported hic version: " + this.version)
        }
        this.footerPosition = binaryParser.getLong();

        // Read footer and determine file position for body section (i.e. end of header)

        await this.readFooter();

        const bodyPostion = Object.values(this.masterIndex).reduce((min, currentValue) => {
            return Math.min(min, currentValue.start)
        }, Number.MAX_VALUE);

        const remainingSize = bodyPostion - 16;
        data = await this.file.read(16, remainingSize);
        binaryParser = new BinaryParser(new DataView(data));

        this.genomeId = binaryParser.getString();

        if (this.version >= 9) {
            this.normVectorIndexPosition = binaryParser.getLong();
            this.normVectorIndexSize = binaryParser.getLong();
        }

        this.attributes = {};
        let nAttributes = binaryParser.getInt();
        while (nAttributes-- > 0) {
            this.attributes[binaryParser.getString()] = binaryParser.getString();
        }

        this.chromosomes = [];
        this.chromosomeIndexMap = {};
        let nChrs = binaryParser.getInt();
        let i = 0;
        while (nChrs-- > 0) {
            const chr = {
                index: i,
                name: binaryParser.getString(),
                size: this.version < 9 ? binaryParser.getInt() : binaryParser.getLong()
            };
            if (chr.name.toLowerCase() === "all") {
                this.wholeGenomeChromosome = chr;
                this.wholeGenomeResolution = Math.round(chr.size * (1000 / 500));    // Hardcoded in juicer
            }
            this.chromosomes.push(chr);
            this.chromosomeIndexMap[chr.name] = chr.index;
            i++;
        }

        this.bpResolutions = [];
        let nBpResolutions = binaryParser.getInt();
        while (nBpResolutions-- > 0) {
            this.bpResolutions.push(binaryParser.getInt());
        }

        if (this.loadFragData) {
            this.fragResolutions = [];
            let nFragResolutions = binaryParser.getInt();
            if (nFragResolutions > 0) {
                while (nFragResolutions-- > 0) {
                    this.fragResolutions.push(binaryParser.getInt());
                }

                // this.sites = [];
                // for(let i=0; i<this.chromosomes.length - 1; i++) {
                //     const chrSites = [];
                //     this.sites.push(chrSites);
                //     let nSites = binaryParser.getInt();
                //     console.log(nSites);
                //     for(let s=0; s<nSites; s++) {
                //         chrSites.push(binaryParser.getInt());
                //     }
                // }
            }
        }

        // Build lookup table for well-known chr aliases
        this.chrAliasTable = {};
        for (let chrName of Object.keys(this.chromosomeIndexMap)) {

            if (chrName.startsWith("chr")) {
                this.chrAliasTable[chrName.substr(3)] = chrName;
            } else if (chrName === "MT") {
                this.chrAliasTable["chrM"] = chrName;
            } else {
                this.chrAliasTable["chr" + chrName] = chrName;
            }
        }


        // Meta data for the API
        this.meta = {
            "version": this.version,
            "genome": this.genomeId,
            "chromosomes": this.chromosomes,
            "resolutions": this.bpResolutions,
        };


    }

    async readFooter() {


        const skip = this.version < 9 ? 8 : 12;
        let data = await this.file.read(this.footerPosition, skip);
        if (!data) {
            return null;
        }

        let binaryParser = new BinaryParser(new DataView(data));
        const nBytes = this.version < 9 ? binaryParser.getInt() : binaryParser.getLong();  // Total size, master index + expected values
        let nEntries = binaryParser.getInt();

        // Estimate the size of the master index. String length of key is unknown, be conservative (100 bytes)

        const miSize = nEntries * (100 + 64 + 32);
        data = await this.file.read(this.footerPosition + skip, Math.min(miSize, nBytes));

        binaryParser = new BinaryParser(new DataView(data));

        this.masterIndex = {};
        while (nEntries-- > 0) {
            const key = binaryParser.getString();
            const pos = binaryParser.getLong();
            const size = binaryParser.getInt();
            this.masterIndex[key] = {start: pos, size: size};
        }

        this.expectedValueVectors = {};

        // Expected values
        // const nExpValues = binaryParser.readInt();
        // while (nExpValues-- > 0) {
        //     type = "NONE";
        //     unit = binaryParser.getString();
        //     binSize = binaryParser.getInt();
        //     nValues = binaryParser.getInt();
        //     values = [];
        //     while (nValues-- > 0) {
        //         values.push(binaryParser.getDouble());
        //     }
        //
        //     nChrScaleFactors = binaryParser.getInt();
        //     normFactors = {};
        //     while (nChrScaleFactors-- > 0) {
        //         normFactors[binaryParser.getInt()] = binaryParser.getDouble();
        //     }
        //
        //     // key = unit + "_" + binSize + "_" + type;
        //     //  NOT USED YET SO DON'T STORE
        //     //  dataset.expectedValueVectors[key] =
        //     //      new ExpectedValueFunction(type, unit, binSize, values, normFactors);
        // }

        // normalized expected values start after expected value.  Add 4 for
        if (this.version > 5) {
            const skip = this.version < 9 ? 4 : 8;
            this.normExpectedValueVectorsPosition = this.footerPosition + skip + nBytes;
        }
        return this;
    };

    async printIndexStats() {

        let totalSize = 0;
        let maxSize = 0;
        await await this.init();
        for (let key of Object.keys(this.masterIndex)) {
            const entry = this.masterIndex[key];
            //  console.log(`${key}\t${entry.start}\t${entry.size}`)
            totalSize += entry.size;
            if (entry.size > maxSize) {
                maxSize = entry.size;
            }
        }
        // console.log(`Total size  = ${totalSize}`);
    }

    async getMatrix(chrIdx1, chrIdx2) {
        const key = Matrix.getKey(chrIdx1, chrIdx2);
        if (this.matrixCache.has(key)) {
            return this.matrixCache.get(key);
        } else {
            const matrix = await this.readMatrix(chrIdx1, chrIdx2);
            this.matrixCache.set(key, matrix);
            return matrix;
        }
    }

    async readMatrix(chrIdx1, chrIdx2) {

        await this.init();

        if (chrIdx1 > chrIdx2) {
            const tmp = chrIdx1;
            chrIdx1 = chrIdx2;
            chrIdx2 = tmp;
        }

        const key = Matrix.getKey(chrIdx1 , chrIdx2);
        const idx = this.masterIndex[key];
        if (!idx) {
            return undefined
        }
        const data = await this.file.read(idx.start, idx.size);
        if (!data) {
            return undefined
        }

        return Matrix.parseMatrix(data, this.chromosomes);

    }

    async getContactRecords(normalization, region1, region2, units, binsize, allRecords = false) {

        await this.init();

        const idx1 = this.chromosomeIndexMap[this.getFileChrName(region1.chr)];
        const idx2 = this.chromosomeIndexMap[this.getFileChrName(region2.chr)];

        const transpose = (idx1 > idx2) || (idx1 === idx2 && region1.start >= region2.end);
        if (transpose) {
            const tmp = region1;
            region1 = region2;
            region2 = tmp;
        }

        const blocks = await this.getBlocks(region1, region2, units, binsize);
        if (!blocks || blocks.length === 0) {
            return []
        }

        const contactRecords = [];
        const x1 = region1.start / binsize;
        const x2 = region1.end / binsize;
        const y1 = region2.start / binsize;
        const y2 = region2.end / binsize;
        const nvX1 = Math.floor(x1);
        const nvX2 = Math.ceil(x2);
        const nvY1 = Math.floor(y1);
        const nvY2 = Math.ceil(y2);
        for (let block of blocks) {
            if (block) { // An undefined block is most likely caused by a base pair range outside the chromosome
                let normVector1;
                let normVector2;
                let isNorm = normalization && normalization !== "NONE";
                const chr1 = this.getFileChrName(region1.chr);
                const chr2 = this.getFileChrName(region2.chr);
                if (isNorm) {

                    const nv1 = await this.getNormalizationVector(normalization, chr1, units, binsize);
                    const nv2 = (chr1 === chr2) ? nv1 : await this.getNormalizationVector(normalization, chr2, units, binsize);

                    if (nv1 && nv2) {
                        normVector1 = await nv1.getValues(nvX1, nvX2);
                        normVector2 = await nv2.getValues(nvY1, nvY2);
                    } else {
                        isNorm = false;
                        // Raise message and switch pulldown
                    }
                }

                for (let rec of block.records) {
                    if (allRecords || (rec.bin1 >= x1 && rec.bin1 < x2 && rec.bin2 >= y1 && rec.bin2 < y2)) {
                        if (isNorm) {
                            const x = rec.bin1;
                            const y = rec.bin2;
                            const nvnv = normVector1[x - nvX1] * normVector2[y - nvY1];
                            if (nvnv !== 0 && !isNaN(nvnv)) {
                                const counts = rec.counts / nvnv;
                                contactRecords.push(new ContactRecord(x, y, counts));
                            }
                        } else {
                            contactRecords.push(rec);
                        }
                    }
                }

                if(this.percentile95 === undefined && block.records.length > 10) {
                    this.percentile95 = computePercentile(block.records, 95);
                }
            }
        }

        return contactRecords;
    }

    async getBlocks(region1, region2, unit, binSize) {

        const blockKey = (blockNumber, zd) => `${zd.getKey()}_${blockNumber}`;

        await this.init();
        const chr1 = this.getFileChrName(region1.chr);
        const chr2 = this.getFileChrName(region2.chr);
        const idx1 = this.chromosomeIndexMap[chr1];
        const idx2 = this.chromosomeIndexMap[chr2];

        if (idx1 === undefined) {
            return []
        }
        if (idx2 === undefined) {
            return []
        }

        const matrix = await this.getMatrix(idx1, idx2);
        if (!matrix) {
            return []
        }

        const zd = matrix.getZoomData(binSize, unit);
        if (!zd) {
            let msg = `No data avalailble for resolution: ${binSize}  for map ${region1.chr}-${region2.chr}`;
            throw new Error(msg)
        }

        const blockNumbers = zd.getBlockNumbers(region1, region2, this.version);

        const blocks = [];
        const blockNumbersToQuery = [];
        for (let num of blockNumbers) {
            const key = blockKey(num, zd);
            if (this.blockCache.has(binSize, key)) {
                blocks.push(this.blockCache.get(binSize, key));
            } else {
                blockNumbersToQuery.push(num);
            }
        }

        const promises = blockNumbersToQuery.map(blockNumber => this.readBlock(blockNumber, zd));
        const newBlocks = await Promise.all(promises);
        for (let block of newBlocks) {
            if (block) {
                this.blockCache.set(binSize, blockKey(block.blockNumber, zd), block);
            }
        }
        return blocks.concat(newBlocks);
    }

    async readBlock(blockNumber, zd) {

        const idx = await zd.blockIndex.getBlockIndexEntry(blockNumber);

        if (!idx) {
            return undefined
        } else {
            let data = await this.file.read(idx.filePosition, idx.size);
            if (!data) {
                return undefined;
            }

            const plain = new inflate_1$3(new Uint8Array(data));
            data = plain.buffer;

            const parser = new BinaryParser(new DataView(data));
            const nRecords = parser.getInt();
            const records = [];

            if (this.version < 7) {
                for (let i = 0; i < nRecords; i++) {
                    const binX = parser.getInt();
                    const binY = parser.getInt();
                    const counts = parser.getFloat();
                    records.push(new ContactRecord(binX, binY, counts));
                }
            } else {

                const binXOffset = parser.getInt();
                const binYOffset = parser.getInt();

                const useFloatContact = parser.getByte() === 1;
                const useIntXPos = this.version < 9 ? false : parser.getByte() == 1;
                const useIntYPos = this.version < 9 ? false : parser.getByte() == 1;
                const type = parser.getByte();

                if (type === 1) {
                    // List-of-rows representation
                    const rowCount = useIntYPos ? parser.getInt() : parser.getShort();
                    for (let i = 0; i < rowCount; i++) {
                        const dy = useIntYPos ? parser.getInt() : parser.getShort();
                        const binY = binYOffset + dy;
                        const colCount = useIntXPos ? parser.getInt() : parser.getShort();
                        for (let j = 0; j < colCount; j++) {
                            const dx = useIntXPos ? parser.getInt() : parser.getShort();
                            const binX = binXOffset + dx;
                            const counts = useFloatContact ? parser.getFloat() : parser.getShort();
                            records.push(new ContactRecord(binX, binY, counts));
                        }
                    }
                } else if (type == 2) {

                    const nPts = parser.getInt();
                    const w = parser.getShort();

                    for (let i = 0; i < nPts; i++) {
                        //int idx = (p.y - binOffset2) * w + (p.x - binOffset1);
                        const row = Math.floor(i / w);
                        const col = i - row * w;
                        const bin1 = binXOffset + col;
                        const bin2 = binYOffset + row;

                        if (useFloatContact) {
                            const counts = parser.getFloat();
                            if (!isNaN(counts)) {
                                records.push(new ContactRecord(bin1, bin2, counts));
                            }
                        } else {
                            const counts = parser.getShort();
                            if (counts != Short_MIN_VALUE) {
                                records.push(new ContactRecord(bin1, bin2, counts));
                            }
                        }
                    }
                } else {
                    throw new Error("Unknown block type: " + type);
                }

            }

            return new Block(blockNumber, zd, records, idx);


        }
    };

    async hasNormalizationVector(type, chr, unit, binSize) {
        await this.init();
        let chrIdx;
        if (Number.isInteger(chr)) {
            chrIdx = chr;
        } else {
            const canonicalName = this.getFileChrName(chr);
            chrIdx = this.chromosomeIndexMap[canonicalName];
        }
        const key = getNormalizationVectorKey(type, chrIdx, unit.toString(), binSize);
        const normVectorIndex = await this.getNormVectorIndex();
        return normVectorIndex && normVectorIndex[key];
    }

    async isNormalizationValueAvailableAtResolution(normalization, chr, unit, resolution) {

        let chromosomeIndex;
        if (Number.isInteger(chr)) {
            chromosomeIndex = chr;
        } else {
            const canonicalName = this.getFileChrName(chr);
            chromosomeIndex = this.chromosomeIndexMap[canonicalName];
        }

        const normVectorIndex = await this.getNormVectorIndex();

        const key = getNormalizationVectorKey(normalization, chromosomeIndex, unit.toString(), resolution);

        const index = normVectorIndex[key];

        return undefined !== index

    }

    async getNormalizationVector(type, chr, unit, binSize) {

        await this.init();

        let chrIdx;
        if (Number.isInteger(chr)) {
            chrIdx = chr;
        } else {
            const canonicalName = this.getFileChrName(chr);
            chrIdx = this.chromosomeIndexMap[canonicalName];
        }

        const key = getNormalizationVectorKey(type, chrIdx, unit.toString(), binSize);

        if (this.normVectorCache.has(key)) {
            return this.normVectorCache.get(key);
        }

        const normVectorIndex = await this.getNormVectorIndex();

        if (!normVectorIndex) {
            return undefined
        }

        const status = await this.isNormalizationValueAvailableAtResolution(type, chr, unit, binSize);

        if (false === status) {

            const str = `Normalization option ${ type } not available at resolution ${ binSize }. Will use NONE.`;

            if (this.alert) {
                this.alert(str);
            } 
            return undefined
        }

        const idx = normVectorIndex[key];

        const data = await this.file.read(idx.filePosition, 8);

        if (!data) {
            return undefined;
        }

        const parser = new BinaryParser(new DataView(data));
        const nValues = this.version < 9 ? parser.getInt() : parser.getLong();
        const dataType = this.version < 9 ? DOUBLE : FLOAT;
        const filePosition = this.version < 9 ? idx.filePosition + 4 : idx.filePosition + 8;
        const nv = new NormalizationVector(this.file, filePosition, nValues, dataType);
        this.normVectorCache.set(key, nv);
        return nv;

    }

    async getNormVectorIndex() {

        if (this.version < 6) {
            return undefined;
        }

        if (!this.normVectorIndex) {

            // If nvi is not supplied, try reading from remote lambda service
            if (!this.config.nvi && this.remote && this.url) {
                const url = new URL(this.url);
                const key = encodeURIComponent(url.hostname + url.pathname);
                if(nvi.hasOwnProperty(key)) {
                    this.config.nvi = nvi[key];
                }
            }

            if (this.config.nvi) {
                const nviArray = decodeURIComponent(this.config.nvi).split(",");
                const range = {start: parseInt(nviArray[0]), size: parseInt(nviArray[1])};
                return this.readNormVectorIndex(range)
            } else {
                try {
                    await this.readNormExpectedValuesAndNormVectorIndex();
                    return this.normVectorIndex
                } catch (e) {
                    if (e.code === "416" || e.code === 416) {
                        // This is expected if file does not contain norm vectors
                        this.normExpectedValueVectorsPosition = undefined;
                    } else {
                        console.error(e);
                    }
                }
            }
        }

        return this.normVectorIndex
    }

    async getNormalizationOptions() {
        // Normalization options are computed as a side effect of loading the index.  A bit
        // ugly but alternatives are worse.
        await this.getNormVectorIndex();
        return this.normalizationTypes;
    }

    /**
     * Return a promise to load the normalization vector index
     *
     * @param dataset
     * @param range  -- file range {position, size}
     * @returns Promise for the normalization vector index
     */
    async readNormVectorIndex(range) {

        await this.init();

        this.normalizationVectorIndexRange = range;

        const data = await this.file.read(range.start, range.size);

        const binaryParser = new BinaryParser(new DataView(data));

        this.normVectorIndex = {};

        let nEntries = binaryParser.getInt();
        while (nEntries-- > 0) {
            this.parseNormVectorEntry(binaryParser);
        }

        return this.normVectorIndex;

    }

    /**
     * This function is used when the position of the norm vector index is unknown.  We must read through the expected
     * values to find the index
     *
     * @param dataset
     * @returns {Promise}
     */
    async readNormExpectedValuesAndNormVectorIndex() {

        await this.init();

        if (this.normExpectedValueVectorsPosition === undefined) {
            return;
        }

        const nviStart = await this.skipExpectedValues(this.normExpectedValueVectorsPosition);
        let byteCount = INT;

        let data = await this.file.read(nviStart, INT);
        if (data.byteLength === 0) {
            // This is possible if there are no norm vectors.  Its a legal v8 file, though uncommon
            return;
        }
        const binaryParser = new BinaryParser(new DataView(data));
        const nEntries = binaryParser.getInt();
        const sizeEstimate = nEntries * 30;
        const range = {start: nviStart + byteCount, size: sizeEstimate};

        data = await this.file.read(range.start, range.size);
        this.normalizedExpectedValueVectors = {};
        this.normVectorIndex = {};

        // Recursively process entries
        await processEntries.call(this, nEntries, data);

        this.config.nvi = nviStart.toString() + "," + byteCount;

        async function processEntries(nEntries, data) {

            const binaryParser = new BinaryParser(new DataView(data));

            while (nEntries-- > 0) {

                if (binaryParser.available() < 100) {

                    nEntries++;   // Reset counter as entry is not processed

                    byteCount += binaryParser.position;
                    const sizeEstimate = Math.max(1000, nEntries * 30);
                    const range = {start: nviStart + byteCount, size: sizeEstimate};
                    const data = await this.file.read(range.start, range.size);
                    return processEntries.call(this, nEntries, data);
                }

                this.parseNormVectorEntry(binaryParser);

            }
            byteCount += binaryParser.position;
        }
    }

    /**
     * This function is used when the position of the norm vector index is unknown.  We must read through the
     * normalized expected values to find the index
     *
     * @param dataset
     * @returns {Promise}
     */
    async skipExpectedValues(start) {

        const version = this.version;
        const file = new BufferedFile({file: this.file, size: 256000});
        const range = {start: start, size: INT};
        const data = await file.read(range.start, range.size);
        const binaryParser = new BinaryParser(new DataView(data));
        const nEntries = binaryParser.getInt();   // Total # of expected value chunks
        if (nEntries === 0) {
            return start + INT;
        } else {
            return parseNext(start + INT, nEntries);
        }     // Skip 4 bytes for int


        async function parseNext(start, nEntries) {

            let range = {start: start, size: 500};
            let chunkSize = 0;
            let p0 = start;

            let data = await file.read(range.start, range.size);
            let binaryParser = new BinaryParser(new DataView(data));
            binaryParser.getString(); // type
            binaryParser.getString(); // unit
            binaryParser.getInt(); // binSize
            const nValues = version < 9 ? binaryParser.getInt() : binaryParser.getLong();

            chunkSize += binaryParser.position + nValues * (version < 9 ? DOUBLE : FLOAT);

            range = {start: start + chunkSize, size: INT};
            data = await file.read(range.start, range.size);
            binaryParser = new BinaryParser(new DataView(data));
            const nChrScaleFactors = binaryParser.getInt();
            chunkSize += (INT + nChrScaleFactors * (INT + (version < 9 ? DOUBLE : FLOAT)));


            nEntries--;
            if (nEntries === 0) {
                return p0 + chunkSize;
            } else {
                return parseNext(p0 + chunkSize, nEntries);
            }
        }
    }

    getZoomIndexForBinSize(binSize, unit) {

        unit = unit || "BP";

        let resolutionArray;
        if (unit === "BP") {
            resolutionArray = this.bpResolutions;
        } else if (unit === "FRAG") {
            resolutionArray = this.fragResolutions;
        } else {
            throw new Error("Invalid unit: " + unit);
        }

        for (let i = 0; i < resolutionArray.length; i++) {
            if (resolutionArray[i] === binSize) return i;
        }

        return -1;
    }

    parseNormVectorEntry(binaryParser) {
        const type = binaryParser.getString();      //15
        const chrIdx = binaryParser.getInt();       //4
        const unit = binaryParser.getString();      //3
        const binSize = binaryParser.getInt();      //4
        const filePosition = binaryParser.getLong();  //8
        const sizeInBytes = this.version < 9 ? binaryParser.getInt() : binaryParser.getLong();     //4:8
        const key = type + "_" + chrIdx + "_" + unit + "_" + binSize;
        // TODO -- why does this not work?  NormalizationVector.getNormalizationVectorKey(type, chrIdx, unit, binSize);

        if (!this.normalizationTypes.includes(type)) {
            this.normalizationTypes.push(type);
        }
        this.normVectorIndex[key] = {filePosition: filePosition, size: sizeInBytes};
    }

    getFileChrName(chrAlias) {
        if (this.chrAliasTable.hasOwnProperty(chrAlias)) {
            return this.chrAliasTable[chrAlias]
        } else {
            return chrAlias
        }
    }


    // NOTE sties are not currently used
    // async getSites(chrName) {
    //     let sites = this.fragmentSitesCache[chrName];
    //     if (!sites) {
    //         if (this.fragmentSitesIndex) {
    //             const entry = self.fragmentSitesIndex[chrName];
    //             if (entry && entry.nSites > 0) {
    //                 sites = await this.readSites(entry.position, entry.nSites)
    //                 this.fragmentSitesCache[chrName] = sites;
    //             }
    //         }
    //     }
    //     return sites;
    // }
    //

}


function getNormalizationVectorKey(type, chrIdx, unit, resolution) {
    return type + "_" + chrIdx + "_" + unit + "_" + resolution;
}

function isGoogleDrive(url) {
    return url.indexOf("drive.google.com") >= 0 || url.indexOf("www.googleapis.com/drive") > 0
}

class Block {
    constructor(blockNumber, zoomData, records, idx) {
        this.blockNumber = blockNumber;
        this.zoomData = zoomData;
        this.records = records;
        this.idx = idx;
    }
}


class BlockCache {

    constructor() {
        this.resolution = undefined;
        this.map = new LRU(6);
    }

    set(resolution, key, value) {
        if (this.resolution !== resolution) {
            this.map.clear();
        }
        this.resolution = resolution;
        this.map.set(key, value);
    }

    get(resolution, key) {
        return this.resolution === resolution ? this.map.get(key) : undefined;
    }

    has(resolution, key) {
        return this.resolution === resolution && this.map.has(key);
    }
}

function computePercentile(records, p) {
    const counts = records.map(r => r.counts);
    counts.sort(function (a, b) {
        return a - b;
    });
    const idx = Math.floor((p / 100) * records.length);
    return counts[idx];

    // return HICMath.percentile(array, p);
}

async function translateSession(juiceboxSession) {

    const jbBrowser = juiceboxSession.browsers[0];
    const igvSession = {};

    const hicFile = new HicFile({url: jbBrowser.url});
    await hicFile.readHeaderAndFooter();
    //`${this.chr1},${this.chr2},${this.zoom},${this.x},${this.y},${this.width},${this.height},${this.pixelSize}`


    igvSession.sampleNameViewportWidth = 20;
    igvSession.genome = "hg38";  // TODO -- determine from hicfile

    const stateTokens = jbBrowser.state.split(",");
    const binSize = hicFile.bpResolutions[Number.parseInt(stateTokens[2])];
    const screenWidth = 1700;  // Approximate guess
    const chrIdx = Number.parseInt(stateTokens[0]);
    const start = Math.floor(Number.parseFloat(stateTokens[3]) * binSize); //- 100
    const end = start + Math.floor(screenWidth * binSize); //+ 100
    igvSession.locus = `${hicFile.chromosomes[chrIdx].name}:${start}-${end}`;


    igvSession.tracks = (jbBrowser.tracks || []).filter(t => !(t.format === "refgene" || t.name === "cellType"));

    igvSession.tracks.push({
        type: "shoebox",
        url: jbBrowser.url,
        name: jbBrowser.name,
        colorScale: jbBrowser.colorScale,
        _hicFile: hicFile
    });

    return igvSession

}

/**
 * Default chromosome aliases, mostly 1<->chr1 etc.  Used if chrom alias file is not supplied.
 *
 */

class ChromAliasDefaults {

    aliasRecordCache = new Map()

    constructor(id, chromosomeNames) {
        this.genomeID = id;
        this.update(id, chromosomeNames);
    }

    async preload() {
        // no-op
    }

    /**
     * Return the canonical chromosome name for the alias.  If none found return the alias
     *
     * @param alias
     * @returns {*}
     */
    getChromosomeName(alias) {
        return this.aliasRecordCache.has(alias) ? this.aliasRecordCache.get(alias).chr : alias
    }

    /**
     * Return an alternate chromosome name (alias).
     *
     * @param chr
     * @param nameSet -- The name set, e.g. "ucsc"
     * @returns {*|undefined}
     */
    getChromosomeAlias(chr, nameSet) {
        const aliasRecord = this.aliasRecordCache.get(chr);
        return aliasRecord ? aliasRecord[nameSet] || chr : chr
    }

    update(id, chromosomeNames) {

        if (chromosomeNames) {
            const aliasRecords = [];
            for (let name of chromosomeNames) {

                if(this.aliasRecordCache.has(name)) {
                    continue;
                }

                const record = {chr: name};
                aliasRecords.push(record);

                if (name.startsWith("gi|")) {
                    // NCBI
                    const alias = ChromAliasDefaults.getNCBIName(name);
                    record["ncbi-gi-versioned"] = alias;

                    // Also strip version number out, if present
                    const dotIndex = alias.lastIndexOf('.');
                    if (dotIndex > 0) {
                        const alias = alias.substring(0, dotIndex);
                        record["ncbi-gi"] = alias;
                    }
                } else {

                    if (name === "chrM") {
                        record["ncbi"] = "MT";
                    } else if (name === "MT") {
                        record["ucsc"] = "chrM";
                    } else if (name.toLowerCase().startsWith("chr") && Number.isInteger(Number(name.substring(3)))) {
                        record["ncbi"] = name.substring(3);
                    } else if (Number.isInteger(Number(name))) {
                        record["ucsc"] = "chr" + name;
                    }

                    // Special cases for human and mouse
                    if (id.startsWith("hg") || id.startsWith("GRCh") || id === "1kg_ref" || id === "b37") {
                        switch (name) {
                            case "23":
                                record["ucsc"] = "chrX";
                                record["assembly"] = "X";
                                break
                            case "24":
                                record["ucsc"] = "chrY";
                                record["assembly"] = "Y";
                                break
                            case "chrX":
                                record["ncbi"] = "23";
                                record["assembly"] = "X";
                                break
                            case "chrY":
                                record["ncbi"] = "24";
                                record["assembly"] = "Y";
                                break
                            case "X":
                                record["ucsc"] = "chrX";
                                record["ncbi"] = "23";
                                break
                            case "Y":
                                record["ucsc"] = "chrY";
                                record["ncbi"] = "24";
                                break

                        }
                    } else if (id.startsWith("mm") || id.startsWith("GRCm") || id.startsWith("rheMac")) {
                        switch (name) {
                            case "21":
                                record["ucsc"] = "chrX";
                                record["assembly"] = "X";
                                break
                            case "22":
                                record["ucsc"] = "chrY";
                                record["assembly"] = "Y";
                                break
                            case "chrX":
                                record["ncbi"] = "21";
                                record["assembly"] = "X";
                                break
                            case "chrY":
                                record["ncbi"] = "22";
                                record["assembly"] = "Y";
                                break
                            case "X":
                                record["ucsc"] = "chrX";
                                record["ncbi"] = "21";
                                break
                            case "Y":
                                record["ucsc"] = "chrY";
                                record["ncbi"] = "22";
                                break

                        }
                    }
                }
            }


            for (let rec of aliasRecords) {
                ChromAliasDefaults.addCaseAliases(rec);
                for (let a of Object.values(rec)) {
                    this.aliasRecordCache.set(a, rec);
                }
            }

        }
    }

    search(alias) {
        return this.aliasRecordCache.get(alias)

    }

    /**
     * Extract the user friendly name from an NCBI accession
     * example: gi|125745044|ref|NC_002229.3|  =>  NC_002229.3
     */
    static getNCBIName(name) {
        const tokens = name.split("\\|");
        return tokens[tokens.length - 1]
    }

    static addCaseAliases(aliasRecord) {

            // Add some aliases for case insensitivy
            const upper = aliasRecord.chr.toUpperCase();
            const lower = aliasRecord.chr.toLowerCase();
            const cap = aliasRecord.chr.charAt(0).toUpperCase() + aliasRecord.chr.slice(1);
            if(aliasRecord.chr !== upper) {
                aliasRecord["_uppercase"] = upper;
            }
            if(aliasRecord.chr !== lower) {
                aliasRecord["_lowercase"] = lower;
            }
            if(aliasRecord.chr !== cap) {
                aliasRecord["_cap"] = cap;
            }

            const chrPrefixAlias = aliasRecord.chr.startsWith("chr") ? aliasRecord.chr.substring(3) : "chr" + aliasRecord.chr;
            aliasRecord["_chrprefix_"] = chrPrefixAlias;

    }

}

/**
 * Chromosome alias source backed by a UCSC bigbed file
 *
 *
 * @param aliasURL
 * @param config
 * @returns {Promise<*[]>}
 */

class ChromAliasBB {

    aliasRecordCache = new Map()

    constructor(url, config, genome) {
        config = config || {};
        config.url = url;
        this.reader = new BWReader(config, genome);
    }

    async preload(chrNames) {
       await this.reader.preload();
        for(let nm of chrNames) {
            await this.search(nm);
        }
    }

    /**
     * Return the cached canonical chromosome name for the alias.  If none found return the alias.
     *
     * Note this will only work if a "search" for ths chromosome has been performed previously.
     *
     * @param alias
     * @returns {*}
     */
    getChromosomeName(alias) {
        return this.aliasRecordCache.has(alias) ? this.aliasRecordCache.get(alias).chr : alias
    }

    /**
     * Return an alternate chromosome name (alias).  If not exists, return chr
     *
     * Note this will only work if a "search" for ths chromosome has been performed previously.
     *
     * @param chr
     * @param nameSet -- The name set, e.g. "ucsc"
     * @returns {*|undefined}
     */
    getChromosomeAlias(chr, nameSet)
    {
        const aliasRecord =  this.aliasRecordCache.get(chr);
        return aliasRecord ? aliasRecord[nameSet] || chr : chr
    }

    /**
     * Search for chromosome alias bed record.  If found, cache results in the alias -> chr map
     * @param alias
     * @returns {Promise<any>}
     */
    async search(alias) {
        if (!this.aliasRecordCache.has(alias)) {
            const aliasRecord = await this.reader.search(alias);
            if (aliasRecord) {
                ChromAliasDefaults.addCaseAliases(aliasRecord);
                for (let key of Object.keys(aliasRecord)) {
                    if ("start" !== key && "end" !== key) {
                        this.aliasRecordCache.set(aliasRecord[key], aliasRecord);
                    }
                }
            }
        }
        return this.aliasRecordCache.get(alias)
    }
}

/**
 * Represenets a UCSC tab-delimited Alias file
 *
 * example  - header line is optional but reccomended, required to support altName setting for genome
 * # refseq	assembly	genbank	ncbi	ucsc
 * NC_048407.1	chr01	CM023106.1	1	chr1
 *
 * @param aliasURL
 * @param config
 * @returns {Promise<*[]>}
 */

class ChromAliasFile {

    aliasRecordCache = new Map()

    constructor(aliasURL, config, genome) {
        this.aliasURL = aliasURL;
        this.config = config;
        this.genome = genome;
    }

    async preload(chrNames) {
        // A no-op, this is a text file, no need to preload
    }
    /**
     * Return the canonical chromosome name for the alias.  If none found return the alias
     *
     * @param alias
     * @returns {*}
     */
    getChromosomeName(alias) {
        return this.aliasRecordCache.has(alias) ? this.aliasRecordCache.get(alias).chr : alias
    }

    /**
     * Return an alternate chromosome name (alias).  If not exists, return chr
     * @param chr
     * @param nameSet -- The name set, e.g. "ucsc"
     * @returns {*|undefined}
     */
    getChromosomeAlias(chr, nameSet)
    {
        const aliasRecord =  this.aliasRecordCache.get(chr);
        return aliasRecord ? aliasRecord[nameSet] || chr : chr
    }


    async loadAliases() {

        const data = await igvxhr$1.loadString(this.aliasURL, buildOptions(this.config));
        const lines = splitLines$3(data);
        const firstLine = lines[0];
        if (firstLine.startsWith("#")) {
            this.headings = firstLine.substring(1).split("\t").map(h => h.trim());
            this.altNameSets = this.headings.slice(1);
        }

        const chromosomeNameSet = this.genome.chromosomeNames ?
            new Set(this.genome.chromosomeNames) : new Set();

        for (let line of lines) {
            if (!line.startsWith("#") && line.length > 0) {
                const tokens = line.split("\t");

                // Find the canonical chromosome
                let chr = tokens.find(t => chromosomeNameSet.has(t));
                if(!chr) {
                    chr = tokens[0];
                }

                const aliasRecord = {chr};
                ChromAliasDefaults.addCaseAliases(aliasRecord);
                for (let i = 0; i < tokens.length; i++) {
                    const key = this.headings ? this.headings[i] : i;
                    aliasRecord[key] = tokens[i];
                }

                for (let a of Object.values(aliasRecord)) {
                    this.aliasRecordCache.set(a, aliasRecord);
                }

            }
        }
    }

    async search(alias) {
        if(this.aliasRecordCache.size === 0) {
            await this.loadAliases();
        }
        return this.aliasRecordCache.get(alias)

    }
}

class Cytoband {
    constructor(start, end, name, typestain) {
        this.start = start;
        this.end = end;
        this.name = name;
        this.stain = 0;

        // Set the type, either p, n, or c
        if (typestain === 'acen') {
            this.type = 'c';
        } else {
            this.type = typestain.charAt(1);
            if (this.type === 'p') {
                this.stain = parseInt(typestain.substring(4));
            }
        }
    }
}

class CytobandFileBB {

    cytobandMap = new Map()

    constructor(url, config, genome) {
        config = config || {};
        config.url = url;
        this.source = new BWSource(config, genome);
    }

    async getCytobands(chr) {

        if (this.cytobandMap.has(chr)) {
            return this.cytobandMap.get(chr)
        } else {
            let cytobands = await this.#readCytobands(chr);
            if (!cytobands) cytobands = [];  // Prevent loading again
            this.cytobandMap.set(chr, cytobands);
            return cytobands
        }
    }

    async #readCytobands(chr) {
        const features = await this.source.getFeatures({chr});
        return features.map(f => new Cytoband(f.start, f.end, f.name, f.gieStain))
    }
}

class CytobandFile {

    cytobands = new Map()

    constructor(url, config) {
        this.url = url;
        this.config = config;
    }

    async getCytobands(chr) {
        if(this.cytobands.size === 0) {
            await this.#loadCytobands();
        }
        return this.cytobands.get(chr)
    }


    /**
     * Load a UCSC bigbed cytoband file. Features are in bed+4 format.
     * {
     *   "chr": "chr1",
     *   "start": 0,
     *   "end": 1735965,
     *   "name": "p36.33",
     *   "gieStain": "gneg"
     * }
     * @returns {Promise<*[]>}
     */
    async #loadCytobands() {

        let data;
        if (isDataURL(this.url)) {
            const plain = decodeDataURI$1(this.url);
            data = "";
            const len = plain.length;
            for (let i = 0; i < len; i++) {
                data += String.fromCharCode(plain[i]);
            }
        } else {
            data = await igvxhr$1.loadString(this.url, buildOptions(this.config));
        }

        let lastChr;
        let bands = [];
        const lines = splitLines$3(data);
        for (let line of lines) {

            const tokens = line.split("\t");
            const chrName = tokens[0];
            if (!lastChr) lastChr = chrName;

            if (chrName !== lastChr) {
                this.cytobands.set(lastChr, bands);
                bands = [];
                lastChr = chrName;
            }

            if (tokens.length === 5) {
                //10	0	3000000	p15.3	gneg
                const start = parseInt(tokens[1]);
                const end = parseInt(tokens[2]);
                const name = tokens[3];
                const stain = tokens[4];
                bands.push(new Cytoband(start, end, name, stain));
            }
        }
        if(bands.length > 0) {
            this.cytobands.set(lastChr, bands);
        }

    }

}

/**
 * Update deprecated fasta & index urls in the reference object.
 */

const isString$4 = (x) => {
    return (x && typeof x === "string") || x instanceof String
};

/**
 * Replaces deprecated s3 fasta URLs with twoBitURLs.  The purpose here is to rescue references from saved sessions
 * that contain pointers to deprectated IGV s3 buckets.  These buckets will eventually be deleted
 *
 * @param reference
 */
function updateReference(reference) {

    if (!(requiresUpdate(reference) && updates[reference.id])) {
        return
    }

    const updatedReference = updates[reference.id];
    if (updatedReference) {
        delete reference.fastaURL;
        if (reference.indexURL) delete reference.indexURL;
        reference.twoBitURL = updatedReference.twoBitURL;
        if (updatedReference.twoBitBptURL) reference.twoBitBptURL = updatedReference.twoBitBptURL;
        if (updatedReference.chromSizesURL) reference.chromSizesURL = updatedReference.chromSizesURL;
    }
}

function requiresUpdate(reference) {
    return isString$4(reference.fastaURL) &&
        (reference.fastaURL.startsWith("https://igv.org") ||
            ["igv.org.genomes", "igv.broadinstitute.org", "igv.genepattern.org", "igvdata.broadinstitute.org",
                "igv-genepattern-org"].some(bucket => reference.fastaURL.includes(bucket)))
}

const updates = {
    "hs1": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/hs1/bigZips/hs1.2bit",
        "twoBitBptURL": "https://hgdownload.soe.ucsc.edu/goldenPath/hs1/bigZips/hs1.2bit.bpt",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/hs1/bigZips/hs1.chrom.sizes.txt"
    },
    "hg38": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/hg38/bigZips/hg38.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/hg38/bigZips/hg38.chrom.sizes"
    },
    "hg19": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/hg19/bigZips/hg19.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/hg19/bigZips/hg19.chrom.sizes"
    },
    "hg18": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/hg18/bigZips/hg18.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/hg18/bigZips/hg18.chrom.sizes"
    },
    "mm39": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/mm39/bigZips/mm39.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/mm39/bigZips/mm39.chrom.sizes"
    },
    "mm10": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/mm10/bigZips/mm10.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/mm10/bigZips/mm10.chrom.sizes"
    },
    "mm9": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/mm9/bigZips/mm9.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/mm9/bigZips/mm9.chrom.sizes"
    },
    "rn7": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/hubs/GCF/015/227/675/GCF_015227675.2/GCF_015227675.2.2bit",
        "twoBitBptURL": "https://hgdownload.soe.ucsc.edu/hubs/GCF/015/227/675/GCF_015227675.2/GCF_015227675.2.2bit.bpt",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/hubs/GCF/015/227/675/GCF_015227675.2/GCF_015227675.2.chrom.sizes.txt"
    },
    "rn6": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/hubs/GCF/000/001/895/GCF_000001895.5/GCF_000001895.5.2bit",
        "twoBitBptURL": "https://hgdownload.soe.ucsc.edu/hubs/GCF/000/001/895/GCF_000001895.5/GCF_000001895.5.2bit.bpt",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/hubs/GCF/000/001/895/GCF_000001895.5/GCF_000001895.5.chrom.sizes.txt"
    },
    "gorGor6": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/gorGor6/bigZips/gorGor6.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/gorGor6/bigZips/gorGor6.chrom.sizes"
    },
    "gorGor4": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/gorGor4/bigZips/gorGor4.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/gorGor4/bigZips/gorGor4.chrom.sizes"
    },
    "panTro6": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/panTro6/bigZips/panTro6.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/panTro6/bigZips/panTro6.chrom.sizes"
    },
    "panTro5": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/panTro5/bigZips/panTro5.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/panTro5/bigZips/panTro5.chrom.sizes"
    },
    "panTro4": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/panTro4/bigZips/panTro4.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/panTro4/bigZips/panTro4.chrom.sizes"
    },
    "macFas5": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/macFas5/bigZips/macFas5.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/macFas5/bigZips/macFas5.chrom.sizes"
    },
    "panPan2": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/panPan2/bigZips/panPan2.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/panPan2/bigZips/panPan2.chrom.sizes"
    },
    "canFam6": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/canFam6/bigZips/canFam6.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/canFam6/bigZips/canFam6.chrom.sizes"
    },
    "canFam5": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/canFam5/bigZips/canFam5.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/canFam5/bigZips/canFam5.chrom.sizes"
    },
    "canFam4": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/canFam4/bigZips/canFam4.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/canFam4/bigZips/canFam4.chrom.sizes"
    },
    "canFam3": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/canFam3/bigZips/canFam3.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/canFam3/bigZips/canFam3.chrom.sizes"
    },
    "bosTau9": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/bosTau9/bigZips/bosTau9.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/bosTau9/bigZips/bosTau9.chrom.sizes"
    },
    "bosTau8": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/bosTau8/bigZips/bosTau8.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/bosTau8/bigZips/bosTau8.chrom.sizes"
    },
    "susScr11": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/susScr11/bigZips/susScr11.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/susScr11/bigZips/susScr11.chrom.sizes"
    },
    "galGal6": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/galGal6/bigZips/galGal6.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/galGal6/bigZips/galGal6.chrom.sizes"
    },
    "danRer11": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/danRer11/bigZips/danRer11.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/danRer11/bigZips/danRer11.chrom.sizes"
    },
    "danRer10": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/danRer10/bigZips/danRer10.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/danRer10/bigZips/danRer10.chrom.sizes"
    },
    "ce11": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/ce11/bigZips/ce11.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/ce11/bigZips/ce11.chrom.sizes"
    },
    "dm6": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/dm6/bigZips/dm6.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/dm6/bigZips/dm6.chrom.sizes"
    },
    "dm3": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/dm3/bigZips/dm3.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/goldenPath/dm3/bigZips/dm3.chrom.sizes"
    },
    "sacCer3": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/hubs/GCF/000/146/045/GCF_000146045.2/GCF_000146045.2.2bit",
        "twoBitBptURL": "https://hgdownload.soe.ucsc.edu/hubs/GCF/000/146/045/GCF_000146045.2/GCF_000146045.2.2bit.bpt",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/hubs/GCF/000/146/045/GCF_000146045.2/GCF_000146045.2.chrom.sizes.txt"
    },
    "GCF_000002945.1": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/hubs/GCF/000/002/945/GCF_000002945.1/GCF_000002945.1.2bit",
        "twoBitBptURL": "https://hgdownload.soe.ucsc.edu/hubs/GCF/000/002/945/GCF_000002945.1/GCF_000002945.1.2bit.bpt",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/hubs/GCF/000/002/945/GCF_000002945.1/GCF_000002945.1.chrom.sizes.txt"
    },
    "GCF_009858895.2": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/hubs/GCF/009/858/895/GCF_009858895.2/GCF_009858895.2.2bit",
        "twoBitBptURL": "https://hgdownload.soe.ucsc.edu/hubs/GCF/009/858/895/GCF_009858895.2/GCF_009858895.2.2bit.bpt",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/hubs/GCF/009/858/895/GCF_009858895.2/GCF_009858895.2.chrom.sizes.txt"
    },
    "tair10": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/hubs/GCF/000/001/735/GCF_000001735.3/GCF_000001735.3.2bit",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/hubs/GCF/000/001/735/GCF_000001735.3/GCF_000001735.3.chrom.sizes.txt"
    },
    "GCA_000022165.1": {
        "twoBitURL": "https://hgdownload.soe.ucsc.edu/hubs/GCA/000/022/165/GCA_000022165.1/GCA_000022165.1.2bit",
        "twoBitBptURL": "https://hgdownload.soe.ucsc.edu/hubs/GCA/000/022/165/GCA_000022165.1/GCA_000022165.1.2bit.bpt",
        "chromSizesURL": "https://hgdownload.soe.ucsc.edu/hubs/GCA/000/022/165/GCA_000022165.1/GCA_000022165.1.chrom.sizes.txt"
    }
};

const ucsdIDMap = new Map([
    ["1kg_ref", "hg18"],
    ["1kg_v37", "hg19"],
    ["b37", "hg19"]
]);

/**
 * The Genome class represents an assembly and consists of the following elements
 *   sequence - Object representing the DNA sequence
 *   chromosomes - Objects with chromosome meta data including name, length, and alternate names (aliases)
 *   aliases - table of chromosome name aliases (optional)
 *   cytobands - cytoband data for drawing an ideogram (optional)
 */

class Genome {

    #wgChromosomeNames
    #aliasRecordCache = new Map()

    static async createGenome(options, browser) {

        updateReference(options);
        const genome = new Genome(options, browser);
        await genome.init();
        return genome
    }

    constructor(config, browser) {
        this.config = config;
        this.browser = browser;
        this.id = config.id || generateGenomeID(config);
        this.ucscID = config.ucscID || ucsdIDMap.get(this.id) || this.id;
        this.blatDB = config.blatDB || this.ucscID;
        this.name = config.name;
        this.nameSet = config.nameSet || 'ucsc';
    }


    async init() {

        const config = this.config;

        // Load sequence
        this.sequence = await loadSequence(config, this.browser);

        // Load cytobands.  This is optional but required to support the ideogram.  Only needed for whole genome view
        if(false !== config.showIdeogram && false !== config.wholeGenomeView) {
            if (config.cytobandURL) {
                this.cytobandSource = new CytobandFile(config.cytobandURL, Object.assign({}, config));
            } else if (config.cytobandBbURL) {
                this.cytobandSource = new CytobandFileBB(config.cytobandBbURL, Object.assign({}, config), this);
            }
        }

            // Search for chromosomes, that is an array of chromosome objects containing name and length.  This is
            // optional but required to support whole genome view.
        if (this.sequence.chromosomes) {
            this.chromosomes = this.sequence.chromosomes;
        } else if (config.chromSizesURL) {
            this.chromosomes = await loadChromSizes(config.chromSizesURL);
        } else {
            this.chromosomes = new Map();   // Cache, chromosome are added as they are loaded
        }

        // Search for chromosome names.  This is optional but required to support the chromosome pulldown
        if (this.sequence.chromosomeNames) {
            this.chromosomeNames = this.sequence.chromosomeNames;    // Twobit files can supply chromosome names unless they use an external index
        } else if (this.chromosomes.size > 0) {
            this.chromosomeNames = Array.from(this.chromosomes.keys());
        }

        // Chromosome alias
        if (config.chromAliasBbURL) {
            this.chromAlias = new ChromAliasBB(config.chromAliasBbURL, Object.assign({}, config), this);
        } else if (config.aliasURL) {
            this.chromAlias = new ChromAliasFile(config.aliasURL, Object.assign({}, config), this);
        } else if (this.chromosomeNames) {
            this.chromAlias = new ChromAliasDefaults(this.id, this.chromosomeNames);
        }

        if (false !== config.wholeGenomeView && this.chromosomes.size > 0) {
            // Set chromosome order for WG view and chromosome pulldown.  If chromosome order is not specified sort
            if (config.chromosomeOrder) {
                if (Array.isArray(config.chromosomeOrder)) {
                    this.#wgChromosomeNames = config.chromosomeOrder;
                } else {
                    this.#wgChromosomeNames = config.chromosomeOrder.split(',').map(nm => nm.trim());
                }
                // Trim to remove non-existent chromosomes
                await this.chromAlias.preload(this.#wgChromosomeNames);
                this.#wgChromosomeNames =
                    this.#wgChromosomeNames.map(c =>  this.getChromosomeName(c)).filter(c => this.chromosomes.has(c));
            } else {
                this.#wgChromosomeNames = trimSmallChromosomes(this.chromosomes);
                await this.chromAlias.preload(this.#wgChromosomeNames);
            }
        }

        // Optionally create the psuedo chromosome "all" to support whole genome view
        this.wholeGenomeView = config.wholeGenomeView !== false && this.#wgChromosomeNames && this.chromosomes.size > 1;
        if (this.wholeGenomeView) {
            const l = this.#wgChromosomeNames.reduce((accumulator, currentValue) => accumulator += this.chromosomes.get(currentValue).bpLength, 0);
            this.chromosomes.set("all", new Chromosome("all", 0, l));
        }
    }

    get description() {
        return this.config.description || `${this.id}\n${this.name}`
    }

    get infoURL() {
        return this.config.infoURL
    }

    showWholeGenomeView() {
        return this.wholeGenomeView
    }

    /**
     * Return a json like object representing the current state.  The tracks collection is nullified
     * as tracks are transferred to the browser object on loading.
     *
     * @returns {any}
     */
    toJSON() {
        return Object.assign({}, this.config, {tracks: undefined})
    }

    get initialLocus() {
        return this.config.locus ? this.config.locus : this.getHomeChromosomeName()
    }

    getHomeChromosomeName() {
        if (this.showWholeGenomeView() && this.chromosomes.has("all")) {
            return "all"
        } else if (this.chromosomeNames) {
            return this.chromosomeNames[0]
        } else ;
    }

    getChromosomeName(chr) {
        return this.chromAlias ? this.chromAlias.getChromosomeName(chr, this.chromosomes.keys()) : chr
    }

    getChromosomeDisplayName(str) {
        if (this.nameSet && this.chromAlias) {
            return this.chromAlias.getChromosomeAlias(str, this.nameSet) || str
        } else {
            return str
        }
    }

    getChromosome(chr) {
        if (this.chromAlias) {
            chr = this.chromAlias.getChromosomeName(chr);
        }
        return this.chromosomes.get(chr)
    }

    async loadChromosome(chr) {

        const chromAliasRecord = await this.getAliasRecord(chr);
        if (chromAliasRecord) {
            chr = chromAliasRecord.chr;
        }

        if (!this.chromosomes.has(chr)) {
            let chromosome;
            const sequenceRecord = await this.sequence.getSequenceRecord(chr);
            if (sequenceRecord) {
                chromosome = new Chromosome(chr, 0, sequenceRecord.bpLength);
            }

            this.chromosomes.set(chr, chromosome);  // <= chromosome might be undefined, setting it prevents future attempts
        }

        return this.chromosomes.get(chr)
    }

    async getAliasRecord(chr) {
        if (this.#aliasRecordCache.has(chr)) {
            return this.#aliasRecordCache.get(chr)
        }
        if (this.chromAlias) {
            let aliasRecord = await this.chromAlias.search(chr);
            if (!aliasRecord && chr !== chr.toLowerCase()) {
                aliasRecord = await this.chromAlias.search(chr.toLowerCase());
            }
            if (aliasRecord) {
                // Add some aliases for case insensitivy
                const upper = aliasRecord.chr.toUpperCase();
                const lower = aliasRecord.chr.toLowerCase();
                const cap = aliasRecord.chr.charAt(0).toUpperCase() + aliasRecord.chr.slice(1);
                if (aliasRecord.chr !== upper) {
                    aliasRecord["_uppercase"] = upper;
                }
                if (aliasRecord.chr !== lower) {
                    aliasRecord["_lowercase"] = lower;
                }
                if (aliasRecord.chr !== cap) {
                    aliasRecord["_cap"] = cap;
                }
            }
            this.#aliasRecordCache.set(chr, aliasRecord);  // Set even if undefined to prevent recurrent searches
            return aliasRecord
        }
    }

    async getCytobands(chr) {
        if (this.cytobandSource) {
            const chrName = this.getChromosomeName(chr);
            const cytos = await this.cytobandSource.getCytobands(chrName);
            return cytos
        }
    }

    getChromosomes() {
        return this.chromosomes
    }

    get wgChromosomeNames() {
        return this.#wgChromosomeNames ? this.#wgChromosomeNames.slice() : undefined
    }

    get showChromosomeWidget() {
        return this.config.showChromosomeWidget
    }

    /**
     * Return the genome coordinate in kb for the give chromosome and position.
     * NOTE: This might return undefined if the chr is filtered from whole genome view.
     */
    getGenomeCoordinate(chr, bp) {

        var offset = this.getCumulativeOffset(chr);
        if (offset === undefined) return undefined

        return offset + bp
    }

    /**
     * Return the chromosome and coordinate in bp for the given genome coordinate
     */
    getChromosomeCoordinate(genomeCoordinate) {

        if (this.cumulativeOffsets === undefined) {
            this.cumulativeOffsets = computeCumulativeOffsets.call(this);
        }

        let lastChr = undefined;
        let lastCoord = 0;
        for (let name of this.#wgChromosomeNames) {

            const cumulativeOffset = this.cumulativeOffsets[name];
            if (cumulativeOffset > genomeCoordinate) {
                const position = genomeCoordinate - lastCoord;
                return {chr: lastChr, position: position}
            }
            lastChr = name;
            lastCoord = cumulativeOffset;
        }

        // If we get here off the end
        return {chr: this.#wgChromosomeNames[this.#wgChromosomeNames.length - 1], position: 0}

    };


    /**
     * Return the offset in genome coordinates (kb) of the start of the given chromosome
     * NOTE:  This might return undefined if the chromosome is filtered from whole genome view.
     */
    getCumulativeOffset(chr) {

        if (this.cumulativeOffsets === undefined) {
            this.cumulativeOffsets = computeCumulativeOffsets.call(this);
        }

        const queryChr = this.getChromosomeName(chr);
        return this.cumulativeOffsets[queryChr]

        function computeCumulativeOffsets() {

            let acc = {};
            let offset = 0;
            for (let name of this.#wgChromosomeNames) {
                acc[name] = Math.floor(offset);
                const chromosome = this.getChromosome(name);
                offset += chromosome.bpLength;
            }

            return acc
        }
    }

    /**
     * Return the nominal genome length, this is the length of the main chromosomes (no scaffolds, etc).
     */
    getGenomeLength() {

        if (!this.bpLength) {
            let bpLength = 0;
            for (let cname of this.#wgChromosomeNames) {
                let c = this.chromosomes.get(cname);
                bpLength += c.bpLength;
            }
            this.bpLength = bpLength;
        }
        return this.bpLength
    }

    async getSequence(chr, start, end) {
        chr = this.getChromosomeName(chr);
        return this.sequence.getSequence(chr, start, end)
    }

    /**
     * Return loaded sequence (i.e. cached or otherwise loaded) spanning the given region.  If no sequence has been
     * loaded returns undefined.
     *
     * @param chr
     * @param start
     * @param end
     */
    getSequenceInterval(chr, start, end) {
        if (typeof this.sequence.getSequenceInterval === 'function') {
            return this.sequence.getSequenceInterval(chr, start, end)
        } else {
            return undefined
        }
    }

    getHubURLs() {
        return this.config.hubs
    }
}

/**
 * Trim small sequences (chromosomes) and return the list of trimmed chromosome names.
 * The results are used to construct the whole genome view and optionally chromosome pulldown
 * *
 * @param config - the "reference" configuration object
 * @returns {string|*|*[]|string[]}
 */
function trimSmallChromosomes(chromosomes) {

    const wgChromosomeNames = [];
    let runningAverage;
    let i = 1;
    for (let c of chromosomes.values()) {
        if (!runningAverage) {
            runningAverage = c.bpLength;
            wgChromosomeNames.push(c.name);
        } else {
            if (c.bpLength < runningAverage / 100) {
                continue
            }
            runningAverage = ((i - 1) * runningAverage + c.bpLength) / i;
            wgChromosomeNames.push(c.name);
        }
        i++;
    }
    return wgChromosomeNames
}

function generateGenomeID(config) {
    if (config.id !== undefined) {
        return config.id
    } else if (config.fastaURL && isString$3(config.fastaURL) && !config.fastaURL.startsWith("data:")) {
        return config.fastaURL
    } else if (config.fastaURL && config.fastaURL.name) {
        return config.fastaURL.name
    } else {
        return ""
    }
}

var igvCss = '.igv-ui-dropdown{cursor:default;position:absolute;top:0;left:0;z-index:2048;border-color:#7f7f7f;border-style:solid;border-width:1px;font-family:\"Open Sans\",sans-serif;font-size:small;font-weight:400;background-color:#fff}.igv-ui-dropdown>div{overflow-y:auto;overflow-x:hidden;background-color:#fff}.igv-ui-dropdown>div>div{padding:4px;width:100%;overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap;border-bottom-color:#7f7f7f;border-bottom-style:solid;border-bottom-width:1px;background-color:#fff}.igv-ui-dropdown>div>div:last-child{border-bottom-color:rgba(0,0,0,0);border-bottom-width:0}.igv-ui-dropdown>div>div:hover{cursor:pointer;background-color:rgba(0,0,0,.04)}.igv-ui-popover{cursor:default;position:absolute;z-index:2048;border-color:#7f7f7f;border-radius:5px;border-style:solid;border-width:1px;font-family:\"Open Sans\",sans-serif;font-size:small;background-color:#fff}.igv-ui-popover>div:first-child{display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:space-between;align-items:center;width:100%;height:24px;cursor:move;border-top-width:0;border-top-left-radius:5px;border-top-right-radius:5px;border-bottom-color:#7f7f7f;border-bottom-style:solid;border-bottom-width:thin;background-color:#eee}.igv-ui-popover>div:first-child>div:first-child{margin-left:4px}.igv-ui-popover>div:first-child>div:last-child{margin-right:4px;height:12px;width:12px;color:#7f7f7f}.igv-ui-popover>div:first-child>div:last-child:hover{cursor:pointer;color:#444}.igv-ui-popover>div:last-child{user-select:text;overflow-y:auto;overflow-x:hidden;max-height:400px;max-width:800px;background-color:#fff;border-bottom-width:0;border-bottom-left-radius:5px;border-bottom-right-radius:5px}.igv-ui-popover>div:last-child>div{margin-left:4px;margin-right:4px;min-width:220px;overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap}.igv-ui-popover>div:last-child>div>span{font-weight:bolder}.igv-ui-popover>div:last-child hr{width:100%}.igv-track-label-popover{position:absolute;z-index:2048;border:1px solid #7f7f7f;border-radius:5px;background:#fff;font-family:\"Open Sans\",sans-serif;font-size:small;min-width:260px;max-width:800px;box-shadow:0 2px 8px rgba(0,0,0,.12);user-select:text}.igv-track-label-popover__header{display:flex;align-items:center;justify-content:space-between;height:24px;background:#eee;border-top-left-radius:5px;border-top-right-radius:5px;border-bottom:1px solid #7f7f7f;padding:0 8px;cursor:move}.igv-track-label-popover__title{font-weight:bold;font-size:1em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.igv-track-label-popover__close{width:14px;height:14px;color:#7f7f7f;display:flex;align-items:center;justify-content:center;border-radius:2px;cursor:pointer;transition:background .2s,color .2s;margin-left:8px}.igv-track-label-popover__close svg{width:14px;height:14px}.igv-track-label-popover__close:hover,.igv-track-label-popover__close:focus{background:#ddd;color:#444;outline:none}.igv-track-label-popover__body{max-height:400px;overflow-y:auto;padding:8px 0;background:#fff;border-bottom-left-radius:5px;border-bottom-right-radius:5px}.igv-track-label-popover__row{display:flex;align-items:baseline;margin:0 12px 4px 12px;min-width:220px;white-space:nowrap;overflow-x:hidden;text-overflow:ellipsis}.igv-track-label-popover__key{font-weight:bold;margin-right:.5em}.igv-track-label-popover__value{font-weight:normal;flex:1 1 auto;overflow-x:hidden;text-overflow:ellipsis}.igv-track-label-popover hr{width:100%;border:none;border-top:1px solid #7f7f7f;margin:8px 0}.igv-ui-alert-dialog-container{position:fixed;top:20%;left:50%;transform:translateX(-50%);z-index:2048;box-sizing:content-box;width:400px;height:200px;border-color:#7f7f7f;border-radius:5px;border-style:solid;border-width:thin;outline:none;font-family:\"Open Sans\",sans-serif;font-size:15px;font-weight:400;background-color:#fff;display:flex;flex-flow:column;flex-wrap:nowrap;justify-content:space-between;align-items:center}.igv-ui-alert-dialog-container>div:first-child{display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-start;align-items:center;width:100%;height:24px;cursor:move;border-top-left-radius:5px;border-top-right-radius:5px;border-bottom-color:#7f7f7f;border-bottom-style:solid;border-bottom-width:thin;background-color:#eee}.igv-ui-alert-dialog-container>div:first-child div:first-child{padding-left:8px}.igv-ui-alert-dialog-container .igv-ui-alert-dialog-body{-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;color:#373737;width:100%;height:calc(100% - 24px - 64px);overflow-y:scroll}.igv-ui-alert-dialog-container .igv-ui-alert-dialog-body .igv-ui-alert-dialog-body-copy{margin:16px;width:auto;height:auto;overflow-wrap:break-word;word-break:break-word;background-color:#fff;border:unset}.igv-ui-alert-dialog-container>div:last-child{width:100%;margin-bottom:10px;background-color:#fff;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:center;align-items:center}.igv-ui-alert-dialog-container>div:last-child div{margin:unset;width:40px;height:30px;line-height:30px;text-align:center;color:#fff;font-family:\"Open Sans\",sans-serif;font-size:small;font-weight:400;border-color:#2b81af;border-style:solid;border-width:thin;border-radius:4px;background-color:#2b81af}.igv-ui-alert-dialog-container>div:last-child div:hover{cursor:pointer;border-color:#25597f;background-color:#25597f}.igv-ui-generic-dialog-container{box-sizing:content-box;position:fixed;top:20%;left:75%;transform:translateX(-50%);z-index:2048;background-color:#fff;cursor:pointer;width:300px;height:fit-content;padding-bottom:16px;border-color:#7f7f7f;border-radius:5px;border-style:solid;border-width:thin;font-family:\"Open Sans\",sans-serif;font-size:medium;font-weight:400;display:flex;flex-flow:column;flex-wrap:nowrap;justify-content:flex-start;align-items:center}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-header{display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-end;align-items:center;width:100%;height:24px;cursor:move;border-top-left-radius:5px;border-top-right-radius:5px;border-bottom-color:#7f7f7f;border-bottom-style:solid;border-bottom-width:thin;background-color:#eee}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-header div{margin-right:4px;margin-bottom:2px;height:12px;width:12px;color:#7f7f7f}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-header div:hover{cursor:pointer;color:#444}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-one-liner{color:#373737;width:95%;line-height:24px;text-align:left;margin-top:8px;padding-left:8px;overflow-wrap:break-word;background-color:#fff}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input{margin-top:8px;width:95%;height:24px;color:#373737;line-height:24px;padding-left:8px;background-color:#fff;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-start;align-items:center}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input>div{width:fit-content;height:100%;font-size:16px;text-align:right;padding-right:8px;background-color:#fff}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input input{display:block;height:100%;width:100%;padding-left:4px;font-family:\"Open Sans\",sans-serif;font-weight:400;color:#373737;text-align:left;outline:none;border-style:solid;border-width:thin;border-color:#7f7f7f;background-color:#fff}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input input{width:50%;font-size:16px}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input{margin-top:8px;width:calc(100% - 16px);height:24px;color:#373737;line-height:24px;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:space-around;align-items:center}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input input{display:block;height:100%;width:100%;padding-left:4px;font-family:\"Open Sans\",sans-serif;font-weight:400;color:#373737;text-align:left;outline:none;border-style:solid;border-width:thin;border-color:#7f7f7f;background-color:#fff}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input input{font-size:16px}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input input[type=range]{width:70%;-webkit-appearance:none;background:linear-gradient(90deg, white, black);outline:none;margin:0}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input output{display:block;height:100%;width:20%;font-size:16px}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel{width:100%;height:28px;padding-top:16px;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:space-around;align-items:center}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel>div{color:#fff;font-family:\"Open Sans\",sans-serif;font-size:14px;font-weight:400;width:75px;height:28px;line-height:28px;text-align:center;border-color:rgba(0,0,0,0);border-style:solid;border-width:thin;border-radius:2px}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel>div:first-child{margin-left:32px;margin-right:0;background-color:#5ea4e0}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel>div:last-child{margin-left:0;margin-right:32px;background-color:#c4c4c4}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel>div:first-child:hover{cursor:pointer;background-color:#3b5c7f}.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel>div:last-child:hover{cursor:pointer;background-color:#7f7f7f}.igv-ui-generic-container{position:fixed;cursor:pointer;background-color:#fff;z-index:2048;box-sizing:content-box;width:300px;height:200px;top:20%;left:75%;transform:translateX(-50%);display:flex;flex-direction:row;flex-wrap:wrap;justify-content:flex-start;align-items:center}.igv-ui-generic-container>div:nth-child(1){cursor:move;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-end;align-items:center;height:24px;width:100%;background-color:#ddd}.igv-ui-generic-container>div:nth-child(1)>div{display:block;color:#5f5f5f;cursor:pointer;width:14px;height:14px;margin-right:8px;margin-bottom:4px}.igv-ui-colorpicker-container{background-color:#eee;height:fit-content;flex-direction:column;flex-wrap:nowrap}.igv-ui-colorpicker-container>div:nth-child(2){display:flex;flex-direction:row;flex-wrap:wrap;justify-content:flex-start;align-items:center}.igv-ui-colorpicker-container>div:nth-child(3){box-sizing:border-box;position:relative;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-start;align-items:center;padding:.5rem;font-family:\"Open Sans\",sans-serif;font-size:1rem;font-weight:400;width:100%;border-top-style:solid;border-top-width:thin;border-top-color:#373737}.igv-ui-colorpicker-container>div:nth-child(4){width:100%;display:flex;flex-direction:row;flex-wrap:wrap;justify-content:flex-start;align-items:center}.igv-ui-color-swatch{position:relative;box-sizing:content-box;display:flex;flex-flow:row;flex-wrap:wrap;justify-content:center;align-items:center;margin:2px;width:32px;height:32px;border-style:solid;border-width:0;border-color:#fff;border-radius:3px}.igv-ui-color-swatch-shim{cursor:pointer;position:relative;box-sizing:content-box;display:flex;flex-flow:row;flex-wrap:wrap;justify-content:center;align-items:center;width:32px;height:32px;background-color:#fff;border-style:solid;border-width:thin;border-color:#fff;border-radius:4px}.igv-ui-dialog{z-index:2048;position:fixed;width:fit-content;height:fit-content;display:flex;flex-flow:column;flex-wrap:nowrap;justify-content:flex-start;background-color:#fff;border-color:#7f7f7f;border-radius:5px;border-style:solid;border-width:thin;font-family:\"Open Sans\",sans-serif;font-size:medium;font-weight:400}.igv-ui-dialog .igv-ui-dialog-header{display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-end;align-items:center;width:100%;height:24px;cursor:move;border-top-left-radius:5px;border-top-right-radius:5px;border-bottom-color:#7f7f7f;border-bottom-style:solid;border-bottom-width:thin;background-color:#eee}.igv-ui-dialog .igv-ui-dialog-header div{margin-right:4px;margin-bottom:2px;height:12px;width:12px;color:#7f7f7f}.igv-ui-dialog .igv-ui-dialog-header div:hover{cursor:pointer;color:#444}.igv-ui-dialog .igv-ui-dialog-one-liner{width:95%;height:24px;line-height:24px;text-align:left;margin:8px;overflow-wrap:break-word;background-color:#fff;font-weight:bold}.igv-ui-dialog .igv-ui-dialog-ok-cancel{width:100%;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:space-around;align-items:center}.igv-ui-dialog .igv-ui-dialog-ok-cancel div{margin:16px;margin-top:32px;color:#fff;font-family:\"Open Sans\",sans-serif;font-size:14px;font-weight:400;width:75px;height:28px;line-height:28px;text-align:center;border-color:rgba(0,0,0,0);border-style:solid;border-width:thin;border-radius:2px}.igv-ui-dialog .igv-ui-dialog-ok-cancel div:first-child{background-color:#5ea4e0}.igv-ui-dialog .igv-ui-dialog-ok-cancel div:last-child{background-color:#c4c4c4}.igv-ui-dialog .igv-ui-dialog-ok-cancel div:first-child:hover{cursor:pointer;background-color:#3b5c7f}.igv-ui-dialog .igv-ui-dialog-ok-cancel div:last-child:hover{cursor:pointer;background-color:#7f7f7f}.igv-ui-dialog .igv-ui-dialog-ok{width:100%;height:36px;margin-top:32px;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:space-around;align-items:center}.igv-ui-dialog .igv-ui-dialog-ok div{width:98px;height:36px;line-height:36px;text-align:center;color:#fff;font-family:\"Open Sans\",sans-serif;font-size:medium;font-weight:400;border-color:#fff;border-style:solid;border-width:thin;border-radius:4px;background-color:#2b81af}.igv-ui-dialog .igv-ui-dialog-ok div:hover{cursor:pointer;background-color:#25597f}.igv-ui-panel,.igv-ui-panel-row,.igv-ui-panel-column{z-index:2048;background-color:#fff;font-family:\"Open Sans\",sans-serif;font-size:medium;font-weight:400;display:flex;justify-content:flex-start;align-items:flex-start}.igv-ui-panel-column{display:flex;flex-direction:column}.igv-ui-panel-row{display:flex;flex-direction:row}.igv-ui-textbox{background-color:#fff;font-family:\"Open Sans\",sans-serif;font-size:medium;font-weight:400;display:flex;justify-content:flex-start;align-items:flex-start}.igv-ui-table{background-color:#fff}.igv-ui-table thead{position:sticky;top:0}.igv-ui-table th{text-align:left}.igv-ui-table td{padding-right:20px}.igv-ui-table tr:hover{background-color:#add8e6}.igv-roi-seg-filter-dialog{box-sizing:content-box;position:fixed;top:20%;left:75%;transform:translateX(-50%);z-index:2048;background-color:#fff;cursor:pointer;width:350px;height:fit-content;padding-bottom:16px;border-color:#7f7f7f;border-radius:5px;border-style:solid;border-width:thin;font-family:\"Open Sans\",sans-serif;font-size:medium;font-weight:400;display:flex;flex-flow:column;flex-wrap:nowrap;justify-content:flex-start;align-items:center}.igv-roi-seg-filter-dialog__header{display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-end;align-items:center;width:100%;height:24px;cursor:move;border-top-left-radius:5px;border-top-right-radius:5px;border-bottom-color:#7f7f7f;border-bottom-style:solid;border-bottom-width:thin;background-color:#eee}.igv-roi-seg-filter-dialog__header div{margin-right:4px;margin-bottom:2px;height:12px;width:12px;color:#7f7f7f}.igv-roi-seg-filter-dialog__header div:hover{cursor:pointer;color:#444}.igv-roi-seg-filter-dialog__radio-group{margin-top:8px;display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start}.igv-roi-seg-filter-dialog__radio-group label{display:flex;align-items:center;gap:4px;cursor:pointer}.igv-roi-seg-filter-dialog__radio-group input[type=radio]{cursor:pointer}.igv-roi-seg-filter-dialog__radio-group .op{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;margin-left:16px;margin-bottom:8px;gap:2px}.igv-roi-seg-filter-dialog__radio-group .op input[type=radio]{display:block;margin:0}.igv-roi-seg-filter-dialog__radio-group .op label{display:block;margin-left:4px}.igv-roi-seg-filter-dialog__one-liner{color:#373737;width:95%;line-height:24px;text-align:left;margin-top:8px;padding-left:8px;overflow-wrap:break-word;background-color:#fff}.igv-roi-seg-filter-dialog__input{margin-top:8px;width:80%;height:24px;color:#373737;line-height:24px;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:space-around;align-items:center}.igv-roi-seg-filter-dialog__input input{display:block;height:100%;width:100%;padding-left:4px;font-family:\"Open Sans\",sans-serif;font-weight:400;color:#373737;text-align:left;outline:none;border-style:solid;border-width:thin;border-color:#7f7f7f;background-color:#fff}.igv-roi-seg-filter-dialog__input input{font-size:16px}.igv-roi-seg-filter-dialog__ok-cancel{width:100%;height:28px;padding-top:16px;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:space-around;align-items:center}.igv-roi-seg-filter-dialog__ok-cancel>div{color:#fff;font-family:\"Open Sans\",sans-serif;font-size:14px;font-weight:400;width:75px;height:28px;line-height:28px;text-align:center;border-color:rgba(0,0,0,0);border-style:solid;border-width:thin;border-radius:2px}.igv-roi-seg-filter-dialog__ok-cancel>div:first-child{margin-left:32px;margin-right:0;background-color:#5ea4e0}.igv-roi-seg-filter-dialog__ok-cancel>div:last-child{margin-left:0;margin-right:32px;background-color:#c4c4c4}.igv-roi-seg-filter-dialog__ok-cancel>div:first-child:hover{cursor:pointer;background-color:#3b5c7f}.igv-roi-seg-filter-dialog__ok-cancel>div:first-child.disabled{cursor:not-allowed;background-color:#ccc;opacity:.6}.igv-roi-seg-filter-dialog__ok-cancel>div:first-child.disabled:hover{cursor:not-allowed;background-color:#ccc}.igv-roi-seg-filter-dialog__ok-cancel>div:last-child:hover{cursor:pointer;background-color:#7f7f7f}.igv-clear-filters__container{font-family:\"Open Sans\",sans-serif;font-size:medium;font-weight:400;display:flex;flex-direction:column;max-width:300px;background-color:#fff;padding:12px;gap:8px;border-color:#7f7f7f;border-radius:5px;border-style:solid;border-width:thin}.igv-clear-filters__row{display:flex;align-items:flex-start;padding:12px;gap:12px;cursor:pointer}.igv-clear-filters__row:last-of-type{border-bottom:none}.igv-clear-filters__track-container{border-bottom:1px solid #7f7f7f;padding-bottom:8px;margin-bottom:8px}.igv-clear-filters__track-container:last-of-type{border-bottom:none;margin-bottom:0}.igv-clear-filters__track-header{display:flex;align-items:center;padding:8px 12px;gap:12px;background-color:#f5f5f5;border-radius:4px;margin-bottom:4px;font-weight:500;font-size:14px}.igv-clear-filters__track-name{color:#333;font-weight:600}.igv-clear-filters__filter-checkbox{flex:0 0 16px;width:16px;height:16px;margin-top:2px;cursor:pointer}.igv-clear-filters__checkbox{flex:0 0 16px;width:16px;height:16px;margin-top:2px;cursor:pointer}.igv-clear-filters__content{display:flex;flex-direction:row;flex:1;gap:4px}.igv-clear-filters__description{color:#333;font-size:14px;line-height:1.4}.igv-clear-filters__region{color:#666;font-family:monospace;font-size:12px;line-height:1.4}.igv-clear-filters__button-container{display:flex;gap:8px;margin-top:8px}.igv-clear-filters__button{display:flex;align-items:center;justify-content:center;flex:1;padding:10px;background-color:#5ea4e0;color:#fff;border-color:rgba(0,0,0,0);border-style:solid;border-width:thin;border-radius:2px;cursor:pointer;font-size:14px;font-weight:400;transition:background-color .2s ease}.igv-clear-filters__button:hover{background-color:#3b5c7f}.igv-clear-filters__button--cancel{background-color:#c4c4c4}.igv-clear-filters__button--cancel:hover{background-color:#7f7f7f}.igv-navbar{display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:space-between;align-items:center;box-sizing:border-box;width:100%;color:#444;font-size:12px;font-family:\"Open Sans\",sans-serif;font-weight:400;line-height:32px;padding-left:8px;padding-right:8px;margin-top:2px;margin-bottom:6px;height:32px;border-style:solid;border-radius:3px;border-width:thin;border-color:#bfbfbf;background-color:#f3f3f3}.igv-navbar .igv-navbar-left-container{display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:space-between;align-items:center;height:32px;line-height:32px}.igv-navbar .igv-navbar-left-container .igv-logo{width:34px;height:32px;margin-right:8px}.igv-navbar .igv-navbar-left-container .igv-current-genome{height:32px;margin-left:4px;margin-right:4px;user-select:none;line-height:32px;vertical-align:middle;text-align:center}.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location{display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:space-between;align-items:center;height:100%}.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-chromosome-select-widget-container{display:flex;flex-flow:column;flex-wrap:nowrap;justify-content:space-around;align-items:center;height:100%;width:125px;margin-right:4px}.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-chromosome-select-widget-container select{display:block;cursor:pointer;width:100px;height:75%;outline:none;font-size:12px;font-family:\"Open Sans\",sans-serif;font-weight:400}.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group{display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:space-between;align-items:center;margin-left:8px;height:22px}.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-search-container{display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-start;align-items:center;width:240px;height:22px;line-height:22px}.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-search-container input.igv-search-input{cursor:text;width:85%;height:22px;line-height:22px;font-size:12px;font-family:\"Open Sans\",sans-serif;font-weight:400;text-align:left;padding-left:8px;margin-right:8px;outline:none;border-style:solid;border-radius:3px;border-width:thin;border-color:#bfbfbf;background-color:#fff}.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-search-container .igv-search-icon-container{cursor:pointer;height:16px;width:16px;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:center;align-items:center}.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-windowsize-panel-container{margin-left:4px;user-select:none}.igv-navbar .igv-navbar-right-container{display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:space-between;align-items:center}.igv-navbar .igv-navbar-right-container .igv-navbar-toggle-button-container{position:relative;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-end;align-items:center}.igv-navbar .igv-navbar-right-container .igv-navbar-toggle-button-container-hidden{display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-start;align-items:center;height:100%}.igv-navbar .igv-navbar-right-container .igv-zoom-widget{color:#737373;font-size:18px;margin-left:8px;user-select:none;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-end;align-items:center}.igv-navbar .igv-navbar-right-container .igv-zoom-widget div{cursor:pointer;margin-left:unset;margin-right:unset}.igv-navbar .igv-navbar-right-container .igv-zoom-widget div:first-child{height:20px;width:20px;margin-left:unset;margin-right:4px}.igv-navbar .igv-navbar-right-container .igv-zoom-widget div:last-child{height:20px;width:20px;margin-left:4px;margin-right:unset}.igv-navbar .igv-navbar-right-container .igv-zoom-widget div:nth-child(even){display:block;height:fit-content}.igv-navbar .igv-navbar-right-container .igv-zoom-widget input{display:block;width:125px}.igv-navbar .igv-navbar-right-container .igv-zoom-widget svg{display:block}.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900{color:#737373;font-size:18px;height:32px;line-height:32px;margin-left:8px;user-select:none;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-end;align-items:center}.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div{cursor:pointer;margin-left:unset;margin-right:unset}.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div:first-child{height:20px;width:20px;margin-left:unset;margin-right:4px}.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div:last-child{height:20px;width:20px;margin-left:4px;margin-right:unset}.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div:nth-child(even){width:0;height:0;display:none}.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 input{width:0;height:0;display:none}.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 svg{display:block}.igv-navbar .igv-navbar-right-container .igv-zoom-widget-hidden{display:none}.igv-navbar-button{display:block;box-sizing:unset;padding-left:6px;padding-right:6px;height:18px;text-transform:capitalize;user-select:none;line-height:18px;text-align:center;vertical-align:middle;font-family:\"Open Sans\",sans-serif;font-size:11px;font-weight:200;color:#737373;background-color:#f3f3f3;border-color:#737373;border-style:solid;border-width:thin;border-radius:6px}.igv-navbar-button:hover{cursor:pointer}.igv-navbar-button-clicked{color:#fff;background-color:#737373}.igv-navbar-icon-button{cursor:pointer;position:relative;width:24px;height:24px;margin-left:4px;margin-right:4px;border:none;background-size:contain;background-repeat:no-repeat;background-position:center}.igv-navbar-icon-button>div:first-child{z-index:512;position:absolute;top:36px;left:-18px;width:24px;height:24px;border:none;background-size:contain;background-repeat:no-repeat;background-position:center}.igv-navbar-icon-button>div:last-child{z-index:512;position:absolute;top:36px;left:18px;width:24px;height:24px;border:none;background-size:contain;background-repeat:no-repeat;background-position:center}.igv-navbar-text-button{cursor:pointer;position:relative;margin-left:2px;margin-right:2px;border:none;display:flex;justify-content:center;align-items:center}.igv-navbar-text-button>div:nth-child(2){z-index:512;position:absolute;top:36px;left:0;width:38px;height:18px;border:none;background-size:contain;background-repeat:no-repeat;background-position:center}.igv-navbar-text-button>div:nth-child(3){z-index:512;position:absolute;top:36px;left:42px;width:38px;height:18px;border:none;background-size:contain;background-repeat:no-repeat;background-position:center}#igv-text-button-label{text-anchor:middle;dominant-baseline:middle}.igv-navbar-text-button-svg-inactive rect{stroke:#737373;fill:#fff}.igv-navbar-text-button-svg-inactive text{fill:#737373}.igv-navbar-text-button-svg-inactive tspan{dominant-baseline:middle}.igv-navbar-text-button-svg-hover rect{stroke:#737373;fill:#737373}.igv-navbar-text-button-svg-hover text{fill:#fff}.igv-navbar-text-button-svg-hover tspan{dominant-baseline:middle}#igv-save-svg-group rect{stroke:#737373;fill:#fff}#igv-save-svg-group text{fill:#737373}#igv-save-svg-group:hover rect{stroke:#737373;fill:#737373}#igv-save-svg-group:hover text{fill:#fff}#igv-save-png-group rect{stroke:#737373;fill:#fff}#igv-save-png-group text{fill:#737373}#igv-save-png-group:hover rect{stroke:#737373;fill:#737373}#igv-save-png-group:hover text{fill:#fff}.igv-zoom-in-notice-container{z-index:256;position:absolute;top:8px;left:50%;transform:translate(-50%, 0%);display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:center;align-items:center;background-color:#fff}.igv-zoom-in-notice-container>div{padding-left:4px;padding-right:4px;padding-top:2px;padding-bottom:2px;width:100%;height:100%;font-family:\"Open Sans\",sans-serif;font-size:14px;font-weight:400;color:#3f3f3f}.igv-zoom-in-notice{position:absolute;top:10px;left:50%}.igv-zoom-in-notice div{position:relative;left:-50%;font-family:\"Open Sans\",sans-serif;font-size:medium;font-weight:400;color:#3f3f3f;background-color:hsla(0,0%,100%,.51);z-index:64}.igv-container-spinner{position:absolute;top:90%;left:50%;transform:translate(-50%, -50%);z-index:1024;width:24px;height:24px;pointer-events:none;color:#737373}.igv-multi-locus-close-button{position:absolute;top:2px;right:0;padding-left:2px;padding-right:2px;width:12px;height:12px;color:#666;background-color:#fff;z-index:1000}.igv-multi-locus-close-button>svg{vertical-align:top}.igv-multi-locus-close-button:hover{cursor:pointer;color:#434343}.igv-multi-locus-ruler-label{z-index:64;position:absolute;top:2px;left:0;width:100%;height:12px;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:center;align-items:center}.igv-multi-locus-ruler-label>div{font-family:\"Open Sans\",sans-serif;font-size:12px;color:#101010;background-color:#fff}.igv-multi-locus-ruler-label>div{cursor:pointer}.igv-multi-locus-ruler-label-square-dot{z-index:64;position:absolute;left:50%;top:5%;transform:translate(-50%, 0%);background-color:#fff;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-start;align-items:center}.igv-multi-locus-ruler-label-square-dot>div:first-child{width:14px;height:14px}.igv-multi-locus-ruler-label-square-dot>div:last-child{margin-left:16px;font-family:\"Open Sans\",sans-serif;font-size:14px;font-weight:400;color:#101010}.igv-ruler-sweeper{display:none;pointer-events:none;position:absolute;top:26px;bottom:0;left:0;width:0;z-index:99999;background-color:rgba(68,134,247,.25)}.igv-ruler-tooltip{pointer-events:none;z-index:128;position:absolute;top:0;left:0;width:1px;height:32px;background-color:rgba(0,0,0,0);display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-start;align-items:center}.igv-ruler-tooltip>div{pointer-events:none;width:128px;height:auto;padding:1px;color:#373737;font-size:10px;font-family:\"Open Sans\",sans-serif;font-weight:400;background-color:#fff;border-style:solid;border-width:thin;border-color:#373737}.igv-track-label{position:absolute;left:8px;top:8px;width:auto;height:auto;max-width:50%;padding-left:4px;padding-right:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:\"Open Sans\",sans-serif;font-size:small;font-weight:400;text-align:center;user-select:none;-moz-user-select:none;-webkit-user-select:none;border-color:#444;border-radius:2px;border-style:solid;border-width:thin;background-color:#fff;z-index:128;cursor:pointer}.igv-track-label:hover,.igv-track-label:focus,.igv-track-label:active{background-color:#e8e8e8}.igv-track-label-popup-shim{position:absolute;right:8px;top:8px;width:auto;height:auto;max-width:50%;padding-left:4px;padding-right:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:\"Open Sans\",sans-serif;font-size:small;font-weight:400;text-align:center;user-select:none;-moz-user-select:none;-webkit-user-select:none;border-color:#444;border-radius:2px;border-style:solid;border-width:thin;background-color:#fff;z-index:128}.igv-attribute-group-label{position:absolute;right:5px;z-index:128;width:auto;height:auto;max-width:50%;padding-left:4px;padding-right:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:\"Open Sans\",sans-serif;font-size:small;font-weight:400;text-align:center;user-select:none;-moz-user-select:none;-webkit-user-select:none;border-color:#444;border-radius:2px;border-style:solid;border-width:thin;background-color:#fff}.igv-attribute-group-line{position:absolute;left:0px;width:100%;height:1px;background-color:rgba(0,0,0,0);border-top:1px dotted #666;pointer-events:none;z-index:127}.igv-center-line{display:none;pointer-events:none;position:absolute;top:0;bottom:0;left:50%;transform:translateX(-50%);z-index:8;user-select:none;-moz-user-select:none;-webkit-user-select:none;border-left-style:dashed;border-left-width:thin;border-right-style:dashed;border-right-width:thin}.igv-center-line-wide{background-color:rgba(0,0,0,0);border-left-color:rgba(127,127,127,.51);border-right-color:rgba(127,127,127,.51)}.igv-center-line-thin{background-color:rgba(0,0,0,0);border-left-color:rgba(127,127,127,.51);border-right-color:rgba(0,0,0,0)}.igv-cursor-guide-horizontal{display:none;pointer-events:none;user-select:none;-moz-user-select:none;-webkit-user-select:none;position:absolute;left:0;right:0;top:50%;height:1px;z-index:32;margin-left:50px;margin-right:54px;border-top-style:dotted;border-top-width:thin;border-top-color:rgba(127,127,127,.76)}.igv-cursor-guide-vertical{pointer-events:none;user-select:none;-moz-user-select:none;-webkit-user-select:none;position:absolute;top:0;bottom:0;left:50%;width:1px;z-index:32;border-left-style:dotted;border-left-width:thin;border-left-color:rgba(127,127,127,.76);display:none}.igv-user-feedback{position:fixed;top:50%;left:50%;transform:translate(-50%, -50%);width:512px;height:360px;z-index:2048;background-color:#fff;border-color:#a2a2a2;border-style:solid;border-width:thin;font-family:\"Open Sans\",sans-serif;font-size:medium;font-weight:400;color:#444;display:flex;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start;align-items:center}.igv-user-feedback div:first-child{position:relative;height:24px;width:100%;background-color:#fff;border-bottom-color:#a2a2a2;border-bottom-style:solid;border-bottom-width:thin}.igv-user-feedback div:first-child div{position:absolute;top:2px;width:16px;height:16px;background-color:rgba(0,0,0,0)}.igv-user-feedback div:first-child div:first-child{left:8px}.igv-user-feedback div:first-child div:last-child{cursor:pointer;right:8px}.igv-user-feedback div:last-child{width:100%;height:calc(100% - 24px);border-width:0}.igv-user-feedback div:last-child div{width:auto;height:auto;margin:8px}.igv-generic-dialog-container{position:fixed;top:20%;left:75%;transform:translateX(-50%);width:300px;height:200px;border-color:#7f7f7f;border-radius:4px;border-style:solid;border-width:thin;font-family:\"Open Sans\",sans-serif;font-size:medium;font-weight:400;z-index:2048;background-color:#fff;display:flex;flex-flow:column;flex-wrap:nowrap;justify-content:flex-start;align-items:center}.igv-generic-dialog-container .igv-generic-dialog-header{display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-end;align-items:center;width:100%;height:24px;cursor:move;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-color:#7f7f7f;border-bottom-style:solid;border-bottom-width:thin;background-color:#eee}.igv-generic-dialog-container .igv-generic-dialog-header div{margin-right:4px;margin-bottom:2px;height:12px;width:12px;color:#7f7f7f}.igv-generic-dialog-container .igv-generic-dialog-header div:hover{cursor:pointer;color:#444}.igv-generic-dialog-container .igv-generic-dialog-one-liner{color:#373737;width:95%;height:24px;line-height:24px;text-align:left;margin-top:8px;padding-left:8px;overflow-wrap:break-word;background-color:#fff}.igv-generic-dialog-container .igv-generic-dialog-label-input{margin-top:8px;width:95%;height:24px;color:#373737;line-height:24px;padding-left:8px;background-color:#fff;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-start;align-items:center}.igv-generic-dialog-container .igv-generic-dialog-label-input div{width:30%;height:100%;font-size:16px;text-align:right;padding-right:8px;background-color:#fff}.igv-generic-dialog-container .igv-generic-dialog-label-input input{display:block;height:100%;width:100%;padding-left:4px;font-family:\"Open Sans\",sans-serif;font-weight:400;color:#373737;text-align:left;outline:none;border-style:solid;border-width:thin;border-color:#7f7f7f;background-color:#fff}.igv-generic-dialog-container .igv-generic-dialog-label-input input{width:50%;font-size:16px}.igv-generic-dialog-container .igv-generic-dialog-input{margin-top:8px;width:calc(100% - 16px);height:24px;color:#373737;line-height:24px;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:space-around;align-items:center}.igv-generic-dialog-container .igv-generic-dialog-input input{display:block;height:100%;width:100%;padding-left:4px;font-family:\"Open Sans\",sans-serif;font-weight:400;color:#373737;text-align:left;outline:none;border-style:solid;border-width:thin;border-color:#7f7f7f;background-color:#fff}.igv-generic-dialog-container .igv-generic-dialog-input input{font-size:16px}.igv-generic-dialog-container .igv-generic-dialog-ok-cancel{width:100%;height:28px;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:space-around;align-items:center}.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div{margin-top:32px;color:#fff;font-family:\"Open Sans\",sans-serif;font-size:14px;font-weight:400;width:75px;height:28px;line-height:28px;text-align:center;border-color:rgba(0,0,0,0);border-style:solid;border-width:thin;border-radius:2px}.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:first-child{margin-left:32px;margin-right:0;background-color:#5ea4e0}.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:last-child{margin-left:0;margin-right:32px;background-color:#c4c4c4}.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:first-child:hover{cursor:pointer;background-color:#3b5c7f}.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:last-child:hover{cursor:pointer;background-color:#7f7f7f}.igv-generic-dialog-container .igv-generic-dialog-ok{width:100%;height:36px;margin-top:32px;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:space-around;align-items:center}.igv-generic-dialog-container .igv-generic-dialog-ok div{width:98px;height:36px;line-height:36px;text-align:center;color:#fff;font-family:\"Open Sans\",sans-serif;font-size:medium;font-weight:400;border-color:#fff;border-style:solid;border-width:thin;border-radius:4px;background-color:#2b81af}.igv-generic-dialog-container .igv-generic-dialog-ok div:hover{cursor:pointer;background-color:#25597f}.igv-generic-container{position:fixed;top:20%;left:75%;transform:translateX(-50%);z-index:2048;background-color:#fff;cursor:pointer;display:flex;flex-direction:row;flex-wrap:wrap;justify-content:flex-start;align-items:center}.igv-generic-container>div:nth-child(1){cursor:move;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-end;align-items:center;height:24px;width:100%;background-color:#ddd}.igv-generic-container>div:nth-child(1) i{display:block;color:#5f5f5f;cursor:pointer;width:14px;height:14px;margin-right:8px;margin-bottom:4px}.igv-menu-popup{position:absolute;top:0;left:0;width:max-content;max-width:400px;z-index:2048;cursor:pointer;font-family:\"Open Sans\",sans-serif;font-size:small;font-weight:400;color:#4b4b4b;background:#fff;border-radius:4px;border-color:#7f7f7f;border-style:solid;border-width:thin;display:flex;flex-flow:column;flex-wrap:nowrap;justify-content:flex-start;align-items:flex-end;text-align:left}.igv-menu-popup>div:not(:first-child){width:100%}.igv-menu-popup>div:not(:first-child)>div{background:#fff}.igv-menu-popup>div:not(:first-child)>div.context-menu{padding-left:4px;padding-right:4px}.igv-menu-popup>div:not(:first-child)>div:last-child{border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-bottom-color:rgba(0,0,0,0);border-bottom-style:solid;border-bottom-width:thin}.igv-menu-popup>div:not(:first-child)>div:hover{background:#efefef}.igv-menu-popup-shim{padding-left:8px;padding-right:8px;padding-bottom:1px;padding-top:1px}.igv-menu-popup-header{position:relative;width:100%;height:24px;cursor:move;border-top-color:rgba(0,0,0,0);border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-color:#7f7f7f;border-bottom-style:solid;border-bottom-width:thin;background-color:#eee;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-end;align-items:center}.igv-menu-popup-header div{margin-right:4px;height:12px;width:12px;color:#7f7f7f}.igv-menu-popup-header div:hover{cursor:pointer;color:#444}.igv-menu-popup-check-container{display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-start;align-items:center;width:100%;height:20px;margin-right:4px;background-color:rgba(0,0,0,0)}.igv-menu-popup-check-container div{padding-top:2px;padding-left:8px}.igv-menu-popup-check-container div:first-child{position:relative;width:12px;height:12px}.igv-menu-popup-check-container div:first-child svg{position:absolute;width:12px;height:12px}.igv-loading-spinner-container{z-index:1024;position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);width:32px;height:32px;display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:center;align-items:center}.igv-loading-spinner-container>div{box-sizing:border-box;width:100%;height:100%;border-radius:50%;border:4px solid rgba(128,128,128,.5);border-top-color:#fff;animation:spin 1s ease-in-out infinite;-webkit-animation:spin 1s ease-in-out infinite}@keyframes spin{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes spin{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.igv-roi-menu{position:absolute;z-index:512;font-family:\"Open Sans\",sans-serif;font-size:small;font-weight:400;color:#4b4b4b;background-color:#fff;width:192px;border-radius:4px;border-color:#7f7f7f;border-style:solid;border-width:thin;display:flex;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start;align-items:stretch}.igv-roi-menu>div:first-child{height:24px;border-top-color:rgba(0,0,0,0);border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-color:#7f7f7f;border-bottom-style:solid;border-bottom-width:thin;background-color:#eee;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:flex-end;align-items:center}.igv-roi-menu>div:first-child>div{margin-right:4px;height:12px;width:12px;color:#7f7f7f}.igv-roi-menu>div:first-child>div:hover{cursor:pointer;color:#444}.igv-roi-menu>div:last-child{background-color:#fff;border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-bottom-color:rgba(0,0,0,0);border-bottom-style:solid;border-bottom-width:0;display:flex;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start;align-items:stretch;text-align:start;vertical-align:middle}.igv-roi-menu>div:last-child>div{height:24px;padding-left:4px;border-bottom-style:solid;border-bottom-width:thin;border-bottom-color:#7f7f7f}.igv-roi-menu>div:last-child>div:not(:first-child):hover{cursor:pointer;background-color:rgba(127,127,127,.1)}.igv-roi-menu>div:last-child div:first-child{font-style:italic;text-align:center;padding-right:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.igv-roi-menu>div:last-child>div:last-child{border-bottom-width:0;border-bottom-color:rgba(0,0,0,0)}.igv-roi-placeholder{font-style:normal;color:rgba(75,75,75,.6)}.igv-roi-table{position:absolute;z-index:1024;display:flex;flex-flow:column;flex-wrap:nowrap;justify-content:flex-start;align-items:stretch;resize:both;overflow:hidden;width:min-content;max-width:1600px;border-color:#7f7f7f;border-radius:4px;border-style:solid;border-width:thin;font-family:\"Open Sans\",sans-serif;font-size:12px;font-weight:400;background-color:#fff;cursor:default}.igv-roi-table>div{height:24px;font-size:14px;text-align:start;vertical-align:middle;line-height:24px}.igv-roi-table>div:first-child{border-color:rgba(0,0,0,0);border-top-left-radius:4px;border-top-right-radius:4px;border-top-width:0;border-bottom-color:#7f7f7f;border-bottom-style:solid;border-bottom-width:thin;background-color:#eee;cursor:move;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:space-between;align-items:center}.igv-roi-table>div:first-child>div:first-child{text-align:center;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;margin-left:4px;margin-right:4px;width:calc(100% - 4px - 12px)}.igv-roi-table>div:first-child>div:last-child{margin-right:4px;margin-bottom:2px;height:12px;width:12px;color:#7f7f7f}.igv-roi-table>div:first-child>div:last-child>svg{display:block}.igv-roi-table>div:first-child>div:last-child:hover{cursor:pointer;color:#444}.igv-roi-table>.igv-roi-table-description{padding:4px;margin-left:4px;word-break:break-all;overflow-y:auto;display:flex;flex-flow:row;flex-wrap:nowrap;background-color:rgba(0,0,0,0)}.igv-roi-table>.igv-roi-table-goto-explainer{margin-top:5px;margin-left:4px;color:#7f7f7f;font-style:italic;height:24px;border-top:solid #d3d3d3;background-color:rgba(0,0,0,0)}.igv-roi-table>.igv-roi-table-column-titles{height:24px;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:stretch;align-items:stretch;padding-right:16px;background-color:#fff;border-top-color:#7f7f7f;border-top-style:solid;border-top-width:thin;border-bottom-color:#7f7f7f;border-bottom-style:solid;border-bottom-width:thin}.igv-roi-table>.igv-roi-table-column-titles>div{font-size:14px;vertical-align:middle;line-height:24px;text-align:left;margin-left:4px;height:24px;overflow:hidden;text-overflow:ellipsis;border-right-color:#7f7f7f;border-right-style:solid;border-right-width:thin}.igv-roi-table>.igv-roi-table-column-titles>div:last-child{border-right:unset}.igv-roi-table>.igv-roi-table-row-container{display:flex;flex-flow:column;flex-wrap:nowrap;justify-content:flex-start;align-items:stretch;overflow:auto;height:360px;flex:1 1 auto;background-color:rgba(0,0,0,0)}.igv-roi-table>.igv-roi-table-row-container>.igv-roi-table-row{height:24px;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:stretch;align-items:stretch}.igv-roi-table>.igv-roi-table-row-container>.igv-roi-table-row>div{font-size:14px;vertical-align:middle;line-height:24px;text-align:left;margin-left:4px;height:24px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-right-color:rgba(0,0,0,0);border-right-style:solid;border-right-width:thin}.igv-roi-table>.igv-roi-table-row-container>.igv-roi-table-row>div:last-child{border-right:unset}.igv-roi-table>.igv-roi-table-row-container>.igv-roi-table-row-hover{background-color:rgba(0,0,0,.04)}.igv-roi-table>div:last-child{min-height:32px;height:32px;line-height:32px;border-top-color:#7f7f7f;border-top-style:solid;border-top-width:thin;border-bottom-color:rgba(0,0,0,0);border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-bottom-width:0;background-color:#eee;display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:space-around;align-items:center}.igv-roi-table-row-selected{background-color:rgba(0,0,0,.125)}.igv-roi-table-button{cursor:pointer;height:20px;user-select:none;line-height:20px;text-align:center;vertical-align:middle;font-family:\"Open Sans\",sans-serif;font-size:13px;font-weight:400;color:#000;padding-left:6px;padding-right:6px;background-color:#efefef;border-color:#000;border-style:solid;border-width:thin;border-radius:3px}.igv-roi-table-button:hover{font-weight:400;background-color:rgba(0,0,0,.13)}.igv-roi-region{z-index:64;position:absolute;top:0;bottom:0;pointer-events:none;overflow:visible;display:flex;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start;align-items:stretch}.igv-roi-region>div{position:relative;width:100%;height:8px;pointer-events:auto}.igv-roi-menu-row{height:24px;padding-left:8px;font-size:small;text-align:start;vertical-align:middle;line-height:24px;background-color:#fff}.igv-roi-menu-row-edit-description{width:-webkit-fill-available;font-size:small;text-align:start;vertical-align:middle;background-color:#fff;padding-left:4px;padding-right:4px;padding-bottom:4px;display:flex;flex-direction:column;flex-wrap:nowrap;justify-content:stretch;align-items:stretch}.igv-roi-menu-row-edit-description>label{margin-left:2px;margin-bottom:0;display:block;width:-webkit-fill-available}.igv-roi-menu-row-edit-description>input{display:block;margin-left:2px;margin-right:2px;margin-bottom:1px;width:-webkit-fill-available}.picker_wrapper.no_alpha .picker_alpha{display:none}.picker_wrapper.no_editor .picker_editor{position:absolute;z-index:-1;opacity:0}.picker_wrapper.no_cancel .picker_cancel{display:none}.layout_default.picker_wrapper{display:flex;flex-flow:row wrap;justify-content:space-between;align-items:stretch;font-size:10px;width:25em;padding:.5em}.layout_default.picker_wrapper input,.layout_default.picker_wrapper button{font-size:1rem}.layout_default.picker_wrapper>*{margin:.5em}.layout_default.picker_wrapper::before{content:\"\";display:block;width:100%;height:0;order:1}.layout_default .picker_slider,.layout_default .picker_selector{padding:1em}.layout_default .picker_hue{width:100%}.layout_default .picker_sl{flex:1 1 auto}.layout_default .picker_sl::before{content:\"\";display:block;padding-bottom:100%}.layout_default .picker_editor{order:1;width:6.5rem}.layout_default .picker_editor input{width:100%;height:100%}.layout_default .picker_sample{order:1;flex:1 1 auto}.layout_default .picker_done,.layout_default .picker_cancel{order:1}.picker_wrapper{box-sizing:border-box;background:#f2f2f2;box-shadow:0 0 0 1px silver;cursor:default;font-family:sans-serif;color:#444;pointer-events:auto}.picker_wrapper:focus{outline:none}.picker_wrapper button,.picker_wrapper input{box-sizing:border-box;border:none;box-shadow:0 0 0 1px silver;outline:none}.picker_wrapper button:focus,.picker_wrapper button:active,.picker_wrapper input:focus,.picker_wrapper input:active{box-shadow:0 0 2px 1px #1e90ff}.picker_wrapper button{padding:.4em .6em;cursor:pointer;background-color:#f5f5f5;background-image:linear-gradient(0deg, gainsboro, transparent)}.picker_wrapper button:active{background-image:linear-gradient(0deg, transparent, gainsboro)}.picker_wrapper button:hover{background-color:#fff}.picker_selector{position:absolute;z-index:1;display:block;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);border:2px solid #fff;border-radius:100%;box-shadow:0 0 3px 1px #67b9ff;background:currentColor;cursor:pointer}.picker_slider .picker_selector{border-radius:2px}.picker_hue{position:relative;background-image:linear-gradient(90deg, red, yellow, lime, cyan, blue, magenta, red);box-shadow:0 0 0 1px silver}.picker_sl{position:relative;box-shadow:0 0 0 1px silver;background-image:linear-gradient(180deg, white, rgba(255, 255, 255, 0) 50%),linear-gradient(0deg, black, rgba(0, 0, 0, 0) 50%),linear-gradient(90deg, #808080, rgba(128, 128, 128, 0))}.picker_alpha,.picker_sample{position:relative;background:linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%) 0 0/2em 2em,linear-gradient(45deg, lightgrey 25%, white 25%, white 75%, lightgrey 75%) 1em 1em/2em 2em;box-shadow:0 0 0 1px silver}.picker_alpha .picker_selector,.picker_sample .picker_selector{background:none}.picker_editor input{font-family:monospace;padding:.2em .4em}.picker_sample::before{content:\"\";position:absolute;display:block;width:100%;height:100%;background:currentColor}.picker_arrow{position:absolute;z-index:-1}.picker_wrapper.popup{position:absolute;z-index:2;margin:1.5em}.picker_wrapper.popup,.picker_wrapper.popup .picker_arrow::before,.picker_wrapper.popup .picker_arrow::after{background:#f2f2f2;box-shadow:0 0 10px 1px rgba(0,0,0,.4)}.picker_wrapper.popup .picker_arrow{width:3em;height:3em;margin:0}.picker_wrapper.popup .picker_arrow::before,.picker_wrapper.popup .picker_arrow::after{content:\"\";display:block;position:absolute;top:0;left:0;z-index:-99}.picker_wrapper.popup .picker_arrow::before{width:100%;height:100%;-webkit-transform:skew(45deg);transform:skew(45deg);-webkit-transform-origin:0 100%;transform-origin:0 100%}.picker_wrapper.popup .picker_arrow::after{width:150%;height:150%;box-shadow:none}.popup.popup_top{bottom:100%;left:0}.popup.popup_top .picker_arrow{bottom:0;left:0;-webkit-transform:rotate(-90deg);transform:rotate(-90deg)}.popup.popup_bottom{top:100%;left:0}.popup.popup_bottom .picker_arrow{top:0;left:0;-webkit-transform:rotate(90deg) scale(1, -1);transform:rotate(90deg) scale(1, -1)}.popup.popup_left{top:0;right:100%}.popup.popup_left .picker_arrow{top:0;right:0;-webkit-transform:scale(-1, 1);transform:scale(-1, 1)}.popup.popup_right{top:0;left:100%}.popup.popup_right .picker_arrow{top:0;left:0}.igv-ui-center-fixed{left:50%;top:50%;transform:translate(-50%, -50%)}.igv-container{position:relative;display:flex;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start;align-items:flex-start;padding-top:4px;user-select:none;-webkit-user-select:none;-ms-user-select:none;min-height:160px}.igv-viewport{position:relative;margin-top:5px;line-height:1;overflow-x:hidden;overflow-y:hidden}.igv-viewport-content{position:relative;width:100%}.igv-viewport-content>canvas{position:relative;display:block}.igv-column-container{position:relative;display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:flex-start;align-items:stretch;width:100%}.igv-column-shim{width:1px;margin-left:2px;margin-right:2px;background-color:#545453}.igv-axis-column{position:relative;display:flex;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start;align-items:flex-start;box-sizing:border-box;height:100%;width:50px}.igv-axis-column>div{position:relative;margin-top:5px;width:100%}.igv-axis-column>div>div{z-index:512;position:absolute;top:8px;left:8px;width:fit-content;height:fit-content;background-color:rgba(0,0,0,0);display:grid;align-items:start;justify-items:center}.igv-axis-column>div>div>input{display:block;margin:unset;cursor:pointer}.igv-column{position:relative;position:relative;display:flex;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start;align-items:flex-start;box-sizing:border-box;height:100%}.igv-sample-info-column{position:relative;display:flex;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start;align-items:flex-start;box-sizing:border-box;height:100%}.igv-sample-name-column{position:relative;display:flex;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start;align-items:flex-start;box-sizing:border-box;height:100%}.igv-scrollbar-column{position:relative;display:flex;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start;align-items:flex-start;box-sizing:border-box;height:100%;width:14px}.igv-scrollbar-column>div{position:relative;margin-top:5px;width:14px}.igv-scrollbar-column>div>div{cursor:pointer;position:absolute;top:0;left:2px;width:8px;border-width:1px;border-style:solid;border-color:#c4c4c4;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-left-radius:4px;border-bottom-right-radius:4px}.igv-scrollbar-column>div>div:hover{background-color:#c4c4c4}.igv-track-drag-column{position:relative;display:flex;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start;align-items:flex-start;box-sizing:border-box;height:100%;width:12px;background-color:#fff}.igv-track-drag-column>.igv-track-drag-handle{z-index:512;position:relative;cursor:pointer;margin-top:5px;width:100%;border-style:solid;border-width:0;border-top-right-radius:6px;border-bottom-right-radius:6px}.igv-track-drag-column .igv-track-drag-handle-color{background-color:#c4c4c4}.igv-track-drag-column .igv-track-drag-handle-hover-color{background-color:#787878}.igv-track-drag-column .igv-track-drag-handle-selected-color{background-color:#0963fa}.igv-track-drag-column>.igv-track-drag-shim{position:relative;margin-top:5px;width:100%;border-style:solid;border-width:0}.igv-gear-menu-column{position:relative;display:flex;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start;align-items:flex-start;box-sizing:border-box;height:100%;width:28px}.igv-gear-menu-column>div{display:flex;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start;align-items:center;margin-top:5px;width:100%;background:#fff}.igv-gear-menu-column>div>div{position:relative;margin-top:4px;width:16px;height:16px;color:#7f7f7f}.igv-gear-menu-column>div>div:hover{cursor:pointer;color:#444}.igv-vertical-center{margin:0 !important;top:50% !important;-ms-transform:translateY(-50%) !important;transform:translateY(-50%) !important}';

/**
 * Manages XQTL selections.
 */
class QTLSelections {

    constructor() {
        this.clear();
    }

    clear() {
        this.phenotypeColors = new Map();
        this.snps = new Set();
        this.qtl = null;
    }

    isEmpty() {
        return this.phenotypeColors.size === 0 &&  this.snps.size === 0 && this.qtl === null
    }

    addSnp(snpName) {
        snpName = snpName.toUpperCase();
        this.snps.add(snpName);
    }

    addPhenotype(geneName) {
        geneName = geneName.toUpperCase();
        const genesCount = this.phenotypeColors.size;
        if (!this.phenotypeColors.has(geneName.toUpperCase())) {
            const color = genesCount < brewer.length ? brewer[genesCount] : IGVColor.randomRGB(0, 255);
            this.phenotypeColors.set(geneName.toUpperCase(), color);
        }
    }

    hasSnp(snp) {
        return snp && this.snps.has(snp.toUpperCase())
    }

    hasPhenotype(name) {
        return name && this.phenotypeColors.has(name.toUpperCase())
    }

    hasQTL(qtl) {
        return this.qtls.has(qtl)
    }

    colorForGene(geneName) {
        return geneName ? this.phenotypeColors.get(geneName.toUpperCase()) : "black"
    }

    /**
     * Returns a plain "json like" object, that is an object that is easily converted to json
     * @returns {{}}
     */
    toJSON() {
        const obj = {};
        if (this.phenotypeColors.size > 0) {
            obj.phenotypes = Array.from(this.phenotypeColors.keys());
        }
        if(this.snps.size > 0) {
            obj.snps = Array.from(this.snps);
        }
        if(this.qtl) {
            obj.qtl = this.qtl;
        }
        return obj
    }

    static fromJSON(json) {
        const qtlSelections = new QTLSelections();
        if(json.phenotypes) {
            for(let g of json.phenotypes) {
                qtlSelections.addPhenotype(g);
            }
        }
        if(json.snps) {
            for(let s of json.snps) {
                qtlSelections.addSnp(s);
            }
        }
        if(json.qtl) {
            qtlSelections.qtl = json.qtl;
        }
        return qtlSelections
    }
}

const brewer = [];
// Set +!
brewer.push("rgb(228,26,28)");
brewer.push("rgb(55,126,184)");
brewer.push("rgb(77,175,74)");
brewer.push("rgb(166,86,40)");
brewer.push("rgb(152,78,163)");
brewer.push("rgb(255,127,0)");
brewer.push("rgb(247,129,191)");
brewer.push("rgb(255,255,51)");

// #Set 2
brewer.push("rgb(102,194,165)");
brewer.push("rgb(252,141,98)");
brewer.push("rgb(141, 160, 203)");
brewer.push("rgb(231, 138, 195)");
brewer.push("rgb(166, 216, 84)");
brewer.push("rgb(255, 217, 47)");
brewer.push("rgb(229, 196, 148)");

//#Set 3
brewer.push("rgb( 141, 211, 199)");
brewer.push("rgb(255, 255, 179)");
brewer.push("rgb(190, 186, 218)");
brewer.push("rgb(251, 128, 114)");
brewer.push("rgb(128, 177, 211)");
brewer.push("rgb(253, 180, 98)");
brewer.push("rgb(179, 222, 105)");
brewer.push("rgb(252, 205, 229)");
brewer.push("rgb(188, 128, 189)");
brewer.push("rgb(204, 235, 197)");
brewer.push("rgb(255, 237, 111)");

class CursorGuide {

    constructor(columnContainer, browser) {
        this.browser = browser;
        this.columnContainer = columnContainer;

        this.horizontalGuide = div$2({class: 'igv-cursor-guide-horizontal'});
        columnContainer.appendChild(this.horizontalGuide);

        this.verticalGuide = div$2({class: 'igv-cursor-guide-vertical'});
        columnContainer.appendChild(this.verticalGuide);

        this.addMouseHandler(browser);

        this.setVisibility(browser.config.showCursorGuide);

    }

    addMouseHandler(browser) {

        this.boundMouseMoveHandler = mouseMoveHandler.bind(this);
        this.columnContainer.addEventListener('mousemove', this.boundMouseMoveHandler);

        function mouseMoveHandler(event) {

            event.target.tagName;

            const {x, y} = translateMouseCoordinates(event, this.columnContainer);
            this.horizontalGuide.style.top = `${y}px`;

            if ('CANVAS' === event.target.tagName) {

                const viewport = findAncestorOfClass(event.target, 'igv-viewport');

                if (viewport && browser.getRulerTrackView()) {

                    this.verticalGuide.style.left = `${x}px`;

                    const columns = browser.root.querySelectorAll('.igv-column');
                    let index = undefined;
                    const viewportParent = viewport.parentElement;
                    for (let i = 0; i < columns.length; i++) {
                        if (undefined === index && viewportParent === columns[i]) {
                            index = i;
                        }
                    }

                    if (!(undefined === index)) {

                        const rulerViewport = browser.getRulerTrackView().viewports[index];
                        const result = rulerViewport.mouseMove(event);

                        if (result) {

                            const {start, bp, end} = result;
                            const interpolant = (bp - start) / (end - start);

                            if (this.customMouseHandler) {
                                this.customMouseHandler({start, bp, end, interpolant});
                            }
                        } // if (result)

                    } // if (index)

                } // if (viewport && browser.getRulerTrackView())

            } // if ('CANVAS' === event.target.tagName) {

        }
    }

    removeMouseHandler() {
        this.columnContainer.removeEventListener('mousemove', this.boundMouseMoveHandler);
    }

    setVisibility(showCursorGuide) {
        if (true === showCursorGuide) {
            this.show();
        } else {
            this.hide();
        }
    }

    show() {
        this.verticalGuide.style.display = 'block';
        this.horizontalGuide.style.display = 'block';

    }

    hide() {

        this.verticalGuide.style.display = 'none';
        this.horizontalGuide.style.display = 'none';

        if (this.browser.getRulerTrackView()) {
            for (let viewport of this.browser.getRulerTrackView().viewports) {
                viewport.tooltip.style.display = 'none';
            }
        }

    }

}

/**
 * Walk up the tree until a parent is found with the given classname.  If no ancestor is found return undefined.
 * @param target
 * @param classname
 * @returns {*}
 */
function findAncestorOfClass(target, classname) {

    while (target.parentElement) {
        if (target.parentElement.classList.contains(classname)) {
            return target.parentElement
        } else {
            target = target.parentElement;
        }
    }
    return undefined

}

class SliderDialog {

    constructor(parent) {

        this.parent = parent;

        // dialog container
        this.container = div$2({class: 'igv-ui-generic-dialog-container'});
        parent.appendChild(this.container);

        // dialog header
        const header = div$2({class: 'igv-ui-generic-dialog-header'});
        this.container.appendChild(header);

        // dialog label
        this.label = div$2({class: 'igv-ui-generic-dialog-one-liner'});
        this.container.appendChild(this.label);
        this.label.textContent = 'Unlabeled';

        // input container
        this.input_container = div$2({class: 'igv-ui-generic-dialog-input'});
        this.container.appendChild(this.input_container);

        // input element
        let html = `<input type="range" id="igv-slider-dialog-input" name="igv-slider-dialog-input" />`;
        this._input = document.createRange().createContextualFragment(html).firstChild;
        this.input_container.appendChild(this._input);

        // output element
        html = `<output id="igv-slider-dialog-output" name="igv-slider-dialog-output" for="igv-slider-dialog-input"></output>`;
        this._output = document.createRange().createContextualFragment(html).firstChild;
        this.input_container.appendChild(this._output);


        // ok | cancel
        const buttons = div$2({class: 'igv-ui-generic-dialog-ok-cancel'});
        this.container.appendChild(buttons);

        // ok
        this.ok = div$2();
        buttons.appendChild(this.ok);
        this.ok.textContent = 'OK';

        // cancel
        this.cancel = div$2();
        buttons.appendChild(this.cancel);
        this.cancel.textContent = 'Cancel';

        hide$1(this.container);

        this._input.addEventListener('input', () => {
            const number = parseFloat(this._input.value)/this._scaleFactor;
            this.callback(number);
            this._output.value = `${number.toFixed(this._precision)}`;
        }, false);

        this.ok.addEventListener('click', () => {
            if (typeof this.callback === 'function') {
                const number = parseFloat(this._input.value)/this._scaleFactor;
                this.callback(number);
                this.callback = undefined;
            }
            this._input.value = undefined;
            hide$1(this.container);
        });

        const cancel = () => {
            this._input.value = undefined;
            hide$1(this.container);
        };

        this.cancel.addEventListener('click', cancel);

        attachDialogCloseHandlerWithParent$1(header, cancel);
        makeDraggable$1(this.container, header);

    }

    get value() {
        return purify.sanitize(this._input.value)
    }

    present(options, e) {

        this.label.textContent = options.label;

        this._scaleFactor = options.scaleFactor;
        this._precision = options.precision || 2; // added precision option with default value of 2
        const [ minS, maxS, valueS ] = [ options.min, options.max, options.value ].map(number => (Math.floor(this._scaleFactor * number)).toString());

        this._input.min = minS;
        this._input.max = maxS;
        this._input.value = valueS;

        const numer = parseFloat(valueS);
        const denom = this._scaleFactor;
        const number = numer/denom;
        this._output.value = `${number.toFixed(this._precision)}`;

        this.callback = options.callback || options.click;

        show$1(this.container);
        this.clampLocation(e.clientX, e.clientY);

    }

    clampLocation(clientX, clientY) {

        const {width: w, height: h} = this.container.getBoundingClientRect();
        const wh = window.innerHeight;
        const ww = window.innerWidth;

        const y = Math.min(wh - h, clientY);
        const x = Math.min(ww - w, clientX);
        this.container.style.left = `${x}px`;
        this.container.style.top = `${y}px`;

    }
}

// css - $igv-scrollbar-outer-width: 14px;
const igv_scrollbar_outer_width = 14;

// css - $igv-track-drag-column-width: 12px;
const igv_track_manipulation_handle_width = 12;

// css - $igv-track-gear-menu-column-width: 28px;
const igv_track_gear_menu_column_width = 28;

// $igv-column-shim-width: 1px;
// $igv-column-shim-margin: 2px;
const column_multi_locus_shim_width = 2 + 1 + 2;

class Browser {

    qtlSelections = new QTLSelections()

    constructor(config, parentDiv) {

        this.config = config;
        this.guid = guid$2();
        this.namespace = '.browser_' + this.guid;

        this.parent = parentDiv;

        let shadowRoot = parentDiv.shadowRoot;
        if (!shadowRoot) {
            // Create the shadow root and attach the IGV CSS stylesheet.
            shadowRoot = parentDiv.attachShadow({mode: "open"});
            const sheet = new CSSStyleSheet();
            sheet.replaceSync(igvCss);
            shadowRoot.adoptedStyleSheets = [sheet];
        }

        this.root = div$2({class: 'igv-container'});
        shadowRoot.appendChild(this.root);

        this.alert = new Alert(this.root);

        this.columnContainer = div$2({class: 'igv-column-container'});
        this.root.appendChild(this.columnContainer);

        this.menuPopup = new MenuPopup(this.columnContainer);

        this.menuUtils = new MenuUtils(this);

        this.initialize(config);

        this.trackViews = [];

        this.constants = {
            dragThreshold: 3,
            scrollThreshold: 5,
            defaultColor: "rgb(0,0,150)",
            doubleClickDelay: config.doubleClickDelay || 500
        };

        // Map of event name -> [ handlerFn, ... ]
        this.eventHandlers = {};

        if (config.listeners) {
            for (let evt of Object.keys(config.listeners)) {
                this.on(evt, config.listeners[evt]);
            }
        }

        this.on('trackremoved', () => {

            const found = this.findTracks(track => typeof track.getSamples === 'function');

            if (0 === found.length) {

                // sample info
                this.sampleInfoControl.setButtonVisibility(false);

                // sample names
                this.sampleNameViewportWidth = undefined;
                this.showSampleNames = false;
                this.sampleNameControl.setState(this.showSampleNames);
                this.sampleNameControl.hide();

                this.layoutChange();
            }
        });

        this.on('didchangecolumnlayout', () => {
            if (trackViewportPopoverList.length > 0) {
                const len = trackViewportPopoverList.length;
                for (let i = 0; i < len; i++) {
                    trackViewportPopoverList[i].dispose();
                }
                trackViewportPopoverList.length = 0;
            }
        });

        this.addEventHandlers();

        this.sampleInfo = new SampleInfo(this);

        this.createStandardControls(config);

        // Region of interest
        this.roiManager = new ROIManager(this);

        // previous track colors for colorPicker
        this.previousTrackColors = [];

    }

    get doShowROITable() {
        return this.roiManager.roiTableIsVisible()
    }

    initialize(config) {

        this.flanking = config.flanking;
        this.crossDomainProxy = config.crossDomainProxy;
        this.formats = config.formats;
        this.trackDefaults = config.trackDefaults;
        this.nucleotideColors = config.nucleotideColors || defaultNucleotideColors;
        for (let key of Object.keys(this.nucleotideColors)) {
            this.nucleotideColors[key.toLowerCase()] = this.nucleotideColors[key];
        }

        this.doShowTrackLabels = config.showTrackLabels;

        this.doShowCenterLine = config.showCenterGuide;

        this.doShowCursorGuide = config.showCursorGuide;

        this.showSampleNames = config.showSampleNames;

        this.sampleNameViewportWidth = undefined;

        if (config.sampleNameViewportWidth) {
            this.sampleNameViewportWidth = config.sampleNameViewportWidth;
        }

        if (config.search) {
            this.searchConfig = {
                type: "json",
                url: config.search.url,
                coords: config.search.coords === undefined ? 1 : config.search.coords,
                chromosomeField: config.search.chromosomeField || "chromosome",
                startField: config.search.startField || "start",
                endField: config.search.endField || "end",
                geneField: config.search.geneField || "gene",
                snpField: config.search.snpField || "snp",
                resultsField: config.search.resultsField
            };
        }
    }

    createStandardControls(config) {

        this.setTrackLabelVisibility(config.showTrackLabels);

        this.navbar = new ResponsiveNavbar(config, this);

        this.columnContainer.parentNode.insertBefore(this.navbar.navigation, this.columnContainer);

        if (false === config.showControls) {
            this.navbar.hide();
        }
        this.cursorGuide = new CursorGuide(this.columnContainer, this);

        this.inputDialog = new InputDialog(this.root);
        this.inputDialog.container.id = `igv-input-dialog-${guid$2()}`;

        this.dataRangeDialog = new DataRangeDialog(this, this.root);
        this.dataRangeDialog.container.id = `igv-data-range-dialog-${guid$2()}`;

        this.genericColorPicker = new GenericColorPicker({parent: this.root, width: 180});
        this.genericColorPicker.container.id = `igv-track-color-picker-${guid$2()}`;

        this.sliderDialog = new SliderDialog(this.root);
        this.sliderDialog.container.id = `igv-slider-dialog-${guid$2()}`;

    }

    getSampleNameViewportWidth() {

        if (false === this.showSampleNames || undefined === this.sampleNameViewportWidth) {
            return 0
        } else {
            return this.sampleNameViewportWidth
        }

    }

    getSampleInfoViewportWidth() {
        return this.getSampleInfoColumnWidth()
    }

    isMultiLocusMode() {
        return this.referenceFrameList && this.referenceFrameList.length > 1
    };

    isMultiLocusWholeGenomeView() {

        if (undefined === this.referenceFrameList || 1 === this.referenceFrameList.length) {
            return false
        }

        for (let referenceFrame of this.referenceFrameList) {
            if ('all' === referenceFrame.chr.toLowerCase()) {
                return true
            }
        }

        return false
    };

    /**
     * PUBLIC API FUNCTION
     *
     * Return the current genomic region as a locus string, or array of locus strings if in multi-locus view
     * @returns {string|*[]|*}
     */
    currentLoci() {
        const noCommaLocusString = (rf) => `${rf.chr}:${rf.start + 1}-${rf.end}`;
        if (undefined === this.referenceFrameList || 0 === this.referenceFrameList.length) {
            return ""
        } else if (1 === this.referenceFrameList.length) {
            return noCommaLocusString(this.referenceFrameList[0])
        } else {
            return this.referenceFrameList.map(rf => noCommaLocusString(rf))
        }
    }

    /**
     * Render browse display as SVG
     * @returns {string}
     */
    toSVG() {

        const {x, y, width, height} = this.columnContainer.getBoundingClientRect();

        const h_render = Number.MAX_SAFE_INTEGER;      // <= DO NOT USE 'height' here

        const config =
            {
                width,
                height: h_render,
                backdropColor: 'white',
                multiLocusGap: 0,
                viewbox:
                    {
                        x: 0,
                        y: 0,
                        width,
                        height: h_render
                    }
            };

        const context = new ctx(config);

        // tracks -> SVG
        const delta = {deltaX: 0, deltaY: -y};
        for (let trackView of this.trackViews) {
            trackView.renderSVGContext(context, delta);
        }

        // ROI -> SVG
        delta.deltaX = x;

        // reset height to trim away unneeded svg canvas real estate. Yes, a bit of a hack.
        context.setHeight(height);

        return context.getSerializedSvg(true)
    }

    saveSVGtoFile(filename, container) {

        let svgString = this.toSVG();

        // Append svg t testing, not used in production
        if (container) {
            const svg = document.createElement("svg");
            svg.innerHTML = svgString;
            container.appendChild(svg);
        }

        const path = filename || 'igvjs.svg';
        const data = URL.createObjectURL(new Blob([svgString], {type: "application/octet-stream"}));
        download$1(path, data);
        URL.revokeObjectURL(data);  // Important to prevent memory leak
    }

    savePNGtoFile(filename) {

        const svgAsString = this.toSVG();

        const svgBlob = new Blob([svgAsString], {
            type: 'image/svg+xml'
        });
        const svgObjectUrl = URL.createObjectURL(svgBlob);

        const img = document.createElement('img');

        const onImageLoaded = () => {

            const dimensions = this.columnContainer.getBoundingClientRect();
            const devicePixelRatio = window.devicePixelRatio;
            const w = dimensions.width * devicePixelRatio;
            const h = dimensions.height * devicePixelRatio;
            const canvas = document.createElement('canvas');
            canvas.width = w;
            canvas.height = h;
            const context = canvas.getContext('2d');
            context.scale(devicePixelRatio, devicePixelRatio);

            context.drawImage(img, 0, 0);
            const data = canvas.toDataURL("image/png");
            filename = filename || 'igvjs.png';
            download$1(filename, data);

            // Free temporary object URL
            URL.revokeObjectURL(svgObjectUrl);
        };
        img.addEventListener('load', onImageLoaded);
        img.src = svgObjectUrl;
    }


    /**
     * Initialize a session from an object, json, or by loading from a file.
     *
     * @param options
     * @returns {*}
     */
    async loadSession(options) {

        this.sampleInfo.initialize();

        // TODO: deprecated
        this.roiSets = [];

        let session;
        if (options.url || options.file) {
            session = await Browser.loadSessionFile(options);
            // if (options.parentApp``) {
            //     session.parentApp = options.parentApp
            // }
        } else {
            session = options;
        }

        await this.loadSessionObject(session);
    }

    /**
     * Load and parse a session uri or file, and return a session object.  Handles data uris as well as urls.
     *
     * @param options
     * @returns {Promise<*|XMLSession>}
     */
    static async loadSessionFile(options) {

        const urlOrFile = options.url || options.file;

        let config;
        if (options.url && isString$3(options.url) && (options.url.startsWith("blob:") || options.url.startsWith("data:"))) {
            const json = Browser.uncompressSession(options.url);
            config = JSON.parse(json);
        } else {
            let filename = options.filename;
            if (!filename) {
                filename = (options.url ? await getFilename$1(options.url) : options.file.name);
            }

            if (filename.endsWith(".xml")) {
                const knownGenomes = GenomeUtils.KNOWN_GENOMES;
                const string = await igvxhr$1.loadString(urlOrFile);
                config = new XMLSession(string, knownGenomes);

            } else if (filename.endsWith("hub.txt")) {
                const hub = await loadHub(urlOrFile);
                const genomeConfig = hub.getGenomeConfig();
                config = {
                    reference: genomeConfig
                };
            } else if (filename.endsWith(".json")) {
                config = await igvxhr$1.loadJson(urlOrFile);
            } else {
                throw Error("Unrecognized session file format:" + filename)
            }
        }
        return setDefaults(config)

    }

    /**
     * Note:  public API function
     * @param session
     * @returns {Promise<void>}
     */
    async loadSessionObject(session) {

        // prepare to load a new session, discarding DOM and state
        this.cleanHouseForSession();
        this.config = session;

        // Check for juicebox session
        if (session.browsers) {
            session = await translateSession(session);
        }

        this.navbar.sampleInfoControl.setButtonVisibility(false);

        this.showSampleNames = session.showSampleNames || false;
        this.navbar.sampleNameControl.setState(this.showSampleNames === true);

        if (session.sampleNameViewportWidth) {
            this.sampleNameViewportWidth = session.sampleNameViewportWidth;
        }

        // axis column
        createColumn(this.columnContainer, 'igv-axis-column');

        // sample info column
        createColumn(this.columnContainer, 'igv-sample-info-column');

        // SampleName column
        createColumn(this.columnContainer, 'igv-sample-name-column');

        // Track scrollbar column
        createColumn(this.columnContainer, 'igv-scrollbar-column');

        // Track drag/reorder column
        createColumn(this.columnContainer, 'igv-track-drag-column');

        // Track gear column
        createColumn(this.columnContainer, 'igv-gear-menu-column');

        const genomeOrReference = session.reference || session.genome;
        if (!genomeOrReference) {
            console.warn("No genome or reference object specified");
            return
        }

        const genomeConfig = isString$3(genomeOrReference) ?
            await GenomeUtils.expandReference(this.alert, genomeOrReference) :
            genomeOrReference;

        const genome = await this.loadReference(genomeConfig, genomeConfig.locus || session.locus);

        this.centerLineList = this.createCenterLineList(this.columnContainer);

        // Create ideogram and ruler track.  Really this belongs in browser initialization, but creation is
        // deferred because ideogram and ruler are treated as "tracks", and tracks require a reference frame
        if (false !== session.showIdeogram) {
            const track = new IdeogramTrack(this);
            const trackView = new TrackView(this, this.columnContainer, track);
            this.trackViews.push(trackView);
        }

        if (false !== session.showRuler) {
            const track = new RulerTrack(this);
            const trackView = new TrackView(this, this.columnContainer, track);
            this.trackViews.push(trackView);
        }

        if (session.qtlSelections) {
            this.qtlSelections = QTLSelections.fromJSON(session.qtlSelections);
        }

        // ROIs
        if (session.showROIOverlays !== undefined) {
            this.roiManager.showOverlays = session.showROIOverlays;
        }
        this.roiManager.clearROIs();
        if (session.roi) {
            this.roiManager.loadROI(session.roi, genome);
        } else {
            // Reset is called by loadROI, if no ROIs are loaded we need to call it explicitly
            await this.roiManager.reset();
        }

        // Sample info
        const localSampleInfoFiles = [];
        if (session.sampleinfo) {
            const sampleInfoArray = Array.isArray(session.sampleinfo) ? session.sampleinfo : [session.sampleinfo];
            for (const sampleInfoConfig of sampleInfoArray) {
                // The "file" property is recorded in the session when a local file is referenced. It can't be used
                // on reloading, its only purpose is to present an alert to the user.  This could also be used
                // to prompt the user to load the file manually, but we don't currently do that.
                if (sampleInfoConfig.file) {
                    localSampleInfoFiles.push(sampleInfoConfig.file);
                } else {
                    await this.sampleInfo.loadSampleInfo(sampleInfoConfig);
                }
            }
        }

        // Tracks.  Start with genome tracks, if any, then append session tracks
        const genomeTracks = genomeConfig.tracks || [];
        const trackConfigurations = session.tracks ? genomeTracks.concat(session.tracks) : genomeTracks;

        // Ensure that we always have a sequence track with no explicit URL (=> the reference genome sequence track)
        const pushSequenceTrack = trackConfigurations.filter(track => 'sequence' === track.type && !track.url && !track.fastaURL).length === 0;
        if (pushSequenceTrack /*&& false !== this.config.showSequence*/) {
            trackConfigurations.push({type: "sequence", order: defaultSequenceTrackOrder, removable: false});
        }

        const localTrackFileNames = trackConfigurations.filter((config) => undefined !== config.file).map(({file}) => file);

        const localIndexFileNames = trackConfigurations.filter((config) => undefined !== config.indexFile).map(({indexFile}) => indexFile);
        if (localIndexFileNames.length > 0) {
            localTrackFileNames.push(...localIndexFileNames);
        }

        if (localSampleInfoFiles.length > 0) {
            localTrackFileNames.push(...localSampleInfoFiles);
        }

        if (localTrackFileNames.length > 0) {
            alert(`Local files cannot be loaded automatically.\nThis session contains references to these local files:\n${localTrackFileNames.map(str => `    ${str}`).join('\n')}`);
        }

        const nonLocalTrackConfigurations = trackConfigurations.filter((config) => undefined === config.file);

        // Maintain track order unless explicitly set
        let trackOrder = 1;
        for (let t of nonLocalTrackConfigurations) {
            if (undefined === t.order) {
                t.order = trackOrder++;
            }
        }

        // Load a hidden track -- used to populate searchable database without creating a track
        const configHidden = nonLocalTrackConfigurations.filter(config => true === config.hidden);
        for (const config of configHidden) {
            const featureSource = FeatureSource(config, this.genome);
            await featureSource.getFeatures({chr: "1", start: 0, end: Number.MAX_SAFE_INTEGER});
        }

        await this.loadTrackList(nonLocalTrackConfigurations);

    }

    cleanHouseForSession() {

        for (let trackView of this.trackViews) {
            // empty axis column, viewport columns, sampleName column, scroll column, drag column, gear column
            trackView.removeDOMFromColumnContainer();
        }

        // discard all columns   TODO - why do we do this?
        const elements = this.columnContainer.querySelectorAll('.igv-axis-column, .igv-column-shim, .igv-column, .igv-sample-info-column, .igv-sample-name-column, .igv-scrollbar-column, .igv-track-drag-column, .igv-gear-menu-column');
        elements.forEach(column => column.remove());

        this.trackViews = [];

        if (this.circularView) {
            this.circularView.clearChords();
        }

    }

    /**
     * Load a reference genome object.  This includes the sequence, and optional cytoband, but no tracks.  This method
     * is used by loadGenome and loadSession.
     *
     * @param genomeConfig
     * @param initialLocus
     */
    async loadReference(genomeConfig, initialLocus) {

        this.removeAllTracks();   // Do this first, before new genome is set
        this.roiManager.clearROIs();

        this.navbar.setEnableTrackSelection(false);

        let genome;
        if (genomeConfig.gbkURL) {
            genome = await loadGenbank(genomeConfig.gbkURL);
        } else {
            genome = await Genome.createGenome(genomeConfig, this);
        }

        const genomeChange = undefined === this.genome || (this.genome.id !== genome.id);

        this.genome = genome;

        this.navbar.updateGenome(genome);

        let locus = initialLocus || genome.initialLocus;
        if (Array.isArray(locus)) {
            locus = locus.join(' ');
        }

        const locusFound = await this.search(locus, true);
        if (!locusFound) {
            console.error(`Cannot set initial locus ${locus}`);
            if (locus !== genome.initialLocus) {
                await this.search(genome.initialLocus);
            }
        }

        if (genomeChange) {

            this.fireEvent('genomechange', [{genome}]);

            if (this.circularView) {
                this.circularView.setAssembly({
                    name: this.genome.id,
                    id: this.genome.id,
                    chromosomes: makeCircViewChromosomes(this.genome)
                });
            }
        }
        return genome
    }

    /**
     * Load a genome, defined by a string ID or a json-like configuration object. This includes a fasta reference
     * as well as optional cytoband and annotation tracks.
     *
     * @param idOrConfig
     * @returns genome
     */
    async loadGenome(idOrConfig) {

        if (idOrConfig.genarkAccession) {
            idOrConfig.url = convertToHubURL(idOrConfig.genarkAccession);
        }

        // Translate the generic "url" field, used by clients such as igv-webapp
        if (idOrConfig.url) {
            if (isString$3(idOrConfig.url) && idOrConfig.url.endsWith("/hub.txt")) {
                idOrConfig.hubURL = idOrConfig.url;
                delete idOrConfig.url;
            } else if ("gbk" === getFileExtension(idOrConfig.url)) {
                idOrConfig.gbkURL = idOrConfig.url;
                delete idOrConfig.url;
            }
        }

        let genomeConfig;
        const isHubGenome = idOrConfig.hubURL || (idOrConfig.url && isString$3(idOrConfig.url) && idOrConfig.url.endsWith("/hub.txt"));
        if (isHubGenome) {
            const hub = await loadHub(idOrConfig.hubURL || idOrConfig.url);
            genomeConfig = hub.getGenomeConfig();
        } else if (isString$3(idOrConfig) || !(idOrConfig.url || idOrConfig.fastaURL || idOrConfig.twoBitURL || idOrConfig.gbkURL)) {
            // Either an ID, a json string, or an object missing required properties.
            genomeConfig = await GenomeUtils.expandReference(this.alert, idOrConfig);
        } else {
            genomeConfig = idOrConfig;
        }

        await this.loadReference(genomeConfig);

        let tracks;
        if (genomeConfig.gbkURL || "gbk" === genomeConfig.format) {
            tracks = [{
                name: "Annotations",
                format: "gbk",
                url: genomeConfig.gbkURL
            }];
        } else {
            tracks = genomeConfig.tracks || [];
        }

        // Insure that we always have a sequence track
        const pushSequenceTrack = tracks.filter(track => track.type === 'sequence').length === 0;
        if (pushSequenceTrack) {
            tracks.push({type: "sequence", order: defaultSequenceTrackOrder});
        }

        await this.loadTrackList(tracks);

        return this.genome
    }

    /**
     * Called after a session load, search, pan (horizontal drag), or resize
     *
     * @param referenceFrameList
     */
    updateUIWithReferenceFrameList() {

        const referenceFrameList = this.referenceFrameList;

        (this.isMultiLocusWholeGenomeView() || GenomeUtils.isWholeGenomeView(referenceFrameList[0].chr));

        this.navbar.navbarDidResize();

        toggleTrackLabels(this.trackViews, this.doShowTrackLabels);

        if (this.doShowCenterLine && GenomeUtils.isWholeGenomeView(referenceFrameList[0].chr)) {
            this.navbar.centerLineButton.boundMouseClickHandler();
        }

        if (this.doShowCursorGuide && GenomeUtils.isWholeGenomeView(referenceFrameList[0].chr)) {
            this.navbar.cursorGuideButton.boundMouseClickHandler();
        }

        this.setCenterLineAndCenterLineButtonVisibility(GenomeUtils.isWholeGenomeView(referenceFrameList[0].chr));

    }

    setCenterLineAndCenterLineButtonVisibility(isWholeGenomeView) {

        if (isWholeGenomeView) {
            this.navbar.centerLineButton.setVisibility(false);
        } else {
            this.navbar.centerLineButton.setVisibility(this.config.showCenterGuideButton);
        }

        for (let centerLine of this.centerLineList) {
            if (isWholeGenomeView) {
                this.setCenterLineVisibility(!isWholeGenomeView);
            } else {
                this.setCenterLineVisibility(this.doShowCenterLine);
            }
        }

    }

    setTrackLabelVisibility(isVisible) {
        toggleTrackLabels(this.trackViews, isVisible);
    }

    setROITableVisibility(isVisible) {
        true === isVisible ? this.roiManager.presentTable() : this.roiManager.dismissTable();
    }

    // cursor guide
    setCursorGuideVisibility(doShowCursorGuide) {

        if (doShowCursorGuide) {
            this.cursorGuide.show();
        } else {
            this.cursorGuide.hide();
        }
    }

    setCustomCursorGuideMouseHandler(mouseHandler) {
        this.cursorGuide.customMouseHandler = mouseHandler;
    }

    // center line
    setCenterLineVisibility(doShowCenterLine) {
        for (let centerLine of this.centerLineList) {
            if (true === doShowCenterLine) {
                centerLine.show();
                centerLine.repaint();
            } else {
                centerLine.hide();
            }
        }
    }

    /**
     * Public API function. Load a list of tracks.
     *
     * @param configList  Array of track configurations
     * @returns {Promise<*>}  Promise for track objects
     */
    async loadTrackList(configList) {

        // Impose an order if not specified
        let order = this.trackViews.length + 1;
        for (let c of configList) {
            if (c.order === undefined) {
                c.order = order++;
            }
        }

        const promises = [];
        for (const config of configList) {
            promises.push(this.#loadTrackHelper(config));
        }

        const loadedTracks = await Promise.all(promises);

        // If any tracks are selected show the selection buttons
        if (this.trackViews.some(({track}) => track.selected)) {
            this.navbar.setEnableTrackSelection(true);
        }

        this.reorderTracks();

        await resize.call(this);

        this.fireEvent('trackorderchanged', [this.getTrackOrder()]);

        return loadedTracks
    }

    /**
     * Public API function
     *
     * Load an individual track.  If part of an autoscale group force a general update
     *
     * @param config  A track configuration
     * @returns {Promise<*>}  Promise for track object
     */
    async loadTrack(config) {

        const loadedTracks = await this.loadTrackList([config]);
        if (config.autoscaleGroup) {
            this.updateViews();
        }
        return loadedTracks[0]
    }

    async #loadTrackHelper(config) {

        // config might be json
        if (isString$3(config)) {
            config = JSON.parse(config);
        }

        let track;
        try {
            track = await this.createTrack(config);
        } catch (error) {

            let msg = error.message || error.error || error.toString();

            const httpMessages =
                {
                    "401": "Access unauthorized",
                    "403": "Access forbidden",
                    "404": "Not found"
                };

            if (httpMessages.hasOwnProperty(msg)) {
                msg = httpMessages[msg];
            }

            msg = `${msg} : ${isFile$1(config.url) ? config.url.name : config.url}`;
            const err = new Error(msg);
            console.error(err);
            throw err
        }

        if (track) {
            return await this.addTrack(track)
        } else {
            return undefined
        }

    }

    async addTrack(track) {

        // Set order field of track here, otherwise track order might get shuffled during asynchronous load
        if (undefined === track.order) {
            track.order = this.trackViews.length;
        }

        const trackView = new TrackView(this, this.columnContainer, track);
        this.trackViews.push(trackView);
        toggleTrackLabels(this.trackViews, this.doShowTrackLabels);

        if (typeof track.postInit === 'function') {

            try {
                trackView.startSpinner();
                await track.postInit();
            } finally {
                trackView.stopSpinner();
            }
        }

        if (typeof track.hasSamples === 'function' && track.hasSamples()) {

            if (this.sampleInfo.hasAttributes()) {
                this.sampleInfoControl.setButtonVisibility(true);
            }

            if (this.config.showSampleNameButton !== false) {
                this.sampleNameControl.show();
            }
        }

        track.trackView.enableTrackSelection(this.navbar.getEnableTrackSelection());

        return track

    }

    /**
     * Public API function - load a region of interest
     *
     * @param config  A "track" configuration object, or array of objects,  of type == "annotation" (bed, gff, etc)
     *
     * @returns {Promise<void>}
     */
    async loadROI(config) {
        return this.roiManager.loadROI(config, this.genome)
    }

    /**
     * Public API function - clear all regions of interest (ROI), including preloaded and user-defined ROIs
     */
    clearROIs() {
        this.roiManager.clearROIs();
    }

    /**
     * Public API function. Return a promise for the list of user-defined regions-of-interest
     */
    async getUserDefinedROIs() {

        if (this.roiManager) {

            const set = this.roiManager.getUserDefinedROISet();
            if (undefined === set) {
                return []
            }

            const featureHash = await set.getAllFeatures();
            const featureList = [];
            for (let value of Object.values(featureHash)) {
                featureList.push(...value);
            }

            return featureList

        } else {
            return []
        }
    }

    getRulerTrackView() {
        const list = this.trackViews.filter(({track}) => 'ruler' === track.id);
        return list.length > 0 ? list[0] : undefined
    }

    /**
     * Create a Track object.
     * @param config
     * @returns {Promise<*>}
     */
    async createTrack(config) {

        // Resolve function and promise urls
        let url = await resolveURL(config.url || config.fastaURL);
        if (isString$3(url)) {
            url = url.trim();
        }

        if (url) {
            if (config.format) {
                config.format = config.format.toLowerCase();
            } else if (config.fastaURL) {
                config.format = "fasta";  // by definition
            } else if (!config.sourceType) {
                // If not a webservice, see if we can infer a format from the URL
                const format = await inferFileFormat(config);
                if (format) {
                    config.format = format;
                }
            } else if (config.sourceType === "htsget") {
                // Finally check for htsget URL.  This is a longshot
                await HtsgetReader.inferFormat(config);
            }
        }

        if (config.type) {
            translateDeprecatedTypes(config);
        }

        let type = config.type ? config.type.toLowerCase() : undefined;

        if (!type) {

            // If neither format nor type are known throw an error
            if (!config.format) {
                throw Error(`Unrecognized track:  ${JSON.stringify(config)}`)
            } else if (config.format === "hic") {
                const hicFile = new HicFile(config);
                await hicFile.readHeaderAndFooter();
                if (hicFile.chromosomeIndexMap.celltype) {
                    type = "shoebox";
                    config._hicFile = hicFile;
                } else {
                    throw Error("'.hic' files not supported")
                }
            } else {
                type = inferTrackType(config.format);
                if ("bedtype" === type) {
                    // Bed files must be read to determine track type
                    const featureSource = FeatureSource(config, this.genome);
                    config._featureSource = featureSource;    // This is a temp variable, bit of a hack
                    const trackType = await featureSource.trackType();
                    if (trackType && knownTrackTypes().has(trackType)) {
                        type = trackType;
                    } else {
                        type = "annotation";
                    }
                }
            }
            // Record in config to make type persistent in session
            config.type = type;
        }

        // Set defaults if specified
        if (this.trackDefaults && type) {
            const settings = this.trackDefaults[type];
            if (settings) {
                for (let property in settings) {
                    if (settings.hasOwnProperty(property) && config[property] === undefined) {
                        config[property] = settings[property];
                    }
                }
            }
        }

        const track = getTrack(type, config, this);
        if (undefined === track) {
            this.alert.present(new Error(`Error creating track.  Could not determine track type for file: ${config.url || config}`), undefined);
        } else {

            if (config.roi && config.roi.length > 0) {
                track.roiSets = config.roi.map(r => new TrackROISet(r, this.genome));
            }

            return track
        }
    }

    reorderTracks() {

        this.trackViews.sort(function (a, b) {

            const firstSortOrder = tv => {
                return 'ideogram' === tv.track.id ? 1 :
                    'ruler' === tv.track.id ? 2 :
                        3
            };

            const aOrder1 = firstSortOrder(a);
            const bOrder1 = firstSortOrder(b);
            if (aOrder1 === bOrder1) {
                const aOrder2 = a.track.order || 0;
                const bOrder2 = b.track.order || 0;
                return aOrder2 - bOrder2
            } else {
                return aOrder1 - bOrder1
            }
        });

        // discard current track order
        for (let {
            axis,
            viewports,
            sampleInfoViewport,
            sampleNameViewport,
            outerScroll,
            dragHandle,
            gearContainer
        } of this.trackViews) {

            axis.remove();

            for (let {viewportElement} of viewports) {
                viewportElement.parentNode.removeChild(viewportElement);
            }

            sampleInfoViewport.viewport.remove();

            sampleNameViewport.viewport.remove();

            outerScroll.remove();
            dragHandle.remove();
            gearContainer.remove();
        }

        // Reattach the divs to the dom in the correct order
        const viewportColumns = this.columnContainer.querySelectorAll('.igv-column');

        for (let {
            axis,
            viewports,
            sampleInfoViewport,
            sampleNameViewport,
            outerScroll,
            dragHandle,
            gearContainer
        } of this.trackViews) {

            this.columnContainer.querySelector('.igv-axis-column').appendChild(axis);

            for (let i = 0; i < viewportColumns.length; i++) {
                const {viewportElement} = viewports[i];
                viewportColumns[i].appendChild(viewportElement);
            }

            this.columnContainer.querySelector('.igv-sample-info-column').appendChild(sampleInfoViewport.viewport);

            this.columnContainer.querySelector('.igv-sample-name-column').appendChild(sampleNameViewport.viewport);

            this.columnContainer.querySelector('.igv-scrollbar-column').appendChild(outerScroll);

            this.columnContainer.querySelector('.igv-track-drag-column').appendChild(dragHandle);

            this.columnContainer.querySelector('.igv-gear-menu-column').appendChild(gearContainer);
        }

    }

    getTrackOrder() {
        return this.trackViews.filter(tv => tv.track && tv.track.name).map(tv => tv.track.name)
    }

    getSelectedTrackViews() {
        return this.trackViews.filter(trackView => true === trackView.track.selected)
    }

    /**
     * NOTE: Public API function
     *
     * Remove all tracks matching the given name.  Usually this will be a single track, but there is no
     * guarantee names are unique
     *
     * @param name
     */
    removeTrackByName(name) {
        const copy = this.trackViews.slice();
        for (let trackView of copy) {
            if (name === trackView.track.name) {
                this.removeTrack(trackView.track);
            }
        }
    }

    /**
     * NOTE: Public API function
     *
     * Remove the given track.  If it has already been removed this is a no-op.
     *
     * @param track
     */
    removeTrack(track) {
        for (let trackView of this.trackViews) {
            if (track === trackView.track) {
                this._removeTrack(trackView.track);
                break
            }
        }
    }

    _removeTrack(track) {
        if (track.disposed) return
        this.trackViews.splice(this.trackViews.indexOf(track.trackView), 1);
        this.fireEvent('trackremoved', [track]);
        this.fireEvent('trackorderchanged', [this.getTrackOrder()]);
        if (track.trackView) {
            track.trackView.dispose();
        }
    }

    /**
     * API function
     */
    removeAllTracks() {

        const currentTrackViews = this.trackViews;
        this.trackViews = [];

        for (let trackView of currentTrackViews) {

            if (trackView.track.id !== 'ruler' && trackView.track.id !== 'ideogram') {
                this.fireEvent('trackremoved', [trackView.track]);
                trackView.dispose();
            } else {
                this.trackViews.push(trackView);
            }
        }
    }

    /**
     * Currently the ideogram  is always in position 1.  This is a bit fragile, we will wrap it in property to be
     * explicict
     */
    get ideogramTrackView() {
        return this.trackViews[0]
    }

    /**
     * Currently the ruler track is always in position 1.  This is a bit fragile, we will wrap it in property to be
     * explicict
     */
    get rulerTrackView() {
        return this.trackViews[1]
    }

    /**
     *
     * @param property
     * @param value
     * @returns {Array}  tracks with given property value.  e.g. findTracks("type", "annotation")
     */
    findTracks(property, value) {

        let f = typeof property === 'function' ?
            trackView => property(trackView.track) :
            trackView => value === trackView.track[property];

        return this.trackViews.filter(f).map(tv => tv.track)
    }

    get tracks() {
        return this.trackViews.map(tv => tv.track).filter(t => t !== undefined)
    }

    /**
     * Set the track height globally for all tracks.  (Note: Its not clear why this is useful).
     * @param newHeight
     */
    setTrackHeight(newHeight) {

        this.trackHeight = newHeight;

        this.trackViews.forEach(function (trackView) {
            trackView.setTrackHeight(newHeight);
        });

    }

    /**
     * API function to signal that this browser visibility has changed, e.g. from hiding/showing in a tab interface.
     *
     * @returns {Promise<void>}
     */
    async visibilityChange() {
        this.layoutChange();
    }

    async layoutChange() {

        const status = this.referenceFrameList.find(referenceFrame => referenceFrame.bpPerPixel < 0);

        if (status) {
            const viewportWidth = this.calculateViewportWidth(this.referenceFrameList.length);
            for (let referenceFrame of this.referenceFrameList) {
                referenceFrame.bpPerPixel = (referenceFrame.end - referenceFrame.start) / viewportWidth;
            }
        }

        if (this.referenceFrameList) {
            this.navbar.navbarDidResize();
        }

        resize.call(this);

        this.roiManager.updateROIRegionPositions();

        await this.updateViews();
    }

    async updateViews() {

        const trackViews = this.trackViews;

        this.updateLocusSearchWidget();

        for (const {bpPerPixel, chr, start} of this.referenceFrameList) {
            if (bpPerPixel <= bppSequenceThreshold) {
                await this.genome.getSequence(chr, start, start + 1);
            }
        }

        for (const centerGuide of this.centerLineList) {
            centerGuide.repaint();
        }

        // Don't autoscale while dragging.
        if (this.dragObject) {
            for (const trackView of trackViews) {
                await trackView.updateViews();
            }
        } else {
            // Group autoscale is done here as it involves multiple tracks.  Individual track autoscale is done in TrackView
            const groupAutoscaleTrackViews = {};
            const otherTrackViews = [];

            // Isolate group autoscale trackViews
            for (const trackView of trackViews) {
                if (trackView.track.autoscaleGroup) {
                    const autoscaleGroup = trackView.track.autoscaleGroup;
                    if (!groupAutoscaleTrackViews[autoscaleGroup]) {
                        groupAutoscaleTrackViews[autoscaleGroup] = [];
                    }
                    groupAutoscaleTrackViews[autoscaleGroup].push(trackView);
                } else {
                    otherTrackViews.push(trackView);
                }
            }

            // Calculate group autoscale dataRange
            if (Object.entries(groupAutoscaleTrackViews).length > 0) {
                for (const [group, trackViews] of Object.entries(groupAutoscaleTrackViews)) {
                    const inViewFeatures = await Promise.all(trackViews.map(trackView => trackView.getInViewFeatures()));
                    const dataRange = doAutoscale(inViewFeatures.flat());
                    for (const trackView of trackViews) {
                        trackView.track.dataRange = Object.assign({}, dataRange);
                        trackView.track.autoscale = false;
                    }
                    await Promise.all(trackViews.map(trackView => trackView.updateViews()));
                }
            }

            await Promise.all(otherTrackViews.map(trackView => trackView.updateViews()));
        }

    }

    repaintViews() {
        for (let trackView of this.trackViews) {
            trackView.repaintViews();
        }
    }

    updateLocusSearchWidget() {

        if (!this.referenceFrameList) return
        const referenceFrameList = this.referenceFrameList;

        // Update end position of reference frames based on pixel widths.  This is hacky, but its been done here
        // for a long time, although indirectly.
        const width = this.calculateViewportWidth(this.referenceFrameList.length);
        for (let referenceFrame of referenceFrameList) {
            referenceFrame.end = referenceFrame.start + referenceFrame.bpPerPixel * width;
        }

        const loc = this.referenceFrameList.map(rf => rf.getLocusString()).join(' ');

        const chrName = referenceFrameList.length === 1 ? this.genome.getChromosomeDisplayName(this.referenceFrameList[0].chr) : '';

        this.navbar.updateLocus(loc, chrName);

        this.fireEvent('locuschange', [this.referenceFrameList]);
    }

    calculateViewportWidth(columnCount) {

        let {width} = this.columnContainer.getBoundingClientRect();

        width -= igv_axis_column_width + this.getSampleInfoViewportWidth() + this.getSampleNameViewportWidth() + igv_scrollbar_outer_width + igv_track_manipulation_handle_width + igv_track_gear_menu_column_width;

        width -= column_multi_locus_shim_width * (columnCount - 1);

        return Math.floor(width / columnCount)
    }

    /**
     * Update reference frames based on new viewport width
     * @param {number} viewportWidth - The calculated viewport width
     */
    updateReferenceFrames(viewportWidth) {

        for (const referenceFrame of this.referenceFrameList) {
            referenceFrame.updateForViewportWidth(viewportWidth);
        }
    }

    /**
     * Update DOM viewport elements with new width
     * @param {number} viewportWidth - The calculated viewport width
     */
    updateViewportElements(viewportWidth) {

        for (let i = 0; i < this.referenceFrameList.length; i++) {

            for (const {viewports} of this.trackViews) {
                viewports[i].setWidth(viewportWidth);
            }

            for (const {sampleInfoViewport} of this.trackViews) {
                sampleInfoViewport.setWidth(this.getSampleInfoColumnWidth());
                sampleInfoViewport.repaint();
            }

        }
    }

    /**
     * Synchronize UI state after viewport updates
     * @returns {Promise<void>}
     */
    async syncUIState() {
        this.updateUIWithReferenceFrameList();
        await this.updateViews(true);
    }

    minimumBases() {
        return this.config.minimumBases
    }

    // Zoom in by a factor of 2, keeping the same center location
    zoomIn() {
        this.zoomWithScaleFactor(0.5);
    };

    // Zoom out by a factor of 2, keeping the same center location if possible
    zoomOut() {
        this.zoomWithScaleFactor(2.0);
    };

    async zoomWithScaleFactor(scaleFactor, centerBPOrUndefined, referenceFrameOrUndefined) {

        if (!this.referenceFrameList) return

        const viewportWidth = this.calculateViewportWidth(this.referenceFrameList.length);

        let referenceFrames = referenceFrameOrUndefined ? [referenceFrameOrUndefined] : this.referenceFrameList;

        for (let referenceFrame of referenceFrames) {
            referenceFrame.zoomWithScaleFactor(this, scaleFactor, viewportWidth, centerBPOrUndefined);
        }
    }

    /**
     * Add a new multi-locus panel for the specified region
     * @param chr
     * @param start
     * @param end
     * @param referenceFrameLeft - optional, if supplied new panel should be placed to the immediate right
     */
    async addMultiLocusPanel(chr, start, end, referenceFrameLeft) {

        if (!this.referenceFrameList) return

        // account for reduced viewport width as a result of adding right mate pair panel
        const viewportWidth = this.calculateViewportWidth(1 + this.referenceFrameList.length);
        const scaleFactor = this.calculateViewportWidth(this.referenceFrameList.length) / this.calculateViewportWidth(1 + this.referenceFrameList.length);
        for (let refFrame of this.referenceFrameList) {
            refFrame.bpPerPixel *= scaleFactor;
        }

        const bpp = (end - start) / viewportWidth;
        const newReferenceFrame = new ReferenceFrame(this.genome, chr, start, end, bpp);
        const indexLeft = referenceFrameLeft ? this.referenceFrameList.indexOf(referenceFrameLeft) : this.referenceFrameList.length - 1;
        const indexRight = 1 + indexLeft;

        // TODO -- this is really ugly
        const {viewportElement} = this.trackViews[0].viewports[indexLeft];
        const viewportColumn = viewportColumnManager.insertAfter(viewportElement.parentElement);
        this.fireEvent('didchangecolumnlayout');

        if (indexRight === this.referenceFrameList.length) {
            this.referenceFrameList.push(newReferenceFrame);
            for (let trackView of this.trackViews) {
                const viewport = createViewport(trackView, viewportColumn, newReferenceFrame);
                trackView.viewports.push(viewport);
            }
        } else {
            this.referenceFrameList.splice(indexRight, 0, newReferenceFrame);
            for (let trackView of this.trackViews) {
                const viewport = createViewport(trackView, viewportColumn, newReferenceFrame);
                trackView.viewports.splice(indexRight, 0, viewport);
            }
        }


        this.centerLineList = this.createCenterLineList(this.columnContainer);

        resize.call(this);
        await this.updateViews(true);
    }

    createCenterLineList(columnContainer) {

        const centerLines = columnContainer.querySelectorAll('.igv-center-line');
        for (let i = 0; i < centerLines.length; i++) {
            centerLines[i].remove();
        }

        const centerLineList = [];
        const viewportColumns = columnContainer.querySelectorAll('.igv-column');
        for (let i = 0; i < viewportColumns.length; i++) {
            centerLineList.push(new ViewportCenterLine(this, this.referenceFrameList[i], viewportColumns[i]));
        }

        return centerLineList
    }

    async removeMultiLocusPanel(referenceFrame) {

        // find the $column corresponding to this referenceFrame and remove it
        const index = this.referenceFrameList.indexOf(referenceFrame);
        const {viewportElement} = this.trackViews[0].viewports[index];

        viewportColumnManager.removeColumnAtIndex(index, viewportElement.parentElement);
        this.fireEvent('didchangecolumnlayout');

        for (let {viewports} of this.trackViews) {
            viewports[index].dispose();
            viewports.splice(index, 1);
        }

        this.referenceFrameList.splice(index, 1);

        if (1 === this.referenceFrameList.length && this.getRulerTrackView()) {
            for (let rulerViewport of this.getRulerTrackView().viewports) {
                rulerViewport.dismissLocusLabel();
            }
        }

        const scaleFactor = this.calculateViewportWidth(1 + this.referenceFrameList.length) / this.calculateViewportWidth(this.referenceFrameList.length);

        await this.rescaleForMultiLocus(scaleFactor);

    }

    /**
     * Goto the locus represented by the selected referenceFrame, discarding all other panels
     *
     * @param referenceFrame
     * @returns {Promise<void>}
     */
    async gotoMultilocusPanel(referenceFrame) {

        const referenceFrameIndex = this.referenceFrameList.indexOf(referenceFrame);

        // Remove columns for unselected panels
        this.columnContainer.querySelectorAll('.igv-column').forEach((column, c) => {
            if (c === referenceFrameIndex) ; else {
                column.remove();
            }
        });

        // Remove all column shims
        this.columnContainer.querySelectorAll('.igv-column-shim').forEach(shim => shim.remove());

        // Discard viewports
        for (let trackView of this.trackViews) {
            const retain = trackView.viewports[referenceFrameIndex];
            trackView.viewports.filter((viewport, i) => i !== referenceFrameIndex).forEach(viewport => viewport.dispose());
            trackView.viewports = [retain];
        }

        const viewportWidth = this.calculateViewportWidth(1);
        referenceFrame.bpPerPixel = (referenceFrame.end - referenceFrame.start) / viewportWidth;
        this.referenceFrameList = [referenceFrame];

        this.trackViews.forEach(({viewports}) => viewports.forEach(viewport => viewport.setWidth(viewportWidth)));

        this.centerLineList = this.createCenterLineList(this.columnContainer);

        this.updateUIWithReferenceFrameList();

        await this.updateViews(true);

    }

    async rescaleForMultiLocus(scaleFactor) {

        const viewportWidth = this.calculateViewportWidth(this.referenceFrameList.length);

        for (let referenceFrame of this.referenceFrameList) {
            referenceFrame.bpPerPixel *= scaleFactor;
        }

        for (let {viewports} of this.trackViews) {

            for (let viewport of viewports) {
                viewport.setWidth(viewportWidth);
            }
        }

        this.centerLineList = this.createCenterLineList(this.columnContainer);

        this.updateUIWithReferenceFrameList();

        await this.updateViews();

    }

    /**
     * @deprecated  This is a deprecated method with no known usages.
     */
    async goto(chr, start, end) {
        await this.search(chr + ":" + start + "-" + end);
    }


    /**
     * Search for the locus string
     * NOTE: This is part of the API
     * @param stringOrArray
     * @param init  true if called during browser initialization
     *
     * @returns {Promise<boolean>}  true if found, false if not
     */
    async search(stringOrArray, init) {

        const loci = await search(this, stringOrArray);
        return this.updateLoci(loci, init)
    }

    async updateLoci(loci, init) {

        if (loci && loci.length > 0) {

            // create reference frame list based on search loci
            this.referenceFrameList = createReferenceFrameList(loci, this.genome, this.flanking, this.minimumBases(), this.calculateViewportWidth(loci.length), this.isSoftclipped());

            // discard track viewport DOM elements
            for (let trackView of this.trackViews) {
                trackView.removeViewportsFromColumnContainer();
            }

            // discard ONLY viewport columns
            this.columnContainer.querySelectorAll('.igv-column-shim, .igv-column').forEach(el => el.remove());

            // Insert viewport columns preceding the sample info column
            viewportColumnManager.insertBefore(this.columnContainer.querySelector('.igv-sample-info-column'), this.referenceFrameList.length);
            this.fireEvent('didchangecolumnlayout');

            // Create the viewport objects -- TODO -- this is done for every search, which is insane
            for (let trackView of this.trackViews) {
                trackView.createViewports(this, this.columnContainer, this.referenceFrameList);
            }

            this.centerLineList = this.createCenterLineList(this.columnContainer);

            this.updateUIWithReferenceFrameList();

            if (!init) {
                await this.updateViews();
            }
            return true
        } else {
            return false
        }
    }

    async loadSampleInfo(sampleInfoConfig) {


        await this.sampleInfo.loadSampleInfo(sampleInfoConfig);

        if (this.config.sampleinfo) {
            this.config.sampleinfo.push(sampleInfoConfig);
        } else {
            this.config.sampleinfo = [sampleInfoConfig];
        }

        for (const {sampleInfoViewport} of this.trackViews) {
            sampleInfoViewport.setWidth(this.getSampleInfoColumnWidth());
        }

        const found = this.findTracks(t => typeof t.getSamples === 'function');
        if (found.length > 0) {
            this.sampleInfoControl.performClickWithState(this, true);
            this.sampleInfoControl.setButtonVisibility(true);
        }

        for (const {sampleInfoViewport} of this.trackViews) {
            sampleInfoViewport.repaint();
        }

        // await this.layoutChange()
    }

    async discardSampleInfo() {

        this.sampleInfo.discard();

        for (const {sampleInfoViewport} of this.trackViews) {
            sampleInfoViewport.setWidth(this.getSampleInfoColumnWidth());
        }

        const found = this.findTracks(t => typeof t.getSamples === 'function');
        if (found.length > 0) {
            this.sampleInfoControl.performClickWithState(this, false);
            this.sampleInfoControl.setButtonVisibility(false);
        }

        for (const {sampleInfoViewport} of this.trackViews) {
            sampleInfoViewport.repaint();
        }

        await this.layoutChange();
    }

    getSampleInfoColumnWidth() {

        if (!this.sampleInfo.attributeCount) {
            return 0
        } else {

            const found = this.findTracks(t => typeof t.getSamples === 'function');
            const isFound = found.length > 0;
            const hasAttributes = this.sampleInfo.hasAttributes();
            const doShowSampleInfo = this.sampleInfoControl.showSampleInfo;
            const status = isFound && hasAttributes && doShowSampleInfo;

            if (status) {
                return this.sampleInfo.attributeCount * sampleInfoTileWidth + sampleInfoTileXShim
            } else {
                return 0
            }
        }
    }


// EVENTS

    on(eventName, fn) {
        if (!this.eventHandlers[eventName]) {
            this.eventHandlers[eventName] = [];
        }
        this.eventHandlers[eventName].push(fn);
    };

    /**
     * @deprecated use off()
     * @param eventName
     * @param fn
     */
    un(eventName, fn) {
        this.off(eventName, fn);
    };

    off(eventName, fn) {

        if (!eventName) {
            this.eventHandlers = {};   // Remove all event handlers
        } else if (!fn) {
            this.eventHandlers[eventName] = []; // Remove all eventhandlers matching name
        } else {
            // Remove specific event handler
            const handlers = this.eventHandlers[eventName];
            if (!handlers || handlers.length === 0) {
                console.warn("No handlers to remove for event: " + eventName);
            } else {
                const callbackIndex = handlers.indexOf(fn);
                if (callbackIndex !== -1) {
                    this.eventHandlers[eventName].splice(callbackIndex, 1);
                }
            }
        }
    }

    fireEvent(eventName, args, thisObj) {

        const handlers = this.eventHandlers[eventName];
        if (undefined === handlers || handlers.length === 0) {
            return undefined
        }

        const scope = thisObj || window;
        const results = handlers.map(function (event) {
            return event.apply(scope, args)
        });

        return results[0]
    }

    dispose() {
        this.removeEventHandlers();
        for (let trackView of this.trackViews) {
            trackView.dispose();
        }
        if (this.roiManager) {
            this.roiManager.dispose();
        }
    }

    /**
     * Return a json-like object (note not a json string) representing the current state.
     *
     */
    toJSON() {

        const json = {
            "version": version$1()
        };

        if (this.showSampleNames !== undefined) {
            json['showSampleNames'] = this.showSampleNames;
        }

        if (this.sampleNameViewportWidth) {
            json['sampleNameViewportWidth'] = this.sampleNameViewportWidth;
        }

        json["reference"] = this.genome.toJSON();
        if (json.reference.fastaURL instanceof File) {   // Test specifically for File.  Other types of File-like objects might be savable) {
            throw new Error(`Error. Sessions cannot include local file references ${json.reference.fastaURL.name}.`)
        } else if (json.reference.indexURL instanceof File) {   // Test specifically for File.  Other types of File-like objects might be savable) {
            throw new Error(`Error. Sessions cannot include local file references ${json.reference.indexURL.name}.`)
        }

        // Build locus array (multi-locus view).  Use the first track to extract the loci, any track could be used.
        const locus = [];
        let anyTrackView = this.trackViews[0];
        for (let {referenceFrame} of anyTrackView.viewports) {
            const locusString = referenceFrame.getLocusString();
            locus.push(locusString);
        }
        json["locus"] = locus.length === 1 ? locus[0] : locus;

        const roiSets = this.roiManager.toJSON();
        if (roiSets) {
            json["roi"] = roiSets;
            if (!this.roiManager.showOverlays) {
                json["showROIOverlays"] = false;   // true is the default
            }
        }

        if (!this.qtlSelections.isEmpty()) {
            json["qtlSelections"] = this.qtlSelections.toJSON();
        }

        // Filter configurations
        // REMOVED: Filter configurations are now saved as part of individual track configurations
        // if (this.filterConfigurations.size > 0) {
        //     const filterConfigs = {}
        //     for (const [trackType, filters] of this.filterConfigurations) {
        //         filterConfigs[trackType] = filters
        //     }
        //     json["filterConfigurations"] = filterConfigs
        // }

        // Tracks
        const trackJson = [];
        const errors = [];
        for (const {track} of this.trackViews) {
            try {

                let config;
                if (typeof track.getState === "function") {
                    config = TrackBase.localFileInspection(track.getState());
                } else if (track.config) {
                    config = TrackBase.localFileInspection(track.config);
                }

                if (config) {
                    // null backpointer to browser
                    if (config.browser) {
                        delete config.browser;
                    }

                    config.order = track.order;

                    trackJson.push(config);
                }
            } catch (e) {
                const str = `Track: ${track.name}: ${e}`;
                console.error(str);
                errors.push(str);
            }
        }

        if (errors.length > 0) {
            let n = 1;
            let message = 'Errors encountered saving session: </br>';
            for (let e of errors) {
                message += ` (${n++}) ${e.toString()} <br/>`;
            }
            throw Error(message)
        }

        json["tracks"] = trackJson;

        const localFileDetections = [];
        for (const json of trackJson) {
            for (const key of Object.keys(json)) {
                if ('file' === key || 'indexFile' === key) {
                    localFileDetections.push(json[key]);
                }
            }
        }

        // Sample info
        const localSampleInfoFileDetections = [];
        if (this.config.sampleinfo) {

            json["sampleinfo"] = this.config.sampleinfo;

            for (const path of this.sampleInfo.sampleInfoFiles) {
                const config = TrackBase.localFileInspection({url: path});
                if (config.file) {
                    localSampleInfoFileDetections.push(config.file);
                }
            }
            if (localSampleInfoFileDetections.length > 0) {
                localFileDetections.push(...localSampleInfoFileDetections);
            }
        }

        if (localFileDetections.length > 0) {
            alert(`This session includes reference(s) to local file(s):\n${localFileDetections.map(str => `    ${str}`).join('\n')}\nLocal files cannot be loaded automatically when a saved session is restored.`);
        }

        return json
    }

    compressedSession() {
        const json = JSON.stringify(this.toJSON());
        return compressString(json)
    }

    sessionURL() {
        const path = window.location.href.slice();
        const idx = path.indexOf("?");
        const surl = (idx > 0 ? path.substring(0, idx) : path) + "?sessionURL=blob:" + this.compressedSession();
        return surl
    }

    /**
     * Record a mouse click on a specific viewport.   This might be the start of a drag operation.   Dragging
     * (panning) is handled here so that the mouse can move out of a specific viewport (e.g. stray into another
     * track) without halting the drag.
     *
     * @param e
     * @param viewport
     */
    mouseDownOnViewport(e, viewport) {

        var coords;
        coords = pageCoordinates(e);
        this.vpMouseDown = {
            viewport,
            lastMouseX: coords.x,
            mouseDownX: coords.x,
            lastMouseY: coords.y,
            mouseDownY: coords.y,
            referenceFrame: viewport.referenceFrame
        };
    };

    cancelTrackPan() {

        const dragObject = this.dragObject;
        this.dragObject = undefined;
        this.isScrolling = false;
        this.vpMouseDown = undefined;

        if (dragObject && dragObject.viewport.referenceFrame.start !== dragObject.start) {
            this.updateViews();
            this.fireEvent('trackdragend', [dragObject.viewport]);
        }
    }

    isTrackPanning() {
        return this.dragObject
    }

    isSoftclipped() {
        const result = this.trackViews.find(tv => tv.track.showSoftClips === true);
        return result !== undefined
    }


    /**
     * Track drag here refers to vertical dragging to reorder tracks, not horizontal panning.
     *
     * @param trackView
     */
    startTrackDrag(trackView) {

        this.dragTrack = trackView;

    }

    /**
     * Track drag here refers to vertical dragging to reorder tracks, not horizontal panning.
     *
     * @param dragDestination
     */
    updateTrackDrag(dragDestination) {

        if (dragDestination && this.dragTrack) {

            const dragged = this.dragTrack;
            const indexDestination = this.trackViews.indexOf(dragDestination);
            const indexDragged = this.trackViews.indexOf(dragged);
            const trackViews = this.trackViews;

            trackViews[indexDestination] = dragged;
            trackViews[indexDragged] = dragDestination;

            const newOrder = this.trackViews[indexDestination].track.order;
            this.trackViews[indexDragged].track.order = newOrder;

            const nTracks = trackViews.length;
            let lastOrder = newOrder;

            if (indexDestination < indexDragged) {
                // Displace tracks below

                for (let i = indexDestination + 1; i < nTracks; i++) {
                    const track = trackViews[i].track;
                    if (track.order <= lastOrder) {
                        track.order = Math.min(Number.MAX_SAFE_INTEGER, lastOrder + 1);
                        lastOrder = track.order;
                    } else {
                        break
                    }
                }
            } else {
                // Displace tracks above.  First track (index 0) is "ruler"
                for (let i = indexDestination - 1; i > 0; i--) {
                    const track = trackViews[i].track;
                    if (track.order >= lastOrder) {
                        track.order = Math.max(-Number.MAX_SAFE_INTEGER, lastOrder - 1);
                        lastOrder = track.order;
                    } else {
                        break
                    }
                }
            }
            this.reorderTracks();
        }
    }

    /**
     * End vertical dragging of tracks (i.e. track re-order, not horizontal panning of data)
     */
    endTrackDrag() {
        if (this.dragTrack) {
            this.dragTrack = undefined;
            this.fireEvent('trackorderchanged', [this.getTrackOrder()]);
        } else {
            this.dragTrack = undefined;
        }
    }

    /**
     * Mouse handlers to support drag (pan)
     */
    addEventHandlers() {
        this.addWindowResizeHandler();
        this.addRootMouseUpHandler();
        this.addRootMouseLeaveHandler();
        this.addColumnContainerEventHandlers();
        this.addKeyboardHandler();
    }

    removeEventHandlers() {
        this.removeWindowResizeHandler();
        this.removeRootMouseUpHandler();
        this.removeRootMouseLeaveHandler();
        this.removeColumnContainerEventHandlers();
        this.removeKeyboardHandler();
    }

    addWindowResizeHandler() {
        // Create a copy of the prototype "resize" function bound to this instance.  Neccessary to support removing.
        this.boundWindowResizeHandler = resize.bind(this);
        window.addEventListener('resize', this.boundWindowResizeHandler);
    }

    removeWindowResizeHandler() {
        window.removeEventListener('resize', this.boundWindowResizeHandler);
    }

    addRootMouseUpHandler() {
        this.boundRootMouseUpHandler = mouseUpOrLeave.bind(this);
        this.root.addEventListener('mouseup', this.boundRootMouseUpHandler);
    }

    removeRootMouseUpHandler() {
        this.root.removeEventListener('mouseup', this.boundRootMouseUpHandler);
    }

    addRootMouseLeaveHandler() {
        this.boundRootMouseLeaveHandler = mouseUpOrLeave.bind(this);
        this.root.addEventListener('mouseleave', this.boundRootMouseLeaveHandler);
    }

    removeRootMouseLeaveHandler() {
        this.root.removeEventListener('mouseleave', this.boundRootMouseLeaveHandler);
    }

    addColumnContainerEventHandlers() {
        this.boundColumnContainerMouseMoveHandler = handleMouseMove.bind(this);
        this.boundColumnContainerTouchMoveHandler = handleMouseMove.bind(this);
        this.boundColumnContainerMouseLeaveHandler = mouseUpOrLeave.bind(this);
        this.boundColumnContainerMouseUpHandler = mouseUpOrLeave.bind(this);
        this.boundColumnContainerTouchEndHandler = mouseUpOrLeave.bind(this);

        this.columnContainer.addEventListener('mousemove', this.boundColumnContainerMouseMoveHandler);
        this.columnContainer.addEventListener('touchmove', this.boundColumnContainerTouchMoveHandler);

        this.columnContainer.addEventListener('mouseleave', this.boundColumnContainerMouseLeaveHandler);

        this.columnContainer.addEventListener('mouseup', this.boundColumnContainerMouseUpHandler);
        this.columnContainer.addEventListener('touchend', this.boundColumnContainerTouchEndHandler);
    }

    removeColumnContainerEventHandlers() {
        this.columnContainer.removeEventListener('mousemove', this.boundColumnContainerMouseMoveHandler);
        this.columnContainer.removeEventListener('touchmove', this.boundColumnContainerTouchMoveHandler);

        this.columnContainer.removeEventListener('mouseleave', this.boundColumnContainerMouseLeaveHandler);

        this.columnContainer.removeEventListener('mouseup', this.boundColumnContainerMouseUpHandler);
        this.columnContainer.removeEventListener('touchend', this.boundColumnContainerTouchEndHandler);
    }

    addKeyboardHandler() {
        this.keyUpHandler = keyUpHandler.bind(this);
        document.addEventListener("keyup", this.keyUpHandler);
    }

    removeKeyboardHandler() {
        document.addEventListener("keyup", this.keyUpHandler);
    }


    static uncompressSession(url) {

        let bytes;
        if (url.indexOf('/gzip;base64') > 0) {
            //Proper dataURI
            bytes = decodeDataURI$1(url);
            let json = '';
            for (let b of bytes) {
                json += String.fromCharCode(b);
            }
            return json
        } else {

            let enc = url.substring(5);
            return uncompressString(enc)
        }
    }

    createCircularView(container, show) {
        show = show === true;   // convert undefined to boolean
        this.circularView = createCircularView(container, this);
        this.circularViewControl = new CircularViewControl(this.navbar.toggleButtonContainer, this);
        this.circularView.setAssembly({
            name: this.genome.id,
            id: this.genome.id,
            chromosomes: makeCircViewChromosomes(this.genome)
        });
        this.circularViewVisible = show;
        return this.circularView
    }

    get circularViewVisible() {
        return this.circularView !== undefined && this.circularView.visible
    }

    set circularViewVisible(isVisible) {
        if (this.circularView) {
            this.circularView.visible = isVisible;
            this.circularViewControl.setState(isVisible);
        }
    }

    // Navbar delegates
    get overlayTrackButton() {
        return this.navbar.overlayTrackButton
    }

    get roiTableControl() {
        return this.navbar.roiTableControl
    }

    get sampleNameControl() {
        return this.navbar.sampleNameControl
    }

    get sampleInfoControl() {
        return this.navbar.sampleInfoControl
    }

    async blat(sequence) {
        return createBlatTrack({sequence, browser: this, name: 'Blat', title: 'Blat'})
    }
}

function getFileExtension(input) {
    let fileName;

    // Check if input is a File object or a URL string
    if (input instanceof File) {
        fileName = input.name;
    } else if (typeof input === 'string') {
        fileName = input;
    } else {
        throw new Error('Input must be a File object or a URL string')
    }

    // Extract the file extension
    const fileExtension = fileName.split('.').pop();

    // If the URL is from Dropbox, the extension may be followed by a query string
    // Remove the query string, if present
    const cleanFileExtension = fileExtension.split('?')[0];

    return cleanFileExtension
}

/**
 * Called when window is resized, or visibility changed (e.g. "show" from a tab).  This is a function rather
 * than class method because it needs to be copied and bound to specific instances of browser to support listener
 * removal
 *
 * @returns {Promise<void>}
 */
async function resize(event) {

    if (undefined === this.referenceFrameList || 0 === this.referenceFrameList.length) {
        return
    }

    const viewportWidth = this.calculateViewportWidth(this.referenceFrameList.length);
    this.updateReferenceFrames(viewportWidth);
    this.updateViewportElements(viewportWidth);
    await this.syncUIState();
}


function handleMouseMove(e) {

    e.preventDefault();

    const {x, y} = pageCoordinates(e);

    if (this.vpMouseDown) {

        const {viewport, referenceFrame} = this.vpMouseDown;

        // Determine direction,  true == horizontal
        const horizontal = Math.abs((x - this.vpMouseDown.mouseDownX)) > Math.abs((y - this.vpMouseDown.mouseDownY));

        if (!this.dragObject && !this.isScrolling) {
            if (horizontal) {
                if (this.vpMouseDown.mouseDownX && Math.abs(x - this.vpMouseDown.mouseDownX) > this.constants.dragThreshold) {
                    this.dragObject = {viewport, start: referenceFrame.start};
                }
            } else {
                if (this.vpMouseDown.mouseDownY &&
                    Math.abs(y - this.vpMouseDown.mouseDownY) > this.constants.scrollThreshold) {
                    // Scrolling => dragging track vertically
                    this.isScrolling = true;
                    const viewportHeight = viewport.viewportElement.clientHeight;
                    const contentHeight = viewport.trackView.maxViewportContentHeight();
                    this.vpMouseDown.r = viewportHeight / contentHeight;
                }
            }
        }

        if (this.dragObject) {
            const clampDrag = !this.isSoftclipped();
            let deltaX = this.vpMouseDown.lastMouseX - x;
            const viewChanged = referenceFrame.shiftPixels(deltaX, viewport.viewportElement.clientWidth, clampDrag);
            if (viewChanged) {
                this.updateViews();
            }
            this.fireEvent('trackdrag');
        }


        if (this.isScrolling) {
            const delta = (this.vpMouseDown.lastMouseY - y);
            viewport.trackView.scrollByPixels(delta);
        }


        this.vpMouseDown.lastMouseX = x;
        this.vpMouseDown.lastMouseY = y;
    }
}

function mouseUpOrLeave(e) {
    this.cancelTrackPan();
    this.endTrackDrag();
}

/**
 * Handle keyup event, used for navigating feature tracks with hot keys.  This will get bound to the browser object
 * @param event
 */
async function keyUpHandler(event) {

    // Feature jumping disabled in multi-locus view
    if (!this.referenceFrameList || this.referenceFrameList.length > 1) return

    if (event.code === 'KeyF' || event.code === 'KeyB') {

        const selectedTrackViews = this.getSelectedTrackViews();

        if (selectedTrackViews.length > 0) {

            const track = selectedTrackViews[0].track;

            if (typeof track.nextFeatureAfter === 'function') {

                const referenceFrame = this.referenceFrameList[0];
                const viewportWidth = referenceFrame.viewport ? referenceFrame.viewport.getWidth() : this.calculateViewportWidth(this.referenceFrameList.length);


                // Check visibility window
                const isWGV = 'all' === referenceFrame.chr.toLowerCase();
                const vizWindow = track.visibilityWindow;
                if (isWGV || (vizWindow && vizWindow > 0 && referenceFrame.bpPerPixel * viewportWidth > vizWindow)) {
                    return
                }


                const direction = 'KeyF' === event.code;
                const chr = referenceFrame.chr;
                const center = referenceFrame.center;
                const nextFeature = await track.nextFeatureAfter(chr, center, direction);
                if (nextFeature) {
                    const nextChr = await this.genome.getChromosomeName(nextFeature.chr);
                    if (chr === nextChr) {

                        // On same chromoeoms
                        const newCenter = (nextFeature.start + nextFeature.end) / 2;
                        if (event.shiftKey) {

                            // Zoom to next feature with 10% buffer
                            const minimumBases = this.config.minimumBases || 40;
                            const extent = Math.max(minimumBases, 1.1 * (nextFeature.end - nextFeature.start));
                            referenceFrame.start = Math.max(0, newCenter - extent / 2);
                            referenceFrame.end = newCenter + extent / 2;
                            referenceFrame.bpPerPixel = (referenceFrame.end - referenceFrame.start) / viewportWidth;
                        } else {

                            // Center next feature leaving resolution unchanged
                            referenceFrame.shift(newCenter - center);
                        }
                        this.updateViews();
                    } else {

                        // Change in chromosome
                        referenceFrame.chr = nextChr;
                        const newCenter = (nextFeature.start + nextFeature.end) / 2;
                        if (event.shiftKey) {

                            // Zoom to next feature with 10% buffer
                            const minimumBases = this.config.minimumBases || 40;
                            const extent = Math.max(minimumBases, 1.1 * (nextFeature.end - nextFeature.start));
                            referenceFrame.start = Math.max(0, newCenter - extent / 2);
                            referenceFrame.end = referenceFrame.start + extent;
                            referenceFrame.bpPerPixel = (referenceFrame.end - referenceFrame.start) / viewportWidth;
                        } else {

                            // Center next feature leaving resolution unchanged
                            referenceFrame.start = newCenter - (viewportWidth * referenceFrame.bpPerPixel) / 2;
                            referenceFrame.end = referenceFrame.start + viewportWidth * referenceFrame.bpPerPixel;
                        }
                        this.updateViews();
                    }
                }
            }
        }
    }
}

function toggleTrackLabels(trackViews, isVisible) {

    for (let {viewports} of trackViews) {
        for (let viewport of viewports) {
            if (viewport.trackLabelElement) {
                if (0 === viewports.indexOf(viewport) && true === isVisible) {
                    viewport.trackLabelElement.style.display = 'block';
                } else {
                    viewport.trackLabelElement.style.display = 'none';
                }
            }
        }
    }
}

let allBrowsers = [];

/**
 * Create an igv.browser instance.  This object defines the public API for interacting with the genome browser.
 *
 * @param parentDiv - DOM tree root
 * @param config - configuration options.
 *
 */
async function createBrowser(parentDiv, config) {

    if (undefined === config) config = {};

    // Initialize pre-defined genomes.  The genome list is shared among all browser instances
    if (!GenomeUtils.KNOWN_GENOMES) {
        await GenomeUtils.initializeGenomes(config);
    }

    setDefaults(config);

    if (config.queryParametersSupported) {
        extractQuery(config);
    }
    if (config.apiKey) {
        igvxhr$1.setApiKey(config.apiKey);
    }
    if (config.oauthToken) {
        igvxhr$1.setOauthToken(config.oauthToken);
    }
    if (config.clientId && (!isInitialized$2())) {
        await init$2({
            client_id: config.clientId,
            apiKey: config.apiKey,
            scope: 'https://www.googleapis.com/auth/userinfo.profile'
        });
    }

    // Create browser
    const browser = new Browser(config, parentDiv);
    allBrowsers.push(browser);

    const sessionURL = config.sessionURL || config.session || config.hubURL;
    if (sessionURL) {
        await browser.loadSession({
            url: sessionURL
        });
    } else {
        await browser.loadSessionObject(config);
    }

    browser.navbar.navbarDidResize();

    return browser

}

function removeBrowser(browser) {
    browser.dispose();
    browser.root.remove();
    allBrowsers = allBrowsers.filter(item => item !== browser);
}

function removeAllBrowsers() {
    for (let browser of allBrowsers) {
        browser.dispose();
        browser.root.remove();
    }
    allBrowsers = [];
}

/**
 * This function provided so clients can inform igv of a visibility change, typically when an igv instance is
 * made visible from a tab, accordion, or similar widget.
 */
async function visibilityChange() {
    for (let browser of allBrowsers) {
        await browser.visibilityChange();
    }
}

function setDefaults(config) {

    if (undefined === config.minimumBases) {
        config.minimumBases = 40;
    }

    if (undefined === config.showIdeogram) {
        config.showIdeogram = true;
    }

    if (undefined == config.showCytobandNames) {
        config.showCytobandNames = false;
    }

    if (undefined === config.showCircularView) {
        config.showCircularView = false;
    }

    if (undefined === config.showCircularViewButton) {
        config.showCircularViewButton = false;
    }

    if (undefined === config.showTrackLabelButton) {
        config.showTrackLabelButton = true;
    }

    if (undefined === config.showTrackLabels) {
        config.showTrackLabels = true;
    }

    if (undefined === config.showCursorTrackingGuideButton) {
        config.showCursorTrackingGuideButton = true;
    }

    if (undefined === config.showCursorGuide) {
        config.showCursorGuide = config.showCursorTrackingGuide || false;   // showCursorTrackingGuide is a synonym
    }

    if (undefined === config.showCenterGuideButton) {
        config.showCenterGuideButton = true;
    }

    if (undefined === config.showCenterGuide) {
        config.showCenterGuide = false;
    }

    if (undefined === config.showSampleNames) {
        config.showSampleNames = false;
    }

    if (undefined === config.showSVGButton) {
        config.showSVGButton = true;
    }

    if (config.showControls === undefined) {
        config.showControls = true;
    }

    if (config.showNavigation === undefined) {
        config.showNavigation = true;
    }

    if (config.showRuler === undefined) {
        config.showRuler = true;
    }

    if (config.flanking === undefined) {
        config.flanking = 1000;
    }

    if (config.pairsSupported === undefined) {
        config.pairsSupported = true;
    }

    if (!config.tracks) {
        config.tracks = [];
    }

    return config

}


function extractQuery(config) {

    var i1, i2, i, j, s, query, tokens, uri, key, value;

    uri = window.location.href;

    query = {};
    i1 = uri.indexOf("?");
    i2 = uri.lastIndexOf("#");

    let files;
    let indexURLs;
    let names;
    if (i1 >= 0) {
        if (i2 < 0) i2 = uri.length;
        for (i = i1 + 1; i < i2;) {
            j = uri.indexOf("&", i);
            if (j < 0) j = i2;

            s = uri.substring(i, j);
            tokens = s.split("=", 2);

            if (tokens.length === 2) {
                key = tokens[0];
                value = decodeURIComponent(tokens[1]);

                if ('file' === key) {
                    // IGV desktop style file parameter
                    files = value.split(',');
                } else if ('index' === key) {
                    // IGV desktop style index parameter
                    indexURLs = value.split(',');
                } else if ('name' === key) {
                    // IGV desktop style index parameter
                    names = value.split(',');
                } else if ('genome' === key) {
                    if ((value.startsWith("https://") || value.startsWith("http://")) && !value.endsWith(".json")) {
                        // IGV desktop compatibility -- assuming url to fasta
                        config['reference'] = {
                            fastaURL: value,
                            indexURL: value + ".fai"
                        };
                    } else {
                        config[key] = value;
                        config['reference'] = undefined;
                    }
                } else {
                    if ('reference' === key) {
                        config['genome'] = undefined;   // Can specify either reference or genome, not both
                    }
                    config[key] = value;
                }
                i = j + 1;
            } else {
                i++;
            }
        }
    }

    if (files) {
        if (!config.tracks)
            config.tracks = [];
        for (let i = 0; i < files.length; i++) {

            if (files[i].endsWith(".xml") || files[i].endsWith(".json")) {
                config.sessionURL = files[i];
                break
            }

            const trackConfig = {url: files[i]};
            if (indexURLs && indexURLs.length > i) {
                trackConfig.indexURL = indexURLs[i];
            }
            if (names && names.length > i) {
                trackConfig.name = names[i];
            }
            config.tracks.push(trackConfig);
        }
    }

    return query
}


async function createTrack(config, browser) {
    return await Browser.prototype.createTrack.call(browser, config)
}

// Defines the top-level API for the igv module

const setApiKey = igvxhr$1.setApiKey;

function setGoogleOauthToken(accessToken) {
    return igvxhr$1.setOauthToken(accessToken)
}

function setOauthToken(accessToken, host) {
    return igvxhr$1.setOauthToken(accessToken, host)
}

// Backward compatibility
const oauth = igvxhr$1.oauth;

var index = {
    AlertDialog: AlertDialog$1,
    TrackUtils,
    IGVGraphics,
    MenuUtils,
    DataRangeDialog,
    createTrack,
    createBrowser,
    removeBrowser,
    removeAllBrowsers,
    visibilityChange,
    setGoogleOauthToken,
    setOauthToken,
    oauth,
    version: version$1,
    setApiKey,
    TrackBase,
    registerTrackClass,
    registerTrackCreatorFunction,
    registerFileFormats,
    loadSessionFile: Browser.loadSessionFile,
    loadHub
};

//	%23	%24	%25	%26	%27	%28	%29	%2A	%2B	%2C	%2F	%3A	%3B	%3D	%3F	%40	%5B	%5D
const encodings$1 = new Map();
encodings$1.set("!", "%21");
encodings$1.set("#", "%23");
encodings$1.set("$", "%24");
encodings$1.set("%", "%25");
encodings$1.set("&", "%26");
encodings$1.set("'", "%27");
encodings$1.set("(", "%28");
encodings$1.set(")", "%29");
encodings$1.set("*", "%2A");
encodings$1.set("+", "%2B");
encodings$1.set(",", "%2C");
encodings$1.set("/", "%2F");
encodings$1.set(":", "%3A");
encodings$1.set(";", "%3B");
encodings$1.set("=", "%3D");
encodings$1.set("?", "%3F");
encodings$1.set("@", "%40");
encodings$1.set("[", "%5B");
encodings$1.set("]", "%5D");
encodings$1.set(" ", "%20");

// Convenience functions for the gapi oAuth library.

async function init$1(config) {

    if (!(google.accounts.oauth2.initTokenClient)) {
        throw new Error("Google accounts token client not loaded (https://accounts.google.com/gsi/client)")
    }

    if (isInitialized$1()) {
        throw new Error("Google client is already initialized")
    }

    // Note: callback is added when accessToken is requested
    const codeClientConfig = {
        client_id: config.client_id,
        scope: config.scope || 'https://www.googleapis.com/auth/userinfo.profile',
        state: config.state || 'igv',
        error: (err) => {
            throw new Error(err.type)
        },
        hint: config.hint,     // Optional
        hosted_domain: config.hosted_domain  // Optional
    };

    const tokenClient = google.accounts.oauth2.initTokenClient(codeClientConfig);

    // Attach an object to keep igv state
    google.igv = {
        tokenClient: tokenClient,
        apiKey: config.apiKey
    };
}

function isInitialized$1() {
    return window.google && window.google.igv
}

/**
 * Return the current access token if the user is signed in, or undefined otherwise.  This function does not
 * attempt a signIn or request any specfic scopes.
 *
 * @returns access_token || undefined
 */
function getCurrentAccessToken$1() {
    return (isInitialized$1() && google.igv.tokenResponse && Date.now() < google.igv.tokenExpiresAt) ?
        google.igv.tokenResponse.access_token :
        undefined
}

/**
 * Return the of the currently logged in user, if any, as represented by the current access token.  This does not force a login.*
 * @returns {Promise<any>}
 */
async function getCurrentUserProfile() {
    const access_token = getCurrentAccessToken$1();
    if (access_token) {
        const endPoint = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json";
        const response = await fetch(endPoint, {
            headers: {
                'Authorization': `Bearer ${access_token}`
            }
        });
        return response.json()
    } else {
        return undefined
    }
}


/**
 * Return a promise for an access token for the given scope.  If the user hasn't authorized the scope request it
 *
 * @param scope
 * @returns {Promise<unknown>}
 */
async function getAccessToken$1(scope) {

    if (!isInitialized$1()) {
        throw Error("Google oAuth has not been initialized")
    }

    if (google.igv.tokenResponse &&
        Date.now() < google.igv.tokenExpiresAt &&
        google.accounts.oauth2.hasGrantedAllScopes(google.igv.tokenResponse, scope)) {
        return google.igv.tokenResponse.access_token
    } else {
        const tokenClient = google.igv.tokenClient;
        return new Promise((resolve, reject) => {
            try {
                // Settle this promise in the response callback for requestAccessToken()
                tokenClient.callback = (tokenResponse) => {
                    if (tokenResponse.error !== undefined) {
                        reject(tokenResponse);
                    }
                    google.igv.tokenResponse = tokenResponse;
                    google.igv.tokenExpiresAt = Date.now() + tokenResponse.expires_in * 1000;
                    resolve(tokenResponse.access_token);
                };
                tokenClient.requestAccessToken({scope});
            } catch (err) {
            }
        })
    }
}

async function signIn(scope) {

    if (!isInitialized$1()) {
        throw Error("Google oAuth has not been initialized")
    }

    scope = scope || 'https://www.googleapis.com/auth/userinfo.profile';
    await getAccessToken$1(scope);

    if (typeof google.igv.signInListener === 'function') {
        google.igv.signInListener(undefined !== google.igv.tokenResponse);

    }

    return google.igv.tokenResponse

}

async function signOut() {

    if (!isInitialized$1()) {
        throw Error("Google oAuth has not been initialized")
    }

    if (google.igv && google.igv.tokenResponse) {
        google.igv.tokenResponse = undefined;
    }

    if (typeof google.igv.signInListener === 'function') {
        google.igv.signInListener(false);
    }
}

/**
 * Make the target element movable by clicking and dragging on the handle.  This is not a general purprose function,
 * it makes several options specific to igv dialogs, the primary one being that the
 * target is absolutely positioned in pixel coordinates

 */

let dragData;  // Its assumed we are only dragging one element at a time.

function makeDraggable(target, handle, constraint) {

    handle.addEventListener('mousedown', dragStart.bind(target));

    function dragStart(event) {

        event.stopPropagation();
        event.preventDefault();

        const dragFunction = drag.bind(this);
        const dragEndFunction = dragEnd.bind(this);
        const computedStyle = getComputedStyle(this);

        dragData =
            {
                constraint,
                dragFunction,
                dragEndFunction,
                screenX: event.screenX,
                screenY: event.screenY,
                top: parseInt(computedStyle.top.replace("px", "")),
                left: parseInt(computedStyle.left.replace("px", ""))
            };

        document.addEventListener('mousemove', dragFunction);
        document.addEventListener('mouseup', dragEndFunction);
        document.addEventListener('mouseleave', dragEndFunction);
        document.addEventListener('mouseexit', dragEndFunction);
    }
}

function drag(event) {

    if (!dragData) {
        console.error("No drag data!");
        return
    }
    event.stopPropagation();
    event.preventDefault();
    const dx = event.screenX - dragData.screenX;
    const dy = event.screenY - dragData.screenY;

    const left = dragData.left + dx;
    const top = dragData.constraint ? Math.max(dragData.constraint.minY, dragData.top + dy) : dragData.top + dy;

    this.style.left = `${left}px`;
    this.style.top = `${top}px`;
}

function dragEnd(event) {

    if (!dragData) {
        console.error("No drag data!");
        return
    }
    event.stopPropagation();
    event.preventDefault();

    const dragFunction = dragData.dragFunction;
    const dragEndFunction = dragData.dragEndFunction;
    document.removeEventListener('mousemove', dragFunction);
    document.removeEventListener('mouseup', dragEndFunction);
    document.removeEventListener('mouseleave', dragEndFunction);
    document.removeEventListener('mouseexit', dragEndFunction);
    dragData = undefined;
}

function div$1(options) {
    return create("div", options)
}

function create(tag, options) {
    const elem = document.createElement(tag);
    if (options) {
        if (options.class) {
            elem.classList.add(options.class);
        }
        if (options.id) {
            elem.id = options.id;
        }
        if (options.style) {
            applyStyle(elem, options.style);
        }
    }
    return elem
}

function hide(elem) {
    const cssStyle = getComputedStyle(elem);
    if (cssStyle.display !== "none") {
        elem._initialDisplay = cssStyle.display;
    }
    elem.style.display = "none";
}

function show(elem) {
    const currentDisplay = getComputedStyle(elem).display;
    if (currentDisplay === "none") {
        const d = elem._initialDisplay || "block";
        elem.style.display = d;
    }
}

function applyStyle(elem, style) {
    for (let key of Object.keys(style)) {
        elem.style[key] = style[key];
    }
}

function guid() {
    return ("0000" + (Math.random() * Math.pow(36, 4) << 0).toString(36)).slice(-4)
}

const httpMessages =
    {
        "401": "Access unauthorized",
        "403": "Access forbidden",
        "404": "Not found"
    };


class AlertDialog {
    constructor(parent) {

        // container
        this.container = div$1({class: "igv-widgets-alert-dialog-container"});
        parent.appendChild(this.container);
        this.container.setAttribute('tabIndex', '-1');

        // header
        const header = div$1();
        this.container.appendChild(header);

        this.errorHeadline = div$1();
        header.appendChild(this.errorHeadline);
        this.errorHeadline.textContent = '';

        // body container
        let bodyContainer = div$1({id: 'igv-widgets-alert-dialog-body'});
        this.container.appendChild(bodyContainer);

        // body copy
        this.body = div$1({id: 'igv-widgets-alert-dialog-body-copy'});
        bodyContainer.appendChild(this.body);

        // ok container
        let ok_container = div$1();
        this.container.appendChild(ok_container);

        // ok
        this.ok = div$1();
        ok_container.appendChild(this.ok);
        this.ok.textContent = 'OK';

        const okHandler = () => {

            if (typeof this.callback === 'function') {
                this.callback("OK");
                this.callback = undefined;
            }
            this.body.innerHTML = '';
            hide(this.container);
        };

        this.ok.addEventListener('click', event => {

            event.stopPropagation();

            okHandler();
        });

        this.container.addEventListener('keypress', event => {

            event.stopPropagation();

            if ('Enter' === event.key) {
                okHandler();
            }
        });

        makeDraggable(this.container, header);

        hide(this.container);
    }

    present(alert, callback) {

        this.errorHeadline.textContent = alert.message ? 'ERROR' : '';
        let string = alert.message || alert;

        if (httpMessages.hasOwnProperty(string)) {
            string = httpMessages[string];
        }

        this.body.innerHTML = string;
        this.callback = callback;
        show(this.container);
        this.container.focus();
    }
}

class AlertSingleton {
    constructor() {
        if (AlertSingleton.instance) {
            return AlertSingleton.instance;
        }
        
        this.alertDialog = undefined;
        AlertSingleton.instance = this;
    }

    init(root) {
        if (!this.alertDialog) {
            this.alertDialog = new AlertDialog(root);
        }
    }

    present(alert, callback) {
        if (!this.alertDialog) {
            throw new Error('AlertSingleton must be initialized with init() before use');
        }
        this.alertDialog.present(alert, callback);
    }
}

// Create and export a single instance
const alertSingleton = new AlertSingleton();

/**
 * Covers string literals and String objects
 * @param x
 * @returns {boolean}
 */
function isString(x) {
    return typeof x === "string" || x instanceof String
}

function getExtension(url) {

    if (undefined === url) {
        return undefined
    }

    let path = (isFilePath(url) || url.google_url) ? url.name : url;
    let filename = path.toLowerCase();

    //Strip parameters -- handle local files later
    let index = filename.indexOf("?");
    if (index > 0) {
        filename = filename.substr(0, index);
    }

    //Strip aux extensions .gz, .tab, and .txt
    if (filename.endsWith(".gz")) {
        filename = filename.substr(0, filename.length - 3);
    } else if (filename.endsWith(".txt") || filename.endsWith(".tab") || filename.endsWith(".bgz")) {
        filename = filename.substr(0, filename.length - 4);
    }

    index = filename.lastIndexOf(".");

    return index < 0 ? filename : filename.substr(1 + index)
}


/**
 * Test if object is a File or File-like object.
 *
 * @param object
 */
function isFile(object) {
    if(!object) {
        return false;
    }
    return typeof object !== 'function' &&
        (object instanceof File ||
            (object.hasOwnProperty("name") && typeof object.slice === 'function' && typeof object.arrayBuffer === 'function'))
}

const isFilePath = isFile;    // deprecated

function download(filename, data) {

    const element = document.createElement('a');
    element.setAttribute('href', data);
    element.setAttribute('download', filename);
    element.style.display = 'none';
    document.body.appendChild(element);
    element.click();
    document.body.removeChild(element);
}

if (typeof process === 'object' && typeof window === 'undefined') {
    global.atob = function (str) {
        return Buffer.from(str, 'base64').toString('binary');
    };
}


function parseUri(str) {

    var o = options,
        m = o.parser["loose"].exec(str),
        uri = {},
        i = 14;

    while (i--) uri[o.key[i]] = m[i] || "";

    uri[o.q.name] = {};
    uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
        if ($1) uri[o.q.name][$1] = $2;
    });

    return uri;
}

const options = {
    strictMode: false,
    key: ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "anchor"],
    q: {
        name: "queryKey",
        parser: /(?:^|&)([^&=]*)=?([^&]*)/g
    },
    parser: {
        strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
        loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
    }
};

/*! pako 2.1.0 https://github.com/nodeca/pako @license (MIT AND Zlib) */
// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

/* eslint-disable space-unary-ops */

/* Public constants ==========================================================*/
/* ===========================================================================*/


//const Z_FILTERED          = 1;
//const Z_HUFFMAN_ONLY      = 2;
//const Z_RLE               = 3;
const Z_FIXED$1               = 4;
//const Z_DEFAULT_STRATEGY  = 0;

/* Possible values of the data_type field (though see inflate()) */
const Z_BINARY              = 0;
const Z_TEXT                = 1;
//const Z_ASCII             = 1; // = Z_TEXT
const Z_UNKNOWN$1             = 2;

/*============================================================================*/


function zero$1(buf) { let len = buf.length; while (--len >= 0) { buf[len] = 0; } }

// From zutil.h

const STORED_BLOCK = 0;
const STATIC_TREES = 1;
const DYN_TREES    = 2;
/* The three kinds of block type */

const MIN_MATCH$1    = 3;
const MAX_MATCH$1    = 258;
/* The minimum and maximum match lengths */

// From deflate.h
/* ===========================================================================
 * Internal compression state.
 */

const LENGTH_CODES$1  = 29;
/* number of length codes, not counting the special END_BLOCK code */

const LITERALS$1      = 256;
/* number of literal bytes 0..255 */

const L_CODES$1       = LITERALS$1 + 1 + LENGTH_CODES$1;
/* number of Literal or Length codes, including the END_BLOCK code */

const D_CODES$1       = 30;
/* number of distance codes */

const BL_CODES$1      = 19;
/* number of codes used to transfer the bit lengths */

const HEAP_SIZE$1     = 2 * L_CODES$1 + 1;
/* maximum heap size */

const MAX_BITS$1      = 15;
/* All codes must not exceed MAX_BITS bits */

const Buf_size      = 16;
/* size of bit buffer in bi_buf */


/* ===========================================================================
 * Constants
 */

const MAX_BL_BITS = 7;
/* Bit length codes must not exceed MAX_BL_BITS bits */

const END_BLOCK   = 256;
/* end of block literal code */

const REP_3_6     = 16;
/* repeat previous bit length 3-6 times (2 bits of repeat count) */

const REPZ_3_10   = 17;
/* repeat a zero length 3-10 times  (3 bits of repeat count) */

const REPZ_11_138 = 18;
/* repeat a zero length 11-138 times  (7 bits of repeat count) */

/* eslint-disable comma-spacing,array-bracket-spacing */
const extra_lbits =   /* extra bits for each length code */
  new Uint8Array([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0]);

const extra_dbits =   /* extra bits for each distance code */
  new Uint8Array([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13]);

const extra_blbits =  /* extra bits for each bit length code */
  new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7]);

const bl_order =
  new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]);
/* eslint-enable comma-spacing,array-bracket-spacing */

/* The lengths of the bit length codes are sent in order of decreasing
 * probability, to avoid transmitting the lengths for unused bit length codes.
 */

/* ===========================================================================
 * Local data. These are initialized only once.
 */

// We pre-fill arrays with 0 to avoid uninitialized gaps

const DIST_CODE_LEN = 512; /* see definition of array dist_code below */

// !!!! Use flat array instead of structure, Freq = i*2, Len = i*2+1
const static_ltree  = new Array((L_CODES$1 + 2) * 2);
zero$1(static_ltree);
/* The static literal tree. Since the bit lengths are imposed, there is no
 * need for the L_CODES extra codes used during heap construction. However
 * The codes 286 and 287 are needed to build a canonical tree (see _tr_init
 * below).
 */

const static_dtree  = new Array(D_CODES$1 * 2);
zero$1(static_dtree);
/* The static distance tree. (Actually a trivial tree since all codes use
 * 5 bits.)
 */

const _dist_code    = new Array(DIST_CODE_LEN);
zero$1(_dist_code);
/* Distance codes. The first 256 values correspond to the distances
 * 3 .. 258, the last 256 values correspond to the top 8 bits of
 * the 15 bit distances.
 */

const _length_code  = new Array(MAX_MATCH$1 - MIN_MATCH$1 + 1);
zero$1(_length_code);
/* length code for each normalized match length (0 == MIN_MATCH) */

const base_length   = new Array(LENGTH_CODES$1);
zero$1(base_length);
/* First normalized length for each code (0 = MIN_MATCH) */

const base_dist     = new Array(D_CODES$1);
zero$1(base_dist);
/* First normalized distance for each code (0 = distance of 1) */


function StaticTreeDesc(static_tree, extra_bits, extra_base, elems, max_length) {

  this.static_tree  = static_tree;  /* static tree or NULL */
  this.extra_bits   = extra_bits;   /* extra bits for each code or NULL */
  this.extra_base   = extra_base;   /* base index for extra_bits */
  this.elems        = elems;        /* max number of elements in the tree */
  this.max_length   = max_length;   /* max bit length for the codes */

  // show if `static_tree` has data or dummy - needed for monomorphic objects
  this.has_stree    = static_tree && static_tree.length;
}


let static_l_desc;
let static_d_desc;
let static_bl_desc;


function TreeDesc(dyn_tree, stat_desc) {
  this.dyn_tree = dyn_tree;     /* the dynamic tree */
  this.max_code = 0;            /* largest code with non zero frequency */
  this.stat_desc = stat_desc;   /* the corresponding static tree */
}



const d_code = (dist) => {

  return dist < 256 ? _dist_code[dist] : _dist_code[256 + (dist >>> 7)];
};


/* ===========================================================================
 * Output a short LSB first on the stream.
 * IN assertion: there is enough room in pendingBuf.
 */
const put_short = (s, w) => {
//    put_byte(s, (uch)((w) & 0xff));
//    put_byte(s, (uch)((ush)(w) >> 8));
  s.pending_buf[s.pending++] = (w) & 0xff;
  s.pending_buf[s.pending++] = (w >>> 8) & 0xff;
};


/* ===========================================================================
 * Send a value on a given number of bits.
 * IN assertion: length <= 16 and value fits in length bits.
 */
const send_bits = (s, value, length) => {

  if (s.bi_valid > (Buf_size - length)) {
    s.bi_buf |= (value << s.bi_valid) & 0xffff;
    put_short(s, s.bi_buf);
    s.bi_buf = value >> (Buf_size - s.bi_valid);
    s.bi_valid += length - Buf_size;
  } else {
    s.bi_buf |= (value << s.bi_valid) & 0xffff;
    s.bi_valid += length;
  }
};


const send_code = (s, c, tree) => {

  send_bits(s, tree[c * 2]/*.Code*/, tree[c * 2 + 1]/*.Len*/);
};


/* ===========================================================================
 * Reverse the first len bits of a code, using straightforward code (a faster
 * method would use a table)
 * IN assertion: 1 <= len <= 15
 */
const bi_reverse = (code, len) => {

  let res = 0;
  do {
    res |= code & 1;
    code >>>= 1;
    res <<= 1;
  } while (--len > 0);
  return res >>> 1;
};


/* ===========================================================================
 * Flush the bit buffer, keeping at most 7 bits in it.
 */
const bi_flush = (s) => {

  if (s.bi_valid === 16) {
    put_short(s, s.bi_buf);
    s.bi_buf = 0;
    s.bi_valid = 0;

  } else if (s.bi_valid >= 8) {
    s.pending_buf[s.pending++] = s.bi_buf & 0xff;
    s.bi_buf >>= 8;
    s.bi_valid -= 8;
  }
};


/* ===========================================================================
 * Compute the optimal bit lengths for a tree and update the total bit length
 * for the current block.
 * IN assertion: the fields freq and dad are set, heap[heap_max] and
 *    above are the tree nodes sorted by increasing frequency.
 * OUT assertions: the field len is set to the optimal bit length, the
 *     array bl_count contains the frequencies for each bit length.
 *     The length opt_len is updated; static_len is also updated if stree is
 *     not null.
 */
const gen_bitlen = (s, desc) => {
//    deflate_state *s;
//    tree_desc *desc;    /* the tree descriptor */

  const tree            = desc.dyn_tree;
  const max_code        = desc.max_code;
  const stree           = desc.stat_desc.static_tree;
  const has_stree       = desc.stat_desc.has_stree;
  const extra           = desc.stat_desc.extra_bits;
  const base            = desc.stat_desc.extra_base;
  const max_length      = desc.stat_desc.max_length;
  let h;              /* heap index */
  let n, m;           /* iterate over the tree elements */
  let bits;           /* bit length */
  let xbits;          /* extra bits */
  let f;              /* frequency */
  let overflow = 0;   /* number of elements with bit length too large */

  for (bits = 0; bits <= MAX_BITS$1; bits++) {
    s.bl_count[bits] = 0;
  }

  /* In a first pass, compute the optimal bit lengths (which may
   * overflow in the case of the bit length tree).
   */
  tree[s.heap[s.heap_max] * 2 + 1]/*.Len*/ = 0; /* root of the heap */

  for (h = s.heap_max + 1; h < HEAP_SIZE$1; h++) {
    n = s.heap[h];
    bits = tree[tree[n * 2 + 1]/*.Dad*/ * 2 + 1]/*.Len*/ + 1;
    if (bits > max_length) {
      bits = max_length;
      overflow++;
    }
    tree[n * 2 + 1]/*.Len*/ = bits;
    /* We overwrite tree[n].Dad which is no longer needed */

    if (n > max_code) { continue; } /* not a leaf node */

    s.bl_count[bits]++;
    xbits = 0;
    if (n >= base) {
      xbits = extra[n - base];
    }
    f = tree[n * 2]/*.Freq*/;
    s.opt_len += f * (bits + xbits);
    if (has_stree) {
      s.static_len += f * (stree[n * 2 + 1]/*.Len*/ + xbits);
    }
  }
  if (overflow === 0) { return; }

  // Tracev((stderr,"\nbit length overflow\n"));
  /* This happens for example on obj2 and pic of the Calgary corpus */

  /* Find the first bit length which could increase: */
  do {
    bits = max_length - 1;
    while (s.bl_count[bits] === 0) { bits--; }
    s.bl_count[bits]--;      /* move one leaf down the tree */
    s.bl_count[bits + 1] += 2; /* move one overflow item as its brother */
    s.bl_count[max_length]--;
    /* The brother of the overflow item also moves one step up,
     * but this does not affect bl_count[max_length]
     */
    overflow -= 2;
  } while (overflow > 0);

  /* Now recompute all bit lengths, scanning in increasing frequency.
   * h is still equal to HEAP_SIZE. (It is simpler to reconstruct all
   * lengths instead of fixing only the wrong ones. This idea is taken
   * from 'ar' written by Haruhiko Okumura.)
   */
  for (bits = max_length; bits !== 0; bits--) {
    n = s.bl_count[bits];
    while (n !== 0) {
      m = s.heap[--h];
      if (m > max_code) { continue; }
      if (tree[m * 2 + 1]/*.Len*/ !== bits) {
        // Tracev((stderr,"code %d bits %d->%d\n", m, tree[m].Len, bits));
        s.opt_len += (bits - tree[m * 2 + 1]/*.Len*/) * tree[m * 2]/*.Freq*/;
        tree[m * 2 + 1]/*.Len*/ = bits;
      }
      n--;
    }
  }
};


/* ===========================================================================
 * Generate the codes for a given tree and bit counts (which need not be
 * optimal).
 * IN assertion: the array bl_count contains the bit length statistics for
 * the given tree and the field len is set for all tree elements.
 * OUT assertion: the field code is set for all tree elements of non
 *     zero code length.
 */
const gen_codes = (tree, max_code, bl_count) => {
//    ct_data *tree;             /* the tree to decorate */
//    int max_code;              /* largest code with non zero frequency */
//    ushf *bl_count;            /* number of codes at each bit length */

  const next_code = new Array(MAX_BITS$1 + 1); /* next code value for each bit length */
  let code = 0;              /* running code value */
  let bits;                  /* bit index */
  let n;                     /* code index */

  /* The distribution counts are first used to generate the code values
   * without bit reversal.
   */
  for (bits = 1; bits <= MAX_BITS$1; bits++) {
    code = (code + bl_count[bits - 1]) << 1;
    next_code[bits] = code;
  }
  /* Check that the bit counts in bl_count are consistent. The last code
   * must be all ones.
   */
  //Assert (code + bl_count[MAX_BITS]-1 == (1<<MAX_BITS)-1,
  //        "inconsistent bit counts");
  //Tracev((stderr,"\ngen_codes: max_code %d ", max_code));

  for (n = 0;  n <= max_code; n++) {
    let len = tree[n * 2 + 1]/*.Len*/;
    if (len === 0) { continue; }
    /* Now reverse the bits */
    tree[n * 2]/*.Code*/ = bi_reverse(next_code[len]++, len);

    //Tracecv(tree != static_ltree, (stderr,"\nn %3d %c l %2d c %4x (%x) ",
    //     n, (isgraph(n) ? n : ' '), len, tree[n].Code, next_code[len]-1));
  }
};


/* ===========================================================================
 * Initialize the various 'constant' tables.
 */
const tr_static_init = () => {

  let n;        /* iterates over tree elements */
  let bits;     /* bit counter */
  let length;   /* length value */
  let code;     /* code value */
  let dist;     /* distance index */
  const bl_count = new Array(MAX_BITS$1 + 1);
  /* number of codes at each bit length for an optimal tree */

  // do check in _tr_init()
  //if (static_init_done) return;

  /* For some embedded targets, global variables are not initialized: */
/*#ifdef NO_INIT_GLOBAL_POINTERS
  static_l_desc.static_tree = static_ltree;
  static_l_desc.extra_bits = extra_lbits;
  static_d_desc.static_tree = static_dtree;
  static_d_desc.extra_bits = extra_dbits;
  static_bl_desc.extra_bits = extra_blbits;
#endif*/

  /* Initialize the mapping length (0..255) -> length code (0..28) */
  length = 0;
  for (code = 0; code < LENGTH_CODES$1 - 1; code++) {
    base_length[code] = length;
    for (n = 0; n < (1 << extra_lbits[code]); n++) {
      _length_code[length++] = code;
    }
  }
  //Assert (length == 256, "tr_static_init: length != 256");
  /* Note that the length 255 (match length 258) can be represented
   * in two different ways: code 284 + 5 bits or code 285, so we
   * overwrite length_code[255] to use the best encoding:
   */
  _length_code[length - 1] = code;

  /* Initialize the mapping dist (0..32K) -> dist code (0..29) */
  dist = 0;
  for (code = 0; code < 16; code++) {
    base_dist[code] = dist;
    for (n = 0; n < (1 << extra_dbits[code]); n++) {
      _dist_code[dist++] = code;
    }
  }
  //Assert (dist == 256, "tr_static_init: dist != 256");
  dist >>= 7; /* from now on, all distances are divided by 128 */
  for (; code < D_CODES$1; code++) {
    base_dist[code] = dist << 7;
    for (n = 0; n < (1 << (extra_dbits[code] - 7)); n++) {
      _dist_code[256 + dist++] = code;
    }
  }
  //Assert (dist == 256, "tr_static_init: 256+dist != 512");

  /* Construct the codes of the static literal tree */
  for (bits = 0; bits <= MAX_BITS$1; bits++) {
    bl_count[bits] = 0;
  }

  n = 0;
  while (n <= 143) {
    static_ltree[n * 2 + 1]/*.Len*/ = 8;
    n++;
    bl_count[8]++;
  }
  while (n <= 255) {
    static_ltree[n * 2 + 1]/*.Len*/ = 9;
    n++;
    bl_count[9]++;
  }
  while (n <= 279) {
    static_ltree[n * 2 + 1]/*.Len*/ = 7;
    n++;
    bl_count[7]++;
  }
  while (n <= 287) {
    static_ltree[n * 2 + 1]/*.Len*/ = 8;
    n++;
    bl_count[8]++;
  }
  /* Codes 286 and 287 do not exist, but we must include them in the
   * tree construction to get a canonical Huffman tree (longest code
   * all ones)
   */
  gen_codes(static_ltree, L_CODES$1 + 1, bl_count);

  /* The static distance tree is trivial: */
  for (n = 0; n < D_CODES$1; n++) {
    static_dtree[n * 2 + 1]/*.Len*/ = 5;
    static_dtree[n * 2]/*.Code*/ = bi_reverse(n, 5);
  }

  // Now data ready and we can init static trees
  static_l_desc = new StaticTreeDesc(static_ltree, extra_lbits, LITERALS$1 + 1, L_CODES$1, MAX_BITS$1);
  static_d_desc = new StaticTreeDesc(static_dtree, extra_dbits, 0,          D_CODES$1, MAX_BITS$1);
  static_bl_desc = new StaticTreeDesc(new Array(0), extra_blbits, 0,         BL_CODES$1, MAX_BL_BITS);

  //static_init_done = true;
};


/* ===========================================================================
 * Initialize a new block.
 */
const init_block = (s) => {

  let n; /* iterates over tree elements */

  /* Initialize the trees. */
  for (n = 0; n < L_CODES$1;  n++) { s.dyn_ltree[n * 2]/*.Freq*/ = 0; }
  for (n = 0; n < D_CODES$1;  n++) { s.dyn_dtree[n * 2]/*.Freq*/ = 0; }
  for (n = 0; n < BL_CODES$1; n++) { s.bl_tree[n * 2]/*.Freq*/ = 0; }

  s.dyn_ltree[END_BLOCK * 2]/*.Freq*/ = 1;
  s.opt_len = s.static_len = 0;
  s.sym_next = s.matches = 0;
};


/* ===========================================================================
 * Flush the bit buffer and align the output on a byte boundary
 */
const bi_windup = (s) =>
{
  if (s.bi_valid > 8) {
    put_short(s, s.bi_buf);
  } else if (s.bi_valid > 0) {
    //put_byte(s, (Byte)s->bi_buf);
    s.pending_buf[s.pending++] = s.bi_buf;
  }
  s.bi_buf = 0;
  s.bi_valid = 0;
};

/* ===========================================================================
 * Compares to subtrees, using the tree depth as tie breaker when
 * the subtrees have equal frequency. This minimizes the worst case length.
 */
const smaller = (tree, n, m, depth) => {

  const _n2 = n * 2;
  const _m2 = m * 2;
  return (tree[_n2]/*.Freq*/ < tree[_m2]/*.Freq*/ ||
         (tree[_n2]/*.Freq*/ === tree[_m2]/*.Freq*/ && depth[n] <= depth[m]));
};

/* ===========================================================================
 * Restore the heap property by moving down the tree starting at node k,
 * exchanging a node with the smallest of its two sons if necessary, stopping
 * when the heap property is re-established (each father smaller than its
 * two sons).
 */
const pqdownheap = (s, tree, k) => {
//    deflate_state *s;
//    ct_data *tree;  /* the tree to restore */
//    int k;               /* node to move down */

  const v = s.heap[k];
  let j = k << 1;  /* left son of k */
  while (j <= s.heap_len) {
    /* Set j to the smallest of the two sons: */
    if (j < s.heap_len &&
      smaller(tree, s.heap[j + 1], s.heap[j], s.depth)) {
      j++;
    }
    /* Exit if v is smaller than both sons */
    if (smaller(tree, v, s.heap[j], s.depth)) { break; }

    /* Exchange v with the smallest son */
    s.heap[k] = s.heap[j];
    k = j;

    /* And continue down the tree, setting j to the left son of k */
    j <<= 1;
  }
  s.heap[k] = v;
};


// inlined manually
// const SMALLEST = 1;

/* ===========================================================================
 * Send the block data compressed using the given Huffman trees
 */
const compress_block = (s, ltree, dtree) => {
//    deflate_state *s;
//    const ct_data *ltree; /* literal tree */
//    const ct_data *dtree; /* distance tree */

  let dist;           /* distance of matched string */
  let lc;             /* match length or unmatched char (if dist == 0) */
  let sx = 0;         /* running index in sym_buf */
  let code;           /* the code to send */
  let extra;          /* number of extra bits to send */

  if (s.sym_next !== 0) {
    do {
      dist = s.pending_buf[s.sym_buf + sx++] & 0xff;
      dist += (s.pending_buf[s.sym_buf + sx++] & 0xff) << 8;
      lc = s.pending_buf[s.sym_buf + sx++];
      if (dist === 0) {
        send_code(s, lc, ltree); /* send a literal byte */
        //Tracecv(isgraph(lc), (stderr," '%c' ", lc));
      } else {
        /* Here, lc is the match length - MIN_MATCH */
        code = _length_code[lc];
        send_code(s, code + LITERALS$1 + 1, ltree); /* send the length code */
        extra = extra_lbits[code];
        if (extra !== 0) {
          lc -= base_length[code];
          send_bits(s, lc, extra);       /* send the extra length bits */
        }
        dist--; /* dist is now the match distance - 1 */
        code = d_code(dist);
        //Assert (code < D_CODES, "bad d_code");

        send_code(s, code, dtree);       /* send the distance code */
        extra = extra_dbits[code];
        if (extra !== 0) {
          dist -= base_dist[code];
          send_bits(s, dist, extra);   /* send the extra distance bits */
        }
      } /* literal or match pair ? */

      /* Check that the overlay between pending_buf and sym_buf is ok: */
      //Assert(s->pending < s->lit_bufsize + sx, "pendingBuf overflow");

    } while (sx < s.sym_next);
  }

  send_code(s, END_BLOCK, ltree);
};


/* ===========================================================================
 * Construct one Huffman tree and assigns the code bit strings and lengths.
 * Update the total bit length for the current block.
 * IN assertion: the field freq is set for all tree elements.
 * OUT assertions: the fields len and code are set to the optimal bit length
 *     and corresponding code. The length opt_len is updated; static_len is
 *     also updated if stree is not null. The field max_code is set.
 */
const build_tree = (s, desc) => {
//    deflate_state *s;
//    tree_desc *desc; /* the tree descriptor */

  const tree     = desc.dyn_tree;
  const stree    = desc.stat_desc.static_tree;
  const has_stree = desc.stat_desc.has_stree;
  const elems    = desc.stat_desc.elems;
  let n, m;          /* iterate over heap elements */
  let max_code = -1; /* largest code with non zero frequency */
  let node;          /* new node being created */

  /* Construct the initial heap, with least frequent element in
   * heap[SMALLEST]. The sons of heap[n] are heap[2*n] and heap[2*n+1].
   * heap[0] is not used.
   */
  s.heap_len = 0;
  s.heap_max = HEAP_SIZE$1;

  for (n = 0; n < elems; n++) {
    if (tree[n * 2]/*.Freq*/ !== 0) {
      s.heap[++s.heap_len] = max_code = n;
      s.depth[n] = 0;

    } else {
      tree[n * 2 + 1]/*.Len*/ = 0;
    }
  }

  /* The pkzip format requires that at least one distance code exists,
   * and that at least one bit should be sent even if there is only one
   * possible code. So to avoid special checks later on we force at least
   * two codes of non zero frequency.
   */
  while (s.heap_len < 2) {
    node = s.heap[++s.heap_len] = (max_code < 2 ? ++max_code : 0);
    tree[node * 2]/*.Freq*/ = 1;
    s.depth[node] = 0;
    s.opt_len--;

    if (has_stree) {
      s.static_len -= stree[node * 2 + 1]/*.Len*/;
    }
    /* node is 0 or 1 so it does not have extra bits */
  }
  desc.max_code = max_code;

  /* The elements heap[heap_len/2+1 .. heap_len] are leaves of the tree,
   * establish sub-heaps of increasing lengths:
   */
  for (n = (s.heap_len >> 1/*int /2*/); n >= 1; n--) { pqdownheap(s, tree, n); }

  /* Construct the Huffman tree by repeatedly combining the least two
   * frequent nodes.
   */
  node = elems;              /* next internal node of the tree */
  do {
    //pqremove(s, tree, n);  /* n = node of least frequency */
    /*** pqremove ***/
    n = s.heap[1/*SMALLEST*/];
    s.heap[1/*SMALLEST*/] = s.heap[s.heap_len--];
    pqdownheap(s, tree, 1/*SMALLEST*/);
    /***/

    m = s.heap[1/*SMALLEST*/]; /* m = node of next least frequency */

    s.heap[--s.heap_max] = n; /* keep the nodes sorted by frequency */
    s.heap[--s.heap_max] = m;

    /* Create a new node father of n and m */
    tree[node * 2]/*.Freq*/ = tree[n * 2]/*.Freq*/ + tree[m * 2]/*.Freq*/;
    s.depth[node] = (s.depth[n] >= s.depth[m] ? s.depth[n] : s.depth[m]) + 1;
    tree[n * 2 + 1]/*.Dad*/ = tree[m * 2 + 1]/*.Dad*/ = node;

    /* and insert the new node in the heap */
    s.heap[1/*SMALLEST*/] = node++;
    pqdownheap(s, tree, 1/*SMALLEST*/);

  } while (s.heap_len >= 2);

  s.heap[--s.heap_max] = s.heap[1/*SMALLEST*/];

  /* At this point, the fields freq and dad are set. We can now
   * generate the bit lengths.
   */
  gen_bitlen(s, desc);

  /* The field len is now set, we can generate the bit codes */
  gen_codes(tree, max_code, s.bl_count);
};


/* ===========================================================================
 * Scan a literal or distance tree to determine the frequencies of the codes
 * in the bit length tree.
 */
const scan_tree = (s, tree, max_code) => {
//    deflate_state *s;
//    ct_data *tree;   /* the tree to be scanned */
//    int max_code;    /* and its largest code of non zero frequency */

  let n;                     /* iterates over all tree elements */
  let prevlen = -1;          /* last emitted length */
  let curlen;                /* length of current code */

  let nextlen = tree[0 * 2 + 1]/*.Len*/; /* length of next code */

  let count = 0;             /* repeat count of the current code */
  let max_count = 7;         /* max repeat count */
  let min_count = 4;         /* min repeat count */

  if (nextlen === 0) {
    max_count = 138;
    min_count = 3;
  }
  tree[(max_code + 1) * 2 + 1]/*.Len*/ = 0xffff; /* guard */

  for (n = 0; n <= max_code; n++) {
    curlen = nextlen;
    nextlen = tree[(n + 1) * 2 + 1]/*.Len*/;

    if (++count < max_count && curlen === nextlen) {
      continue;

    } else if (count < min_count) {
      s.bl_tree[curlen * 2]/*.Freq*/ += count;

    } else if (curlen !== 0) {

      if (curlen !== prevlen) { s.bl_tree[curlen * 2]/*.Freq*/++; }
      s.bl_tree[REP_3_6 * 2]/*.Freq*/++;

    } else if (count <= 10) {
      s.bl_tree[REPZ_3_10 * 2]/*.Freq*/++;

    } else {
      s.bl_tree[REPZ_11_138 * 2]/*.Freq*/++;
    }

    count = 0;
    prevlen = curlen;

    if (nextlen === 0) {
      max_count = 138;
      min_count = 3;

    } else if (curlen === nextlen) {
      max_count = 6;
      min_count = 3;

    } else {
      max_count = 7;
      min_count = 4;
    }
  }
};


/* ===========================================================================
 * Send a literal or distance tree in compressed form, using the codes in
 * bl_tree.
 */
const send_tree = (s, tree, max_code) => {
//    deflate_state *s;
//    ct_data *tree; /* the tree to be scanned */
//    int max_code;       /* and its largest code of non zero frequency */

  let n;                     /* iterates over all tree elements */
  let prevlen = -1;          /* last emitted length */
  let curlen;                /* length of current code */

  let nextlen = tree[0 * 2 + 1]/*.Len*/; /* length of next code */

  let count = 0;             /* repeat count of the current code */
  let max_count = 7;         /* max repeat count */
  let min_count = 4;         /* min repeat count */

  /* tree[max_code+1].Len = -1; */  /* guard already set */
  if (nextlen === 0) {
    max_count = 138;
    min_count = 3;
  }

  for (n = 0; n <= max_code; n++) {
    curlen = nextlen;
    nextlen = tree[(n + 1) * 2 + 1]/*.Len*/;

    if (++count < max_count && curlen === nextlen) {
      continue;

    } else if (count < min_count) {
      do { send_code(s, curlen, s.bl_tree); } while (--count !== 0);

    } else if (curlen !== 0) {
      if (curlen !== prevlen) {
        send_code(s, curlen, s.bl_tree);
        count--;
      }
      //Assert(count >= 3 && count <= 6, " 3_6?");
      send_code(s, REP_3_6, s.bl_tree);
      send_bits(s, count - 3, 2);

    } else if (count <= 10) {
      send_code(s, REPZ_3_10, s.bl_tree);
      send_bits(s, count - 3, 3);

    } else {
      send_code(s, REPZ_11_138, s.bl_tree);
      send_bits(s, count - 11, 7);
    }

    count = 0;
    prevlen = curlen;
    if (nextlen === 0) {
      max_count = 138;
      min_count = 3;

    } else if (curlen === nextlen) {
      max_count = 6;
      min_count = 3;

    } else {
      max_count = 7;
      min_count = 4;
    }
  }
};


/* ===========================================================================
 * Construct the Huffman tree for the bit lengths and return the index in
 * bl_order of the last bit length code to send.
 */
const build_bl_tree = (s) => {

  let max_blindex;  /* index of last bit length code of non zero freq */

  /* Determine the bit length frequencies for literal and distance trees */
  scan_tree(s, s.dyn_ltree, s.l_desc.max_code);
  scan_tree(s, s.dyn_dtree, s.d_desc.max_code);

  /* Build the bit length tree: */
  build_tree(s, s.bl_desc);
  /* opt_len now includes the length of the tree representations, except
   * the lengths of the bit lengths codes and the 5+5+4 bits for the counts.
   */

  /* Determine the number of bit length codes to send. The pkzip format
   * requires that at least 4 bit length codes be sent. (appnote.txt says
   * 3 but the actual value used is 4.)
   */
  for (max_blindex = BL_CODES$1 - 1; max_blindex >= 3; max_blindex--) {
    if (s.bl_tree[bl_order[max_blindex] * 2 + 1]/*.Len*/ !== 0) {
      break;
    }
  }
  /* Update opt_len to include the bit length tree and counts */
  s.opt_len += 3 * (max_blindex + 1) + 5 + 5 + 4;
  //Tracev((stderr, "\ndyn trees: dyn %ld, stat %ld",
  //        s->opt_len, s->static_len));

  return max_blindex;
};


/* ===========================================================================
 * Send the header for a block using dynamic Huffman trees: the counts, the
 * lengths of the bit length codes, the literal tree and the distance tree.
 * IN assertion: lcodes >= 257, dcodes >= 1, blcodes >= 4.
 */
const send_all_trees = (s, lcodes, dcodes, blcodes) => {
//    deflate_state *s;
//    int lcodes, dcodes, blcodes; /* number of codes for each tree */

  let rank;                    /* index in bl_order */

  //Assert (lcodes >= 257 && dcodes >= 1 && blcodes >= 4, "not enough codes");
  //Assert (lcodes <= L_CODES && dcodes <= D_CODES && blcodes <= BL_CODES,
  //        "too many codes");
  //Tracev((stderr, "\nbl counts: "));
  send_bits(s, lcodes - 257, 5); /* not +255 as stated in appnote.txt */
  send_bits(s, dcodes - 1,   5);
  send_bits(s, blcodes - 4,  4); /* not -3 as stated in appnote.txt */
  for (rank = 0; rank < blcodes; rank++) {
    //Tracev((stderr, "\nbl code %2d ", bl_order[rank]));
    send_bits(s, s.bl_tree[bl_order[rank] * 2 + 1]/*.Len*/, 3);
  }
  //Tracev((stderr, "\nbl tree: sent %ld", s->bits_sent));

  send_tree(s, s.dyn_ltree, lcodes - 1); /* literal tree */
  //Tracev((stderr, "\nlit tree: sent %ld", s->bits_sent));

  send_tree(s, s.dyn_dtree, dcodes - 1); /* distance tree */
  //Tracev((stderr, "\ndist tree: sent %ld", s->bits_sent));
};


/* ===========================================================================
 * Check if the data type is TEXT or BINARY, using the following algorithm:
 * - TEXT if the two conditions below are satisfied:
 *    a) There are no non-portable control characters belonging to the
 *       "block list" (0..6, 14..25, 28..31).
 *    b) There is at least one printable character belonging to the
 *       "allow list" (9 {TAB}, 10 {LF}, 13 {CR}, 32..255).
 * - BINARY otherwise.
 * - The following partially-portable control characters form a
 *   "gray list" that is ignored in this detection algorithm:
 *   (7 {BEL}, 8 {BS}, 11 {VT}, 12 {FF}, 26 {SUB}, 27 {ESC}).
 * IN assertion: the fields Freq of dyn_ltree are set.
 */
const detect_data_type = (s) => {
  /* block_mask is the bit mask of block-listed bytes
   * set bits 0..6, 14..25, and 28..31
   * 0xf3ffc07f = binary 11110011111111111100000001111111
   */
  let block_mask = 0xf3ffc07f;
  let n;

  /* Check for non-textual ("block-listed") bytes. */
  for (n = 0; n <= 31; n++, block_mask >>>= 1) {
    if ((block_mask & 1) && (s.dyn_ltree[n * 2]/*.Freq*/ !== 0)) {
      return Z_BINARY;
    }
  }

  /* Check for textual ("allow-listed") bytes. */
  if (s.dyn_ltree[9 * 2]/*.Freq*/ !== 0 || s.dyn_ltree[10 * 2]/*.Freq*/ !== 0 ||
      s.dyn_ltree[13 * 2]/*.Freq*/ !== 0) {
    return Z_TEXT;
  }
  for (n = 32; n < LITERALS$1; n++) {
    if (s.dyn_ltree[n * 2]/*.Freq*/ !== 0) {
      return Z_TEXT;
    }
  }

  /* There are no "block-listed" or "allow-listed" bytes:
   * this stream either is empty or has tolerated ("gray-listed") bytes only.
   */
  return Z_BINARY;
};


let static_init_done = false;

/* ===========================================================================
 * Initialize the tree data structures for a new zlib stream.
 */
const _tr_init$1 = (s) =>
{

  if (!static_init_done) {
    tr_static_init();
    static_init_done = true;
  }

  s.l_desc  = new TreeDesc(s.dyn_ltree, static_l_desc);
  s.d_desc  = new TreeDesc(s.dyn_dtree, static_d_desc);
  s.bl_desc = new TreeDesc(s.bl_tree, static_bl_desc);

  s.bi_buf = 0;
  s.bi_valid = 0;

  /* Initialize the first block of the first file: */
  init_block(s);
};


/* ===========================================================================
 * Send a stored block
 */
const _tr_stored_block$1 = (s, buf, stored_len, last) => {
//DeflateState *s;
//charf *buf;       /* input block */
//ulg stored_len;   /* length of input block */
//int last;         /* one if this is the last block for a file */

  send_bits(s, (STORED_BLOCK << 1) + (last ? 1 : 0), 3);    /* send block type */
  bi_windup(s);        /* align on byte boundary */
  put_short(s, stored_len);
  put_short(s, ~stored_len);
  if (stored_len) {
    s.pending_buf.set(s.window.subarray(buf, buf + stored_len), s.pending);
  }
  s.pending += stored_len;
};


/* ===========================================================================
 * Send one empty static block to give enough lookahead for inflate.
 * This takes 10 bits, of which 7 may remain in the bit buffer.
 */
const _tr_align$1 = (s) => {
  send_bits(s, STATIC_TREES << 1, 3);
  send_code(s, END_BLOCK, static_ltree);
  bi_flush(s);
};


/* ===========================================================================
 * Determine the best encoding for the current block: dynamic trees, static
 * trees or store, and write out the encoded block.
 */
const _tr_flush_block$1 = (s, buf, stored_len, last) => {
//DeflateState *s;
//charf *buf;       /* input block, or NULL if too old */
//ulg stored_len;   /* length of input block */
//int last;         /* one if this is the last block for a file */

  let opt_lenb, static_lenb;  /* opt_len and static_len in bytes */
  let max_blindex = 0;        /* index of last bit length code of non zero freq */

  /* Build the Huffman trees unless a stored block is forced */
  if (s.level > 0) {

    /* Check if the file is binary or text */
    if (s.strm.data_type === Z_UNKNOWN$1) {
      s.strm.data_type = detect_data_type(s);
    }

    /* Construct the literal and distance trees */
    build_tree(s, s.l_desc);
    // Tracev((stderr, "\nlit data: dyn %ld, stat %ld", s->opt_len,
    //        s->static_len));

    build_tree(s, s.d_desc);
    // Tracev((stderr, "\ndist data: dyn %ld, stat %ld", s->opt_len,
    //        s->static_len));
    /* At this point, opt_len and static_len are the total bit lengths of
     * the compressed block data, excluding the tree representations.
     */

    /* Build the bit length tree for the above two trees, and get the index
     * in bl_order of the last bit length code to send.
     */
    max_blindex = build_bl_tree(s);

    /* Determine the best encoding. Compute the block lengths in bytes. */
    opt_lenb = (s.opt_len + 3 + 7) >>> 3;
    static_lenb = (s.static_len + 3 + 7) >>> 3;

    // Tracev((stderr, "\nopt %lu(%lu) stat %lu(%lu) stored %lu lit %u ",
    //        opt_lenb, s->opt_len, static_lenb, s->static_len, stored_len,
    //        s->sym_next / 3));

    if (static_lenb <= opt_lenb) { opt_lenb = static_lenb; }

  } else {
    // Assert(buf != (char*)0, "lost buf");
    opt_lenb = static_lenb = stored_len + 5; /* force a stored block */
  }

  if ((stored_len + 4 <= opt_lenb) && (buf !== -1)) {
    /* 4: two words for the lengths */

    /* The test buf != NULL is only necessary if LIT_BUFSIZE > WSIZE.
     * Otherwise we can't have processed more than WSIZE input bytes since
     * the last block flush, because compression would have been
     * successful. If LIT_BUFSIZE <= WSIZE, it is never too late to
     * transform a block into a stored block.
     */
    _tr_stored_block$1(s, buf, stored_len, last);

  } else if (s.strategy === Z_FIXED$1 || static_lenb === opt_lenb) {

    send_bits(s, (STATIC_TREES << 1) + (last ? 1 : 0), 3);
    compress_block(s, static_ltree, static_dtree);

  } else {
    send_bits(s, (DYN_TREES << 1) + (last ? 1 : 0), 3);
    send_all_trees(s, s.l_desc.max_code + 1, s.d_desc.max_code + 1, max_blindex + 1);
    compress_block(s, s.dyn_ltree, s.dyn_dtree);
  }
  // Assert (s->compressed_len == s->bits_sent, "bad compressed size");
  /* The above check is made mod 2^32, for files larger than 512 MB
   * and uLong implemented on 32 bits.
   */
  init_block(s);

  if (last) {
    bi_windup(s);
  }
  // Tracev((stderr,"\ncomprlen %lu(%lu) ", s->compressed_len>>3,
  //       s->compressed_len-7*last));
};

/* ===========================================================================
 * Save the match info and tally the frequency counts. Return true if
 * the current block must be flushed.
 */
const _tr_tally$1 = (s, dist, lc) => {
//    deflate_state *s;
//    unsigned dist;  /* distance of matched string */
//    unsigned lc;    /* match length-MIN_MATCH or unmatched char (if dist==0) */

  s.pending_buf[s.sym_buf + s.sym_next++] = dist;
  s.pending_buf[s.sym_buf + s.sym_next++] = dist >> 8;
  s.pending_buf[s.sym_buf + s.sym_next++] = lc;
  if (dist === 0) {
    /* lc is the unmatched char */
    s.dyn_ltree[lc * 2]/*.Freq*/++;
  } else {
    s.matches++;
    /* Here, lc is the match length - MIN_MATCH */
    dist--;             /* dist = match distance - 1 */
    //Assert((ush)dist < (ush)MAX_DIST(s) &&
    //       (ush)lc <= (ush)(MAX_MATCH-MIN_MATCH) &&
    //       (ush)d_code(dist) < (ush)D_CODES,  "_tr_tally: bad match");

    s.dyn_ltree[(_length_code[lc] + LITERALS$1 + 1) * 2]/*.Freq*/++;
    s.dyn_dtree[d_code(dist) * 2]/*.Freq*/++;
  }

  return (s.sym_next === s.sym_end);
};

var _tr_init_1  = _tr_init$1;
var _tr_stored_block_1 = _tr_stored_block$1;
var _tr_flush_block_1  = _tr_flush_block$1;
var _tr_tally_1 = _tr_tally$1;
var _tr_align_1 = _tr_align$1;

var trees = {
	_tr_init: _tr_init_1,
	_tr_stored_block: _tr_stored_block_1,
	_tr_flush_block: _tr_flush_block_1,
	_tr_tally: _tr_tally_1,
	_tr_align: _tr_align_1
};

// Note: adler32 takes 12% for level 0 and 2% for level 6.
// It isn't worth it to make additional optimizations as in original.
// Small size is preferable.

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

const adler32 = (adler, buf, len, pos) => {
  let s1 = (adler & 0xffff) |0,
      s2 = ((adler >>> 16) & 0xffff) |0,
      n = 0;

  while (len !== 0) {
    // Set limit ~ twice less than 5552, to keep
    // s2 in 31-bits, because we force signed ints.
    // in other case %= will fail.
    n = len > 2000 ? 2000 : len;
    len -= n;

    do {
      s1 = (s1 + buf[pos++]) |0;
      s2 = (s2 + s1) |0;
    } while (--n);

    s1 %= 65521;
    s2 %= 65521;
  }

  return (s1 | (s2 << 16)) |0;
};


var adler32_1 = adler32;

// Note: we can't get significant speed boost here.
// So write code to minimize size - no pregenerated tables
// and array tools dependencies.

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

// Use ordinary array, since untyped makes no boost here
const makeTable = () => {
  let c, table = [];

  for (var n = 0; n < 256; n++) {
    c = n;
    for (var k = 0; k < 8; k++) {
      c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
    }
    table[n] = c;
  }

  return table;
};

// Create table on load. Just 255 signed longs. Not a problem.
const crcTable = new Uint32Array(makeTable());


const crc32 = (crc, buf, len, pos) => {
  const t = crcTable;
  const end = pos + len;

  crc ^= -1;

  for (let i = pos; i < end; i++) {
    crc = (crc >>> 8) ^ t[(crc ^ buf[i]) & 0xFF];
  }

  return (crc ^ (-1)); // >>> 0;
};


var crc32_1 = crc32;

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

var messages = {
  2:      'need dictionary',     /* Z_NEED_DICT       2  */
  1:      'stream end',          /* Z_STREAM_END      1  */
  0:      '',                    /* Z_OK              0  */
  '-1':   'file error',          /* Z_ERRNO         (-1) */
  '-2':   'stream error',        /* Z_STREAM_ERROR  (-2) */
  '-3':   'data error',          /* Z_DATA_ERROR    (-3) */
  '-4':   'insufficient memory', /* Z_MEM_ERROR     (-4) */
  '-5':   'buffer error',        /* Z_BUF_ERROR     (-5) */
  '-6':   'incompatible version' /* Z_VERSION_ERROR (-6) */
};

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

var constants$2 = {

  /* Allowed flush values; see deflate() and inflate() below for details */
  Z_NO_FLUSH:         0,
  Z_PARTIAL_FLUSH:    1,
  Z_SYNC_FLUSH:       2,
  Z_FULL_FLUSH:       3,
  Z_FINISH:           4,
  Z_BLOCK:            5,
  Z_TREES:            6,

  /* Return codes for the compression/decompression functions. Negative values
  * are errors, positive values are used for special but normal events.
  */
  Z_OK:               0,
  Z_STREAM_END:       1,
  Z_NEED_DICT:        2,
  Z_ERRNO:           -1,
  Z_STREAM_ERROR:    -2,
  Z_DATA_ERROR:      -3,
  Z_MEM_ERROR:       -4,
  Z_BUF_ERROR:       -5,
  //Z_VERSION_ERROR: -6,

  /* compression levels */
  Z_NO_COMPRESSION:         0,
  Z_BEST_SPEED:             1,
  Z_BEST_COMPRESSION:       9,
  Z_DEFAULT_COMPRESSION:   -1,


  Z_FILTERED:               1,
  Z_HUFFMAN_ONLY:           2,
  Z_RLE:                    3,
  Z_FIXED:                  4,
  Z_DEFAULT_STRATEGY:       0,

  /* Possible values of the data_type field (though see inflate()) */
  Z_BINARY:                 0,
  Z_TEXT:                   1,
  //Z_ASCII:                1, // = Z_TEXT (deprecated)
  Z_UNKNOWN:                2,

  /* The deflate compression method */
  Z_DEFLATED:               8
  //Z_NULL:                 null // Use -1 or null inline, depending on var type
};

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

const { _tr_init, _tr_stored_block, _tr_flush_block, _tr_tally, _tr_align } = trees;




/* Public constants ==========================================================*/
/* ===========================================================================*/

const {
  Z_NO_FLUSH: Z_NO_FLUSH$2, Z_PARTIAL_FLUSH, Z_FULL_FLUSH: Z_FULL_FLUSH$1, Z_FINISH: Z_FINISH$3, Z_BLOCK: Z_BLOCK$1,
  Z_OK: Z_OK$3, Z_STREAM_END: Z_STREAM_END$3, Z_STREAM_ERROR: Z_STREAM_ERROR$2, Z_DATA_ERROR: Z_DATA_ERROR$2, Z_BUF_ERROR: Z_BUF_ERROR$1,
  Z_DEFAULT_COMPRESSION: Z_DEFAULT_COMPRESSION$1,
  Z_FILTERED, Z_HUFFMAN_ONLY, Z_RLE, Z_FIXED, Z_DEFAULT_STRATEGY: Z_DEFAULT_STRATEGY$1,
  Z_UNKNOWN,
  Z_DEFLATED: Z_DEFLATED$2
} = constants$2;

/*============================================================================*/


const MAX_MEM_LEVEL = 9;
/* Maximum value for memLevel in deflateInit2 */
const MAX_WBITS$1 = 15;
/* 32K LZ77 window */
const DEF_MEM_LEVEL = 8;


const LENGTH_CODES  = 29;
/* number of length codes, not counting the special END_BLOCK code */
const LITERALS      = 256;
/* number of literal bytes 0..255 */
const L_CODES       = LITERALS + 1 + LENGTH_CODES;
/* number of Literal or Length codes, including the END_BLOCK code */
const D_CODES       = 30;
/* number of distance codes */
const BL_CODES      = 19;
/* number of codes used to transfer the bit lengths */
const HEAP_SIZE     = 2 * L_CODES + 1;
/* maximum heap size */
const MAX_BITS  = 15;
/* All codes must not exceed MAX_BITS bits */

const MIN_MATCH = 3;
const MAX_MATCH = 258;
const MIN_LOOKAHEAD = (MAX_MATCH + MIN_MATCH + 1);

const PRESET_DICT = 0x20;

const INIT_STATE    =  42;    /* zlib header -> BUSY_STATE */
//#ifdef GZIP
const GZIP_STATE    =  57;    /* gzip header -> BUSY_STATE | EXTRA_STATE */
//#endif
const EXTRA_STATE   =  69;    /* gzip extra block -> NAME_STATE */
const NAME_STATE    =  73;    /* gzip file name -> COMMENT_STATE */
const COMMENT_STATE =  91;    /* gzip comment -> HCRC_STATE */
const HCRC_STATE    = 103;    /* gzip header CRC -> BUSY_STATE */
const BUSY_STATE    = 113;    /* deflate -> FINISH_STATE */
const FINISH_STATE  = 666;    /* stream complete */

const BS_NEED_MORE      = 1; /* block not completed, need more input or more output */
const BS_BLOCK_DONE     = 2; /* block flush performed */
const BS_FINISH_STARTED = 3; /* finish started, need only more output at next deflate */
const BS_FINISH_DONE    = 4; /* finish done, accept no more input or output */

const OS_CODE = 0x03; // Unix :) . Don't detect, use this default.

const err = (strm, errorCode) => {
  strm.msg = messages[errorCode];
  return errorCode;
};

const rank = (f) => {
  return ((f) * 2) - ((f) > 4 ? 9 : 0);
};

const zero = (buf) => {
  let len = buf.length; while (--len >= 0) { buf[len] = 0; }
};

/* ===========================================================================
 * Slide the hash table when sliding the window down (could be avoided with 32
 * bit values at the expense of memory usage). We slide even when level == 0 to
 * keep the hash table consistent if we switch back to level > 0 later.
 */
const slide_hash = (s) => {
  let n, m;
  let p;
  let wsize = s.w_size;

  n = s.hash_size;
  p = n;
  do {
    m = s.head[--p];
    s.head[p] = (m >= wsize ? m - wsize : 0);
  } while (--n);
  n = wsize;
//#ifndef FASTEST
  p = n;
  do {
    m = s.prev[--p];
    s.prev[p] = (m >= wsize ? m - wsize : 0);
    /* If n is not on any hash chain, prev[n] is garbage but
     * its value will never be used.
     */
  } while (--n);
//#endif
};

/* eslint-disable new-cap */
let HASH_ZLIB = (s, prev, data) => ((prev << s.hash_shift) ^ data) & s.hash_mask;
// This hash causes less collisions, https://github.com/nodeca/pako/issues/135
// But breaks binary compatibility
//let HASH_FAST = (s, prev, data) => ((prev << 8) + (prev >> 8) + (data << 4)) & s.hash_mask;
let HASH = HASH_ZLIB;


/* =========================================================================
 * Flush as much pending output as possible. All deflate() output, except for
 * some deflate_stored() output, goes through this function so some
 * applications may wish to modify it to avoid allocating a large
 * strm->next_out buffer and copying into it. (See also read_buf()).
 */
const flush_pending = (strm) => {
  const s = strm.state;

  //_tr_flush_bits(s);
  let len = s.pending;
  if (len > strm.avail_out) {
    len = strm.avail_out;
  }
  if (len === 0) { return; }

  strm.output.set(s.pending_buf.subarray(s.pending_out, s.pending_out + len), strm.next_out);
  strm.next_out  += len;
  s.pending_out  += len;
  strm.total_out += len;
  strm.avail_out -= len;
  s.pending      -= len;
  if (s.pending === 0) {
    s.pending_out = 0;
  }
};


const flush_block_only = (s, last) => {
  _tr_flush_block(s, (s.block_start >= 0 ? s.block_start : -1), s.strstart - s.block_start, last);
  s.block_start = s.strstart;
  flush_pending(s.strm);
};


const put_byte = (s, b) => {
  s.pending_buf[s.pending++] = b;
};


/* =========================================================================
 * Put a short in the pending buffer. The 16-bit value is put in MSB order.
 * IN assertion: the stream state is correct and there is enough room in
 * pending_buf.
 */
const putShortMSB = (s, b) => {

  //  put_byte(s, (Byte)(b >> 8));
//  put_byte(s, (Byte)(b & 0xff));
  s.pending_buf[s.pending++] = (b >>> 8) & 0xff;
  s.pending_buf[s.pending++] = b & 0xff;
};


/* ===========================================================================
 * Read a new buffer from the current input stream, update the adler32
 * and total number of bytes read.  All deflate() input goes through
 * this function so some applications may wish to modify it to avoid
 * allocating a large strm->input buffer and copying from it.
 * (See also flush_pending()).
 */
const read_buf = (strm, buf, start, size) => {

  let len = strm.avail_in;

  if (len > size) { len = size; }
  if (len === 0) { return 0; }

  strm.avail_in -= len;

  // zmemcpy(buf, strm->next_in, len);
  buf.set(strm.input.subarray(strm.next_in, strm.next_in + len), start);
  if (strm.state.wrap === 1) {
    strm.adler = adler32_1(strm.adler, buf, len, start);
  }

  else if (strm.state.wrap === 2) {
    strm.adler = crc32_1(strm.adler, buf, len, start);
  }

  strm.next_in += len;
  strm.total_in += len;

  return len;
};


/* ===========================================================================
 * Set match_start to the longest match starting at the given string and
 * return its length. Matches shorter or equal to prev_length are discarded,
 * in which case the result is equal to prev_length and match_start is
 * garbage.
 * IN assertions: cur_match is the head of the hash chain for the current
 *   string (strstart) and its distance is <= MAX_DIST, and prev_length >= 1
 * OUT assertion: the match length is not greater than s->lookahead.
 */
const longest_match = (s, cur_match) => {

  let chain_length = s.max_chain_length;      /* max hash chain length */
  let scan = s.strstart; /* current string */
  let match;                       /* matched string */
  let len;                           /* length of current match */
  let best_len = s.prev_length;              /* best match length so far */
  let nice_match = s.nice_match;             /* stop if match long enough */
  const limit = (s.strstart > (s.w_size - MIN_LOOKAHEAD)) ?
      s.strstart - (s.w_size - MIN_LOOKAHEAD) : 0/*NIL*/;

  const _win = s.window; // shortcut

  const wmask = s.w_mask;
  const prev  = s.prev;

  /* Stop when cur_match becomes <= limit. To simplify the code,
   * we prevent matches with the string of window index 0.
   */

  const strend = s.strstart + MAX_MATCH;
  let scan_end1  = _win[scan + best_len - 1];
  let scan_end   = _win[scan + best_len];

  /* The code is optimized for HASH_BITS >= 8 and MAX_MATCH-2 multiple of 16.
   * It is easy to get rid of this optimization if necessary.
   */
  // Assert(s->hash_bits >= 8 && MAX_MATCH == 258, "Code too clever");

  /* Do not waste too much time if we already have a good match: */
  if (s.prev_length >= s.good_match) {
    chain_length >>= 2;
  }
  /* Do not look for matches beyond the end of the input. This is necessary
   * to make deflate deterministic.
   */
  if (nice_match > s.lookahead) { nice_match = s.lookahead; }

  // Assert((ulg)s->strstart <= s->window_size-MIN_LOOKAHEAD, "need lookahead");

  do {
    // Assert(cur_match < s->strstart, "no future");
    match = cur_match;

    /* Skip to next match if the match length cannot increase
     * or if the match length is less than 2.  Note that the checks below
     * for insufficient lookahead only occur occasionally for performance
     * reasons.  Therefore uninitialized memory will be accessed, and
     * conditional jumps will be made that depend on those values.
     * However the length of the match is limited to the lookahead, so
     * the output of deflate is not affected by the uninitialized values.
     */

    if (_win[match + best_len]     !== scan_end  ||
        _win[match + best_len - 1] !== scan_end1 ||
        _win[match]                !== _win[scan] ||
        _win[++match]              !== _win[scan + 1]) {
      continue;
    }

    /* The check at best_len-1 can be removed because it will be made
     * again later. (This heuristic is not always a win.)
     * It is not necessary to compare scan[2] and match[2] since they
     * are always equal when the other bytes match, given that
     * the hash keys are equal and that HASH_BITS >= 8.
     */
    scan += 2;
    match++;
    // Assert(*scan == *match, "match[2]?");

    /* We check for insufficient lookahead only every 8th comparison;
     * the 256th check will be made at strstart+258.
     */
    do {
      /*jshint noempty:false*/
    } while (_win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&
             _win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&
             _win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&
             _win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&
             scan < strend);

    // Assert(scan <= s->window+(unsigned)(s->window_size-1), "wild scan");

    len = MAX_MATCH - (strend - scan);
    scan = strend - MAX_MATCH;

    if (len > best_len) {
      s.match_start = cur_match;
      best_len = len;
      if (len >= nice_match) {
        break;
      }
      scan_end1  = _win[scan + best_len - 1];
      scan_end   = _win[scan + best_len];
    }
  } while ((cur_match = prev[cur_match & wmask]) > limit && --chain_length !== 0);

  if (best_len <= s.lookahead) {
    return best_len;
  }
  return s.lookahead;
};


/* ===========================================================================
 * Fill the window when the lookahead becomes insufficient.
 * Updates strstart and lookahead.
 *
 * IN assertion: lookahead < MIN_LOOKAHEAD
 * OUT assertions: strstart <= window_size-MIN_LOOKAHEAD
 *    At least one byte has been read, or avail_in == 0; reads are
 *    performed for at least two bytes (required for the zip translate_eol
 *    option -- not supported here).
 */
const fill_window = (s) => {

  const _w_size = s.w_size;
  let n, more, str;

  //Assert(s->lookahead < MIN_LOOKAHEAD, "already enough lookahead");

  do {
    more = s.window_size - s.lookahead - s.strstart;

    // JS ints have 32 bit, block below not needed
    /* Deal with !@#$% 64K limit: */
    //if (sizeof(int) <= 2) {
    //    if (more == 0 && s->strstart == 0 && s->lookahead == 0) {
    //        more = wsize;
    //
    //  } else if (more == (unsigned)(-1)) {
    //        /* Very unlikely, but possible on 16 bit machine if
    //         * strstart == 0 && lookahead == 1 (input done a byte at time)
    //         */
    //        more--;
    //    }
    //}


    /* If the window is almost full and there is insufficient lookahead,
     * move the upper half to the lower one to make room in the upper half.
     */
    if (s.strstart >= _w_size + (_w_size - MIN_LOOKAHEAD)) {

      s.window.set(s.window.subarray(_w_size, _w_size + _w_size - more), 0);
      s.match_start -= _w_size;
      s.strstart -= _w_size;
      /* we now have strstart >= MAX_DIST */
      s.block_start -= _w_size;
      if (s.insert > s.strstart) {
        s.insert = s.strstart;
      }
      slide_hash(s);
      more += _w_size;
    }
    if (s.strm.avail_in === 0) {
      break;
    }

    /* If there was no sliding:
     *    strstart <= WSIZE+MAX_DIST-1 && lookahead <= MIN_LOOKAHEAD - 1 &&
     *    more == window_size - lookahead - strstart
     * => more >= window_size - (MIN_LOOKAHEAD-1 + WSIZE + MAX_DIST-1)
     * => more >= window_size - 2*WSIZE + 2
     * In the BIG_MEM or MMAP case (not yet supported),
     *   window_size == input_size + MIN_LOOKAHEAD  &&
     *   strstart + s->lookahead <= input_size => more >= MIN_LOOKAHEAD.
     * Otherwise, window_size == 2*WSIZE so more >= 2.
     * If there was sliding, more >= WSIZE. So in all cases, more >= 2.
     */
    //Assert(more >= 2, "more < 2");
    n = read_buf(s.strm, s.window, s.strstart + s.lookahead, more);
    s.lookahead += n;

    /* Initialize the hash value now that we have some input: */
    if (s.lookahead + s.insert >= MIN_MATCH) {
      str = s.strstart - s.insert;
      s.ins_h = s.window[str];

      /* UPDATE_HASH(s, s->ins_h, s->window[str + 1]); */
      s.ins_h = HASH(s, s.ins_h, s.window[str + 1]);
//#if MIN_MATCH != 3
//        Call update_hash() MIN_MATCH-3 more times
//#endif
      while (s.insert) {
        /* UPDATE_HASH(s, s->ins_h, s->window[str + MIN_MATCH-1]); */
        s.ins_h = HASH(s, s.ins_h, s.window[str + MIN_MATCH - 1]);

        s.prev[str & s.w_mask] = s.head[s.ins_h];
        s.head[s.ins_h] = str;
        str++;
        s.insert--;
        if (s.lookahead + s.insert < MIN_MATCH) {
          break;
        }
      }
    }
    /* If the whole input has less than MIN_MATCH bytes, ins_h is garbage,
     * but this is not important since only literal bytes will be emitted.
     */

  } while (s.lookahead < MIN_LOOKAHEAD && s.strm.avail_in !== 0);

  /* If the WIN_INIT bytes after the end of the current data have never been
   * written, then zero those bytes in order to avoid memory check reports of
   * the use of uninitialized (or uninitialised as Julian writes) bytes by
   * the longest match routines.  Update the high water mark for the next
   * time through here.  WIN_INIT is set to MAX_MATCH since the longest match
   * routines allow scanning to strstart + MAX_MATCH, ignoring lookahead.
   */
//  if (s.high_water < s.window_size) {
//    const curr = s.strstart + s.lookahead;
//    let init = 0;
//
//    if (s.high_water < curr) {
//      /* Previous high water mark below current data -- zero WIN_INIT
//       * bytes or up to end of window, whichever is less.
//       */
//      init = s.window_size - curr;
//      if (init > WIN_INIT)
//        init = WIN_INIT;
//      zmemzero(s->window + curr, (unsigned)init);
//      s->high_water = curr + init;
//    }
//    else if (s->high_water < (ulg)curr + WIN_INIT) {
//      /* High water mark at or above current data, but below current data
//       * plus WIN_INIT -- zero out to current data plus WIN_INIT, or up
//       * to end of window, whichever is less.
//       */
//      init = (ulg)curr + WIN_INIT - s->high_water;
//      if (init > s->window_size - s->high_water)
//        init = s->window_size - s->high_water;
//      zmemzero(s->window + s->high_water, (unsigned)init);
//      s->high_water += init;
//    }
//  }
//
//  Assert((ulg)s->strstart <= s->window_size - MIN_LOOKAHEAD,
//    "not enough room for search");
};

/* ===========================================================================
 * Copy without compression as much as possible from the input stream, return
 * the current block state.
 *
 * In case deflateParams() is used to later switch to a non-zero compression
 * level, s->matches (otherwise unused when storing) keeps track of the number
 * of hash table slides to perform. If s->matches is 1, then one hash table
 * slide will be done when switching. If s->matches is 2, the maximum value
 * allowed here, then the hash table will be cleared, since two or more slides
 * is the same as a clear.
 *
 * deflate_stored() is written to minimize the number of times an input byte is
 * copied. It is most efficient with large input and output buffers, which
 * maximizes the opportunites to have a single copy from next_in to next_out.
 */
const deflate_stored = (s, flush) => {

  /* Smallest worthy block size when not flushing or finishing. By default
   * this is 32K. This can be as small as 507 bytes for memLevel == 1. For
   * large input and output buffers, the stored block size will be larger.
   */
  let min_block = s.pending_buf_size - 5 > s.w_size ? s.w_size : s.pending_buf_size - 5;

  /* Copy as many min_block or larger stored blocks directly to next_out as
   * possible. If flushing, copy the remaining available input to next_out as
   * stored blocks, if there is enough space.
   */
  let len, left, have, last = 0;
  let used = s.strm.avail_in;
  do {
    /* Set len to the maximum size block that we can copy directly with the
     * available input data and output space. Set left to how much of that
     * would be copied from what's left in the window.
     */
    len = 65535/* MAX_STORED */;     /* maximum deflate stored block length */
    have = (s.bi_valid + 42) >> 3;     /* number of header bytes */
    if (s.strm.avail_out < have) {         /* need room for header */
      break;
    }
      /* maximum stored block length that will fit in avail_out: */
    have = s.strm.avail_out - have;
    left = s.strstart - s.block_start;  /* bytes left in window */
    if (len > left + s.strm.avail_in) {
      len = left + s.strm.avail_in;   /* limit len to the input */
    }
    if (len > have) {
      len = have;             /* limit len to the output */
    }

    /* If the stored block would be less than min_block in length, or if
     * unable to copy all of the available input when flushing, then try
     * copying to the window and the pending buffer instead. Also don't
     * write an empty block when flushing -- deflate() does that.
     */
    if (len < min_block && ((len === 0 && flush !== Z_FINISH$3) ||
                        flush === Z_NO_FLUSH$2 ||
                        len !== left + s.strm.avail_in)) {
      break;
    }

    /* Make a dummy stored block in pending to get the header bytes,
     * including any pending bits. This also updates the debugging counts.
     */
    last = flush === Z_FINISH$3 && len === left + s.strm.avail_in ? 1 : 0;
    _tr_stored_block(s, 0, 0, last);

    /* Replace the lengths in the dummy stored block with len. */
    s.pending_buf[s.pending - 4] = len;
    s.pending_buf[s.pending - 3] = len >> 8;
    s.pending_buf[s.pending - 2] = ~len;
    s.pending_buf[s.pending - 1] = ~len >> 8;

    /* Write the stored block header bytes. */
    flush_pending(s.strm);

//#ifdef ZLIB_DEBUG
//    /* Update debugging counts for the data about to be copied. */
//    s->compressed_len += len << 3;
//    s->bits_sent += len << 3;
//#endif

    /* Copy uncompressed bytes from the window to next_out. */
    if (left) {
      if (left > len) {
        left = len;
      }
      //zmemcpy(s->strm->next_out, s->window + s->block_start, left);
      s.strm.output.set(s.window.subarray(s.block_start, s.block_start + left), s.strm.next_out);
      s.strm.next_out += left;
      s.strm.avail_out -= left;
      s.strm.total_out += left;
      s.block_start += left;
      len -= left;
    }

    /* Copy uncompressed bytes directly from next_in to next_out, updating
     * the check value.
     */
    if (len) {
      read_buf(s.strm, s.strm.output, s.strm.next_out, len);
      s.strm.next_out += len;
      s.strm.avail_out -= len;
      s.strm.total_out += len;
    }
  } while (last === 0);

  /* Update the sliding window with the last s->w_size bytes of the copied
   * data, or append all of the copied data to the existing window if less
   * than s->w_size bytes were copied. Also update the number of bytes to
   * insert in the hash tables, in the event that deflateParams() switches to
   * a non-zero compression level.
   */
  used -= s.strm.avail_in;    /* number of input bytes directly copied */
  if (used) {
    /* If any input was used, then no unused input remains in the window,
     * therefore s->block_start == s->strstart.
     */
    if (used >= s.w_size) {  /* supplant the previous history */
      s.matches = 2;     /* clear hash */
      //zmemcpy(s->window, s->strm->next_in - s->w_size, s->w_size);
      s.window.set(s.strm.input.subarray(s.strm.next_in - s.w_size, s.strm.next_in), 0);
      s.strstart = s.w_size;
      s.insert = s.strstart;
    }
    else {
      if (s.window_size - s.strstart <= used) {
        /* Slide the window down. */
        s.strstart -= s.w_size;
        //zmemcpy(s->window, s->window + s->w_size, s->strstart);
        s.window.set(s.window.subarray(s.w_size, s.w_size + s.strstart), 0);
        if (s.matches < 2) {
          s.matches++;   /* add a pending slide_hash() */
        }
        if (s.insert > s.strstart) {
          s.insert = s.strstart;
        }
      }
      //zmemcpy(s->window + s->strstart, s->strm->next_in - used, used);
      s.window.set(s.strm.input.subarray(s.strm.next_in - used, s.strm.next_in), s.strstart);
      s.strstart += used;
      s.insert += used > s.w_size - s.insert ? s.w_size - s.insert : used;
    }
    s.block_start = s.strstart;
  }
  if (s.high_water < s.strstart) {
    s.high_water = s.strstart;
  }

  /* If the last block was written to next_out, then done. */
  if (last) {
    return BS_FINISH_DONE;
  }

  /* If flushing and all input has been consumed, then done. */
  if (flush !== Z_NO_FLUSH$2 && flush !== Z_FINISH$3 &&
    s.strm.avail_in === 0 && s.strstart === s.block_start) {
    return BS_BLOCK_DONE;
  }

  /* Fill the window with any remaining input. */
  have = s.window_size - s.strstart;
  if (s.strm.avail_in > have && s.block_start >= s.w_size) {
    /* Slide the window down. */
    s.block_start -= s.w_size;
    s.strstart -= s.w_size;
    //zmemcpy(s->window, s->window + s->w_size, s->strstart);
    s.window.set(s.window.subarray(s.w_size, s.w_size + s.strstart), 0);
    if (s.matches < 2) {
      s.matches++;       /* add a pending slide_hash() */
    }
    have += s.w_size;      /* more space now */
    if (s.insert > s.strstart) {
      s.insert = s.strstart;
    }
  }
  if (have > s.strm.avail_in) {
    have = s.strm.avail_in;
  }
  if (have) {
    read_buf(s.strm, s.window, s.strstart, have);
    s.strstart += have;
    s.insert += have > s.w_size - s.insert ? s.w_size - s.insert : have;
  }
  if (s.high_water < s.strstart) {
    s.high_water = s.strstart;
  }

  /* There was not enough avail_out to write a complete worthy or flushed
   * stored block to next_out. Write a stored block to pending instead, if we
   * have enough input for a worthy block, or if flushing and there is enough
   * room for the remaining input as a stored block in the pending buffer.
   */
  have = (s.bi_valid + 42) >> 3;     /* number of header bytes */
    /* maximum stored block length that will fit in pending: */
  have = s.pending_buf_size - have > 65535/* MAX_STORED */ ? 65535/* MAX_STORED */ : s.pending_buf_size - have;
  min_block = have > s.w_size ? s.w_size : have;
  left = s.strstart - s.block_start;
  if (left >= min_block ||
     ((left || flush === Z_FINISH$3) && flush !== Z_NO_FLUSH$2 &&
     s.strm.avail_in === 0 && left <= have)) {
    len = left > have ? have : left;
    last = flush === Z_FINISH$3 && s.strm.avail_in === 0 &&
         len === left ? 1 : 0;
    _tr_stored_block(s, s.block_start, len, last);
    s.block_start += len;
    flush_pending(s.strm);
  }

  /* We've done all we can with the available input and output. */
  return last ? BS_FINISH_STARTED : BS_NEED_MORE;
};


/* ===========================================================================
 * Compress as much as possible from the input stream, return the current
 * block state.
 * This function does not perform lazy evaluation of matches and inserts
 * new strings in the dictionary only for unmatched strings or for short
 * matches. It is used only for the fast compression options.
 */
const deflate_fast = (s, flush) => {

  let hash_head;        /* head of the hash chain */
  let bflush;           /* set if current block must be flushed */

  for (;;) {
    /* Make sure that we always have enough lookahead, except
     * at the end of the input file. We need MAX_MATCH bytes
     * for the next match, plus MIN_MATCH bytes to insert the
     * string following the next match.
     */
    if (s.lookahead < MIN_LOOKAHEAD) {
      fill_window(s);
      if (s.lookahead < MIN_LOOKAHEAD && flush === Z_NO_FLUSH$2) {
        return BS_NEED_MORE;
      }
      if (s.lookahead === 0) {
        break; /* flush the current block */
      }
    }

    /* Insert the string window[strstart .. strstart+2] in the
     * dictionary, and set hash_head to the head of the hash chain:
     */
    hash_head = 0/*NIL*/;
    if (s.lookahead >= MIN_MATCH) {
      /*** INSERT_STRING(s, s.strstart, hash_head); ***/
      s.ins_h = HASH(s, s.ins_h, s.window[s.strstart + MIN_MATCH - 1]);
      hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];
      s.head[s.ins_h] = s.strstart;
      /***/
    }

    /* Find the longest match, discarding those <= prev_length.
     * At this point we have always match_length < MIN_MATCH
     */
    if (hash_head !== 0/*NIL*/ && ((s.strstart - hash_head) <= (s.w_size - MIN_LOOKAHEAD))) {
      /* To simplify the code, we prevent matches with the string
       * of window index 0 (in particular we have to avoid a match
       * of the string with itself at the start of the input file).
       */
      s.match_length = longest_match(s, hash_head);
      /* longest_match() sets match_start */
    }
    if (s.match_length >= MIN_MATCH) {
      // check_match(s, s.strstart, s.match_start, s.match_length); // for debug only

      /*** _tr_tally_dist(s, s.strstart - s.match_start,
                     s.match_length - MIN_MATCH, bflush); ***/
      bflush = _tr_tally(s, s.strstart - s.match_start, s.match_length - MIN_MATCH);

      s.lookahead -= s.match_length;

      /* Insert new strings in the hash table only if the match length
       * is not too large. This saves time but degrades compression.
       */
      if (s.match_length <= s.max_lazy_match/*max_insert_length*/ && s.lookahead >= MIN_MATCH) {
        s.match_length--; /* string at strstart already in table */
        do {
          s.strstart++;
          /*** INSERT_STRING(s, s.strstart, hash_head); ***/
          s.ins_h = HASH(s, s.ins_h, s.window[s.strstart + MIN_MATCH - 1]);
          hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];
          s.head[s.ins_h] = s.strstart;
          /***/
          /* strstart never exceeds WSIZE-MAX_MATCH, so there are
           * always MIN_MATCH bytes ahead.
           */
        } while (--s.match_length !== 0);
        s.strstart++;
      } else
      {
        s.strstart += s.match_length;
        s.match_length = 0;
        s.ins_h = s.window[s.strstart];
        /* UPDATE_HASH(s, s.ins_h, s.window[s.strstart+1]); */
        s.ins_h = HASH(s, s.ins_h, s.window[s.strstart + 1]);

//#if MIN_MATCH != 3
//                Call UPDATE_HASH() MIN_MATCH-3 more times
//#endif
        /* If lookahead < MIN_MATCH, ins_h is garbage, but it does not
         * matter since it will be recomputed at next deflate call.
         */
      }
    } else {
      /* No match, output a literal byte */
      //Tracevv((stderr,"%c", s.window[s.strstart]));
      /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/
      bflush = _tr_tally(s, 0, s.window[s.strstart]);

      s.lookahead--;
      s.strstart++;
    }
    if (bflush) {
      /*** FLUSH_BLOCK(s, 0); ***/
      flush_block_only(s, false);
      if (s.strm.avail_out === 0) {
        return BS_NEED_MORE;
      }
      /***/
    }
  }
  s.insert = ((s.strstart < (MIN_MATCH - 1)) ? s.strstart : MIN_MATCH - 1);
  if (flush === Z_FINISH$3) {
    /*** FLUSH_BLOCK(s, 1); ***/
    flush_block_only(s, true);
    if (s.strm.avail_out === 0) {
      return BS_FINISH_STARTED;
    }
    /***/
    return BS_FINISH_DONE;
  }
  if (s.sym_next) {
    /*** FLUSH_BLOCK(s, 0); ***/
    flush_block_only(s, false);
    if (s.strm.avail_out === 0) {
      return BS_NEED_MORE;
    }
    /***/
  }
  return BS_BLOCK_DONE;
};

/* ===========================================================================
 * Same as above, but achieves better compression. We use a lazy
 * evaluation for matches: a match is finally adopted only if there is
 * no better match at the next window position.
 */
const deflate_slow = (s, flush) => {

  let hash_head;          /* head of hash chain */
  let bflush;              /* set if current block must be flushed */

  let max_insert;

  /* Process the input block. */
  for (;;) {
    /* Make sure that we always have enough lookahead, except
     * at the end of the input file. We need MAX_MATCH bytes
     * for the next match, plus MIN_MATCH bytes to insert the
     * string following the next match.
     */
    if (s.lookahead < MIN_LOOKAHEAD) {
      fill_window(s);
      if (s.lookahead < MIN_LOOKAHEAD && flush === Z_NO_FLUSH$2) {
        return BS_NEED_MORE;
      }
      if (s.lookahead === 0) { break; } /* flush the current block */
    }

    /* Insert the string window[strstart .. strstart+2] in the
     * dictionary, and set hash_head to the head of the hash chain:
     */
    hash_head = 0/*NIL*/;
    if (s.lookahead >= MIN_MATCH) {
      /*** INSERT_STRING(s, s.strstart, hash_head); ***/
      s.ins_h = HASH(s, s.ins_h, s.window[s.strstart + MIN_MATCH - 1]);
      hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];
      s.head[s.ins_h] = s.strstart;
      /***/
    }

    /* Find the longest match, discarding those <= prev_length.
     */
    s.prev_length = s.match_length;
    s.prev_match = s.match_start;
    s.match_length = MIN_MATCH - 1;

    if (hash_head !== 0/*NIL*/ && s.prev_length < s.max_lazy_match &&
        s.strstart - hash_head <= (s.w_size - MIN_LOOKAHEAD)/*MAX_DIST(s)*/) {
      /* To simplify the code, we prevent matches with the string
       * of window index 0 (in particular we have to avoid a match
       * of the string with itself at the start of the input file).
       */
      s.match_length = longest_match(s, hash_head);
      /* longest_match() sets match_start */

      if (s.match_length <= 5 &&
         (s.strategy === Z_FILTERED || (s.match_length === MIN_MATCH && s.strstart - s.match_start > 4096/*TOO_FAR*/))) {

        /* If prev_match is also MIN_MATCH, match_start is garbage
         * but we will ignore the current match anyway.
         */
        s.match_length = MIN_MATCH - 1;
      }
    }
    /* If there was a match at the previous step and the current
     * match is not better, output the previous match:
     */
    if (s.prev_length >= MIN_MATCH && s.match_length <= s.prev_length) {
      max_insert = s.strstart + s.lookahead - MIN_MATCH;
      /* Do not insert strings in hash table beyond this. */

      //check_match(s, s.strstart-1, s.prev_match, s.prev_length);

      /***_tr_tally_dist(s, s.strstart - 1 - s.prev_match,
                     s.prev_length - MIN_MATCH, bflush);***/
      bflush = _tr_tally(s, s.strstart - 1 - s.prev_match, s.prev_length - MIN_MATCH);
      /* Insert in hash table all strings up to the end of the match.
       * strstart-1 and strstart are already inserted. If there is not
       * enough lookahead, the last two strings are not inserted in
       * the hash table.
       */
      s.lookahead -= s.prev_length - 1;
      s.prev_length -= 2;
      do {
        if (++s.strstart <= max_insert) {
          /*** INSERT_STRING(s, s.strstart, hash_head); ***/
          s.ins_h = HASH(s, s.ins_h, s.window[s.strstart + MIN_MATCH - 1]);
          hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];
          s.head[s.ins_h] = s.strstart;
          /***/
        }
      } while (--s.prev_length !== 0);
      s.match_available = 0;
      s.match_length = MIN_MATCH - 1;
      s.strstart++;

      if (bflush) {
        /*** FLUSH_BLOCK(s, 0); ***/
        flush_block_only(s, false);
        if (s.strm.avail_out === 0) {
          return BS_NEED_MORE;
        }
        /***/
      }

    } else if (s.match_available) {
      /* If there was no match at the previous position, output a
       * single literal. If there was a match but the current match
       * is longer, truncate the previous match to a single literal.
       */
      //Tracevv((stderr,"%c", s->window[s->strstart-1]));
      /*** _tr_tally_lit(s, s.window[s.strstart-1], bflush); ***/
      bflush = _tr_tally(s, 0, s.window[s.strstart - 1]);

      if (bflush) {
        /*** FLUSH_BLOCK_ONLY(s, 0) ***/
        flush_block_only(s, false);
        /***/
      }
      s.strstart++;
      s.lookahead--;
      if (s.strm.avail_out === 0) {
        return BS_NEED_MORE;
      }
    } else {
      /* There is no previous match to compare with, wait for
       * the next step to decide.
       */
      s.match_available = 1;
      s.strstart++;
      s.lookahead--;
    }
  }
  //Assert (flush != Z_NO_FLUSH, "no flush?");
  if (s.match_available) {
    //Tracevv((stderr,"%c", s->window[s->strstart-1]));
    /*** _tr_tally_lit(s, s.window[s.strstart-1], bflush); ***/
    bflush = _tr_tally(s, 0, s.window[s.strstart - 1]);

    s.match_available = 0;
  }
  s.insert = s.strstart < MIN_MATCH - 1 ? s.strstart : MIN_MATCH - 1;
  if (flush === Z_FINISH$3) {
    /*** FLUSH_BLOCK(s, 1); ***/
    flush_block_only(s, true);
    if (s.strm.avail_out === 0) {
      return BS_FINISH_STARTED;
    }
    /***/
    return BS_FINISH_DONE;
  }
  if (s.sym_next) {
    /*** FLUSH_BLOCK(s, 0); ***/
    flush_block_only(s, false);
    if (s.strm.avail_out === 0) {
      return BS_NEED_MORE;
    }
    /***/
  }

  return BS_BLOCK_DONE;
};


/* ===========================================================================
 * For Z_RLE, simply look for runs of bytes, generate matches only of distance
 * one.  Do not maintain a hash table.  (It will be regenerated if this run of
 * deflate switches away from Z_RLE.)
 */
const deflate_rle = (s, flush) => {

  let bflush;            /* set if current block must be flushed */
  let prev;              /* byte at distance one to match */
  let scan, strend;      /* scan goes up to strend for length of run */

  const _win = s.window;

  for (;;) {
    /* Make sure that we always have enough lookahead, except
     * at the end of the input file. We need MAX_MATCH bytes
     * for the longest run, plus one for the unrolled loop.
     */
    if (s.lookahead <= MAX_MATCH) {
      fill_window(s);
      if (s.lookahead <= MAX_MATCH && flush === Z_NO_FLUSH$2) {
        return BS_NEED_MORE;
      }
      if (s.lookahead === 0) { break; } /* flush the current block */
    }

    /* See how many times the previous byte repeats */
    s.match_length = 0;
    if (s.lookahead >= MIN_MATCH && s.strstart > 0) {
      scan = s.strstart - 1;
      prev = _win[scan];
      if (prev === _win[++scan] && prev === _win[++scan] && prev === _win[++scan]) {
        strend = s.strstart + MAX_MATCH;
        do {
          /*jshint noempty:false*/
        } while (prev === _win[++scan] && prev === _win[++scan] &&
                 prev === _win[++scan] && prev === _win[++scan] &&
                 prev === _win[++scan] && prev === _win[++scan] &&
                 prev === _win[++scan] && prev === _win[++scan] &&
                 scan < strend);
        s.match_length = MAX_MATCH - (strend - scan);
        if (s.match_length > s.lookahead) {
          s.match_length = s.lookahead;
        }
      }
      //Assert(scan <= s->window+(uInt)(s->window_size-1), "wild scan");
    }

    /* Emit match if have run of MIN_MATCH or longer, else emit literal */
    if (s.match_length >= MIN_MATCH) {
      //check_match(s, s.strstart, s.strstart - 1, s.match_length);

      /*** _tr_tally_dist(s, 1, s.match_length - MIN_MATCH, bflush); ***/
      bflush = _tr_tally(s, 1, s.match_length - MIN_MATCH);

      s.lookahead -= s.match_length;
      s.strstart += s.match_length;
      s.match_length = 0;
    } else {
      /* No match, output a literal byte */
      //Tracevv((stderr,"%c", s->window[s->strstart]));
      /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/
      bflush = _tr_tally(s, 0, s.window[s.strstart]);

      s.lookahead--;
      s.strstart++;
    }
    if (bflush) {
      /*** FLUSH_BLOCK(s, 0); ***/
      flush_block_only(s, false);
      if (s.strm.avail_out === 0) {
        return BS_NEED_MORE;
      }
      /***/
    }
  }
  s.insert = 0;
  if (flush === Z_FINISH$3) {
    /*** FLUSH_BLOCK(s, 1); ***/
    flush_block_only(s, true);
    if (s.strm.avail_out === 0) {
      return BS_FINISH_STARTED;
    }
    /***/
    return BS_FINISH_DONE;
  }
  if (s.sym_next) {
    /*** FLUSH_BLOCK(s, 0); ***/
    flush_block_only(s, false);
    if (s.strm.avail_out === 0) {
      return BS_NEED_MORE;
    }
    /***/
  }
  return BS_BLOCK_DONE;
};

/* ===========================================================================
 * For Z_HUFFMAN_ONLY, do not look for matches.  Do not maintain a hash table.
 * (It will be regenerated if this run of deflate switches away from Huffman.)
 */
const deflate_huff = (s, flush) => {

  let bflush;             /* set if current block must be flushed */

  for (;;) {
    /* Make sure that we have a literal to write. */
    if (s.lookahead === 0) {
      fill_window(s);
      if (s.lookahead === 0) {
        if (flush === Z_NO_FLUSH$2) {
          return BS_NEED_MORE;
        }
        break;      /* flush the current block */
      }
    }

    /* Output a literal byte */
    s.match_length = 0;
    //Tracevv((stderr,"%c", s->window[s->strstart]));
    /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/
    bflush = _tr_tally(s, 0, s.window[s.strstart]);
    s.lookahead--;
    s.strstart++;
    if (bflush) {
      /*** FLUSH_BLOCK(s, 0); ***/
      flush_block_only(s, false);
      if (s.strm.avail_out === 0) {
        return BS_NEED_MORE;
      }
      /***/
    }
  }
  s.insert = 0;
  if (flush === Z_FINISH$3) {
    /*** FLUSH_BLOCK(s, 1); ***/
    flush_block_only(s, true);
    if (s.strm.avail_out === 0) {
      return BS_FINISH_STARTED;
    }
    /***/
    return BS_FINISH_DONE;
  }
  if (s.sym_next) {
    /*** FLUSH_BLOCK(s, 0); ***/
    flush_block_only(s, false);
    if (s.strm.avail_out === 0) {
      return BS_NEED_MORE;
    }
    /***/
  }
  return BS_BLOCK_DONE;
};

/* Values for max_lazy_match, good_match and max_chain_length, depending on
 * the desired pack level (0..9). The values given below have been tuned to
 * exclude worst case performance for pathological files. Better values may be
 * found for specific files.
 */
function Config(good_length, max_lazy, nice_length, max_chain, func) {

  this.good_length = good_length;
  this.max_lazy = max_lazy;
  this.nice_length = nice_length;
  this.max_chain = max_chain;
  this.func = func;
}

const configuration_table = [
  /*      good lazy nice chain */
  new Config(0, 0, 0, 0, deflate_stored),          /* 0 store only */
  new Config(4, 4, 8, 4, deflate_fast),            /* 1 max speed, no lazy matches */
  new Config(4, 5, 16, 8, deflate_fast),           /* 2 */
  new Config(4, 6, 32, 32, deflate_fast),          /* 3 */

  new Config(4, 4, 16, 16, deflate_slow),          /* 4 lazy matches */
  new Config(8, 16, 32, 32, deflate_slow),         /* 5 */
  new Config(8, 16, 128, 128, deflate_slow),       /* 6 */
  new Config(8, 32, 128, 256, deflate_slow),       /* 7 */
  new Config(32, 128, 258, 1024, deflate_slow),    /* 8 */
  new Config(32, 258, 258, 4096, deflate_slow)     /* 9 max compression */
];


/* ===========================================================================
 * Initialize the "longest match" routines for a new zlib stream
 */
const lm_init = (s) => {

  s.window_size = 2 * s.w_size;

  /*** CLEAR_HASH(s); ***/
  zero(s.head); // Fill with NIL (= 0);

  /* Set the default configuration parameters:
   */
  s.max_lazy_match = configuration_table[s.level].max_lazy;
  s.good_match = configuration_table[s.level].good_length;
  s.nice_match = configuration_table[s.level].nice_length;
  s.max_chain_length = configuration_table[s.level].max_chain;

  s.strstart = 0;
  s.block_start = 0;
  s.lookahead = 0;
  s.insert = 0;
  s.match_length = s.prev_length = MIN_MATCH - 1;
  s.match_available = 0;
  s.ins_h = 0;
};


function DeflateState() {
  this.strm = null;            /* pointer back to this zlib stream */
  this.status = 0;            /* as the name implies */
  this.pending_buf = null;      /* output still pending */
  this.pending_buf_size = 0;  /* size of pending_buf */
  this.pending_out = 0;       /* next pending byte to output to the stream */
  this.pending = 0;           /* nb of bytes in the pending buffer */
  this.wrap = 0;              /* bit 0 true for zlib, bit 1 true for gzip */
  this.gzhead = null;         /* gzip header information to write */
  this.gzindex = 0;           /* where in extra, name, or comment */
  this.method = Z_DEFLATED$2; /* can only be DEFLATED */
  this.last_flush = -1;   /* value of flush param for previous deflate call */

  this.w_size = 0;  /* LZ77 window size (32K by default) */
  this.w_bits = 0;  /* log2(w_size)  (8..16) */
  this.w_mask = 0;  /* w_size - 1 */

  this.window = null;
  /* Sliding window. Input bytes are read into the second half of the window,
   * and move to the first half later to keep a dictionary of at least wSize
   * bytes. With this organization, matches are limited to a distance of
   * wSize-MAX_MATCH bytes, but this ensures that IO is always
   * performed with a length multiple of the block size.
   */

  this.window_size = 0;
  /* Actual size of window: 2*wSize, except when the user input buffer
   * is directly used as sliding window.
   */

  this.prev = null;
  /* Link to older string with same hash index. To limit the size of this
   * array to 64K, this link is maintained only for the last 32K strings.
   * An index in this array is thus a window index modulo 32K.
   */

  this.head = null;   /* Heads of the hash chains or NIL. */

  this.ins_h = 0;       /* hash index of string to be inserted */
  this.hash_size = 0;   /* number of elements in hash table */
  this.hash_bits = 0;   /* log2(hash_size) */
  this.hash_mask = 0;   /* hash_size-1 */

  this.hash_shift = 0;
  /* Number of bits by which ins_h must be shifted at each input
   * step. It must be such that after MIN_MATCH steps, the oldest
   * byte no longer takes part in the hash key, that is:
   *   hash_shift * MIN_MATCH >= hash_bits
   */

  this.block_start = 0;
  /* Window position at the beginning of the current output block. Gets
   * negative when the window is moved backwards.
   */

  this.match_length = 0;      /* length of best match */
  this.prev_match = 0;        /* previous match */
  this.match_available = 0;   /* set if previous match exists */
  this.strstart = 0;          /* start of string to insert */
  this.match_start = 0;       /* start of matching string */
  this.lookahead = 0;         /* number of valid bytes ahead in window */

  this.prev_length = 0;
  /* Length of the best match at previous step. Matches not greater than this
   * are discarded. This is used in the lazy match evaluation.
   */

  this.max_chain_length = 0;
  /* To speed up deflation, hash chains are never searched beyond this
   * length.  A higher limit improves compression ratio but degrades the
   * speed.
   */

  this.max_lazy_match = 0;
  /* Attempt to find a better match only when the current match is strictly
   * smaller than this value. This mechanism is used only for compression
   * levels >= 4.
   */
  // That's alias to max_lazy_match, don't use directly
  //this.max_insert_length = 0;
  /* Insert new strings in the hash table only if the match length is not
   * greater than this length. This saves time but degrades compression.
   * max_insert_length is used only for compression levels <= 3.
   */

  this.level = 0;     /* compression level (1..9) */
  this.strategy = 0;  /* favor or force Huffman coding*/

  this.good_match = 0;
  /* Use a faster search when the previous match is longer than this */

  this.nice_match = 0; /* Stop searching when current match exceeds this */

              /* used by trees.c: */

  /* Didn't use ct_data typedef below to suppress compiler warning */

  // struct ct_data_s dyn_ltree[HEAP_SIZE];   /* literal and length tree */
  // struct ct_data_s dyn_dtree[2*D_CODES+1]; /* distance tree */
  // struct ct_data_s bl_tree[2*BL_CODES+1];  /* Huffman tree for bit lengths */

  // Use flat array of DOUBLE size, with interleaved fata,
  // because JS does not support effective
  this.dyn_ltree  = new Uint16Array(HEAP_SIZE * 2);
  this.dyn_dtree  = new Uint16Array((2 * D_CODES + 1) * 2);
  this.bl_tree    = new Uint16Array((2 * BL_CODES + 1) * 2);
  zero(this.dyn_ltree);
  zero(this.dyn_dtree);
  zero(this.bl_tree);

  this.l_desc   = null;         /* desc. for literal tree */
  this.d_desc   = null;         /* desc. for distance tree */
  this.bl_desc  = null;         /* desc. for bit length tree */

  //ush bl_count[MAX_BITS+1];
  this.bl_count = new Uint16Array(MAX_BITS + 1);
  /* number of codes at each bit length for an optimal tree */

  //int heap[2*L_CODES+1];      /* heap used to build the Huffman trees */
  this.heap = new Uint16Array(2 * L_CODES + 1);  /* heap used to build the Huffman trees */
  zero(this.heap);

  this.heap_len = 0;               /* number of elements in the heap */
  this.heap_max = 0;               /* element of largest frequency */
  /* The sons of heap[n] are heap[2*n] and heap[2*n+1]. heap[0] is not used.
   * The same heap array is used to build all trees.
   */

  this.depth = new Uint16Array(2 * L_CODES + 1); //uch depth[2*L_CODES+1];
  zero(this.depth);
  /* Depth of each subtree used as tie breaker for trees of equal frequency
   */

  this.sym_buf = 0;        /* buffer for distances and literals/lengths */

  this.lit_bufsize = 0;
  /* Size of match buffer for literals/lengths.  There are 4 reasons for
   * limiting lit_bufsize to 64K:
   *   - frequencies can be kept in 16 bit counters
   *   - if compression is not successful for the first block, all input
   *     data is still in the window so we can still emit a stored block even
   *     when input comes from standard input.  (This can also be done for
   *     all blocks if lit_bufsize is not greater than 32K.)
   *   - if compression is not successful for a file smaller than 64K, we can
   *     even emit a stored file instead of a stored block (saving 5 bytes).
   *     This is applicable only for zip (not gzip or zlib).
   *   - creating new Huffman trees less frequently may not provide fast
   *     adaptation to changes in the input data statistics. (Take for
   *     example a binary file with poorly compressible code followed by
   *     a highly compressible string table.) Smaller buffer sizes give
   *     fast adaptation but have of course the overhead of transmitting
   *     trees more frequently.
   *   - I can't count above 4
   */

  this.sym_next = 0;      /* running index in sym_buf */
  this.sym_end = 0;       /* symbol table full when sym_next reaches this */

  this.opt_len = 0;       /* bit length of current block with optimal trees */
  this.static_len = 0;    /* bit length of current block with static trees */
  this.matches = 0;       /* number of string matches in current block */
  this.insert = 0;        /* bytes at end of window left to insert */


  this.bi_buf = 0;
  /* Output buffer. bits are inserted starting at the bottom (least
   * significant bits).
   */
  this.bi_valid = 0;
  /* Number of valid bits in bi_buf.  All bits above the last valid bit
   * are always zero.
   */

  // Used for window memory init. We safely ignore it for JS. That makes
  // sense only for pointers and memory check tools.
  //this.high_water = 0;
  /* High water mark offset in window for initialized bytes -- bytes above
   * this are set to zero in order to avoid memory check warnings when
   * longest match routines access bytes past the input.  This is then
   * updated to the new high water mark.
   */
}


/* =========================================================================
 * Check for a valid deflate stream state. Return 0 if ok, 1 if not.
 */
const deflateStateCheck = (strm) => {

  if (!strm) {
    return 1;
  }
  const s = strm.state;
  if (!s || s.strm !== strm || (s.status !== INIT_STATE &&
//#ifdef GZIP
                                s.status !== GZIP_STATE &&
//#endif
                                s.status !== EXTRA_STATE &&
                                s.status !== NAME_STATE &&
                                s.status !== COMMENT_STATE &&
                                s.status !== HCRC_STATE &&
                                s.status !== BUSY_STATE &&
                                s.status !== FINISH_STATE)) {
    return 1;
  }
  return 0;
};


const deflateResetKeep = (strm) => {

  if (deflateStateCheck(strm)) {
    return err(strm, Z_STREAM_ERROR$2);
  }

  strm.total_in = strm.total_out = 0;
  strm.data_type = Z_UNKNOWN;

  const s = strm.state;
  s.pending = 0;
  s.pending_out = 0;

  if (s.wrap < 0) {
    s.wrap = -s.wrap;
    /* was made negative by deflate(..., Z_FINISH); */
  }
  s.status =
//#ifdef GZIP
    s.wrap === 2 ? GZIP_STATE :
//#endif
    s.wrap ? INIT_STATE : BUSY_STATE;
  strm.adler = (s.wrap === 2) ?
    0  // crc32(0, Z_NULL, 0)
  :
    1; // adler32(0, Z_NULL, 0)
  s.last_flush = -2;
  _tr_init(s);
  return Z_OK$3;
};


const deflateReset = (strm) => {

  const ret = deflateResetKeep(strm);
  if (ret === Z_OK$3) {
    lm_init(strm.state);
  }
  return ret;
};


const deflateSetHeader = (strm, head) => {

  if (deflateStateCheck(strm) || strm.state.wrap !== 2) {
    return Z_STREAM_ERROR$2;
  }
  strm.state.gzhead = head;
  return Z_OK$3;
};


const deflateInit2 = (strm, level, method, windowBits, memLevel, strategy) => {

  if (!strm) { // === Z_NULL
    return Z_STREAM_ERROR$2;
  }
  let wrap = 1;

  if (level === Z_DEFAULT_COMPRESSION$1) {
    level = 6;
  }

  if (windowBits < 0) { /* suppress zlib wrapper */
    wrap = 0;
    windowBits = -windowBits;
  }

  else if (windowBits > 15) {
    wrap = 2;           /* write gzip wrapper instead */
    windowBits -= 16;
  }


  if (memLevel < 1 || memLevel > MAX_MEM_LEVEL || method !== Z_DEFLATED$2 ||
    windowBits < 8 || windowBits > 15 || level < 0 || level > 9 ||
    strategy < 0 || strategy > Z_FIXED || (windowBits === 8 && wrap !== 1)) {
    return err(strm, Z_STREAM_ERROR$2);
  }


  if (windowBits === 8) {
    windowBits = 9;
  }
  /* until 256-byte window bug fixed */

  const s = new DeflateState();

  strm.state = s;
  s.strm = strm;
  s.status = INIT_STATE;     /* to pass state test in deflateReset() */

  s.wrap = wrap;
  s.gzhead = null;
  s.w_bits = windowBits;
  s.w_size = 1 << s.w_bits;
  s.w_mask = s.w_size - 1;

  s.hash_bits = memLevel + 7;
  s.hash_size = 1 << s.hash_bits;
  s.hash_mask = s.hash_size - 1;
  s.hash_shift = ~~((s.hash_bits + MIN_MATCH - 1) / MIN_MATCH);

  s.window = new Uint8Array(s.w_size * 2);
  s.head = new Uint16Array(s.hash_size);
  s.prev = new Uint16Array(s.w_size);

  // Don't need mem init magic for JS.
  //s.high_water = 0;  /* nothing written to s->window yet */

  s.lit_bufsize = 1 << (memLevel + 6); /* 16K elements by default */

  /* We overlay pending_buf and sym_buf. This works since the average size
   * for length/distance pairs over any compressed block is assured to be 31
   * bits or less.
   *
   * Analysis: The longest fixed codes are a length code of 8 bits plus 5
   * extra bits, for lengths 131 to 257. The longest fixed distance codes are
   * 5 bits plus 13 extra bits, for distances 16385 to 32768. The longest
   * possible fixed-codes length/distance pair is then 31 bits total.
   *
   * sym_buf starts one-fourth of the way into pending_buf. So there are
   * three bytes in sym_buf for every four bytes in pending_buf. Each symbol
   * in sym_buf is three bytes -- two for the distance and one for the
   * literal/length. As each symbol is consumed, the pointer to the next
   * sym_buf value to read moves forward three bytes. From that symbol, up to
   * 31 bits are written to pending_buf. The closest the written pending_buf
   * bits gets to the next sym_buf symbol to read is just before the last
   * code is written. At that time, 31*(n-2) bits have been written, just
   * after 24*(n-2) bits have been consumed from sym_buf. sym_buf starts at
   * 8*n bits into pending_buf. (Note that the symbol buffer fills when n-1
   * symbols are written.) The closest the writing gets to what is unread is
   * then n+14 bits. Here n is lit_bufsize, which is 16384 by default, and
   * can range from 128 to 32768.
   *
   * Therefore, at a minimum, there are 142 bits of space between what is
   * written and what is read in the overlain buffers, so the symbols cannot
   * be overwritten by the compressed data. That space is actually 139 bits,
   * due to the three-bit fixed-code block header.
   *
   * That covers the case where either Z_FIXED is specified, forcing fixed
   * codes, or when the use of fixed codes is chosen, because that choice
   * results in a smaller compressed block than dynamic codes. That latter
   * condition then assures that the above analysis also covers all dynamic
   * blocks. A dynamic-code block will only be chosen to be emitted if it has
   * fewer bits than a fixed-code block would for the same set of symbols.
   * Therefore its average symbol length is assured to be less than 31. So
   * the compressed data for a dynamic block also cannot overwrite the
   * symbols from which it is being constructed.
   */

  s.pending_buf_size = s.lit_bufsize * 4;
  s.pending_buf = new Uint8Array(s.pending_buf_size);

  // It is offset from `s.pending_buf` (size is `s.lit_bufsize * 2`)
  //s->sym_buf = s->pending_buf + s->lit_bufsize;
  s.sym_buf = s.lit_bufsize;

  //s->sym_end = (s->lit_bufsize - 1) * 3;
  s.sym_end = (s.lit_bufsize - 1) * 3;
  /* We avoid equality with lit_bufsize*3 because of wraparound at 64K
   * on 16 bit machines and because stored blocks are restricted to
   * 64K-1 bytes.
   */

  s.level = level;
  s.strategy = strategy;
  s.method = method;

  return deflateReset(strm);
};

const deflateInit = (strm, level) => {

  return deflateInit2(strm, level, Z_DEFLATED$2, MAX_WBITS$1, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY$1);
};


/* ========================================================================= */
const deflate$2 = (strm, flush) => {

  if (deflateStateCheck(strm) || flush > Z_BLOCK$1 || flush < 0) {
    return strm ? err(strm, Z_STREAM_ERROR$2) : Z_STREAM_ERROR$2;
  }

  const s = strm.state;

  if (!strm.output ||
      (strm.avail_in !== 0 && !strm.input) ||
      (s.status === FINISH_STATE && flush !== Z_FINISH$3)) {
    return err(strm, (strm.avail_out === 0) ? Z_BUF_ERROR$1 : Z_STREAM_ERROR$2);
  }

  const old_flush = s.last_flush;
  s.last_flush = flush;

  /* Flush as much pending output as possible */
  if (s.pending !== 0) {
    flush_pending(strm);
    if (strm.avail_out === 0) {
      /* Since avail_out is 0, deflate will be called again with
       * more output space, but possibly with both pending and
       * avail_in equal to zero. There won't be anything to do,
       * but this is not an error situation so make sure we
       * return OK instead of BUF_ERROR at next call of deflate:
       */
      s.last_flush = -1;
      return Z_OK$3;
    }

    /* Make sure there is something to do and avoid duplicate consecutive
     * flushes. For repeated and useless calls with Z_FINISH, we keep
     * returning Z_STREAM_END instead of Z_BUF_ERROR.
     */
  } else if (strm.avail_in === 0 && rank(flush) <= rank(old_flush) &&
    flush !== Z_FINISH$3) {
    return err(strm, Z_BUF_ERROR$1);
  }

  /* User must not provide more input after the first FINISH: */
  if (s.status === FINISH_STATE && strm.avail_in !== 0) {
    return err(strm, Z_BUF_ERROR$1);
  }

  /* Write the header */
  if (s.status === INIT_STATE && s.wrap === 0) {
    s.status = BUSY_STATE;
  }
  if (s.status === INIT_STATE) {
    /* zlib header */
    let header = (Z_DEFLATED$2 + ((s.w_bits - 8) << 4)) << 8;
    let level_flags = -1;

    if (s.strategy >= Z_HUFFMAN_ONLY || s.level < 2) {
      level_flags = 0;
    } else if (s.level < 6) {
      level_flags = 1;
    } else if (s.level === 6) {
      level_flags = 2;
    } else {
      level_flags = 3;
    }
    header |= (level_flags << 6);
    if (s.strstart !== 0) { header |= PRESET_DICT; }
    header += 31 - (header % 31);

    putShortMSB(s, header);

    /* Save the adler32 of the preset dictionary: */
    if (s.strstart !== 0) {
      putShortMSB(s, strm.adler >>> 16);
      putShortMSB(s, strm.adler & 0xffff);
    }
    strm.adler = 1; // adler32(0L, Z_NULL, 0);
    s.status = BUSY_STATE;

    /* Compression must start with an empty pending buffer */
    flush_pending(strm);
    if (s.pending !== 0) {
      s.last_flush = -1;
      return Z_OK$3;
    }
  }
//#ifdef GZIP
  if (s.status === GZIP_STATE) {
    /* gzip header */
    strm.adler = 0;  //crc32(0L, Z_NULL, 0);
    put_byte(s, 31);
    put_byte(s, 139);
    put_byte(s, 8);
    if (!s.gzhead) { // s->gzhead == Z_NULL
      put_byte(s, 0);
      put_byte(s, 0);
      put_byte(s, 0);
      put_byte(s, 0);
      put_byte(s, 0);
      put_byte(s, s.level === 9 ? 2 :
                  (s.strategy >= Z_HUFFMAN_ONLY || s.level < 2 ?
                   4 : 0));
      put_byte(s, OS_CODE);
      s.status = BUSY_STATE;

      /* Compression must start with an empty pending buffer */
      flush_pending(strm);
      if (s.pending !== 0) {
        s.last_flush = -1;
        return Z_OK$3;
      }
    }
    else {
      put_byte(s, (s.gzhead.text ? 1 : 0) +
                  (s.gzhead.hcrc ? 2 : 0) +
                  (!s.gzhead.extra ? 0 : 4) +
                  (!s.gzhead.name ? 0 : 8) +
                  (!s.gzhead.comment ? 0 : 16)
      );
      put_byte(s, s.gzhead.time & 0xff);
      put_byte(s, (s.gzhead.time >> 8) & 0xff);
      put_byte(s, (s.gzhead.time >> 16) & 0xff);
      put_byte(s, (s.gzhead.time >> 24) & 0xff);
      put_byte(s, s.level === 9 ? 2 :
                  (s.strategy >= Z_HUFFMAN_ONLY || s.level < 2 ?
                   4 : 0));
      put_byte(s, s.gzhead.os & 0xff);
      if (s.gzhead.extra && s.gzhead.extra.length) {
        put_byte(s, s.gzhead.extra.length & 0xff);
        put_byte(s, (s.gzhead.extra.length >> 8) & 0xff);
      }
      if (s.gzhead.hcrc) {
        strm.adler = crc32_1(strm.adler, s.pending_buf, s.pending, 0);
      }
      s.gzindex = 0;
      s.status = EXTRA_STATE;
    }
  }
  if (s.status === EXTRA_STATE) {
    if (s.gzhead.extra/* != Z_NULL*/) {
      let beg = s.pending;   /* start of bytes to update crc */
      let left = (s.gzhead.extra.length & 0xffff) - s.gzindex;
      while (s.pending + left > s.pending_buf_size) {
        let copy = s.pending_buf_size - s.pending;
        // zmemcpy(s.pending_buf + s.pending,
        //    s.gzhead.extra + s.gzindex, copy);
        s.pending_buf.set(s.gzhead.extra.subarray(s.gzindex, s.gzindex + copy), s.pending);
        s.pending = s.pending_buf_size;
        //--- HCRC_UPDATE(beg) ---//
        if (s.gzhead.hcrc && s.pending > beg) {
          strm.adler = crc32_1(strm.adler, s.pending_buf, s.pending - beg, beg);
        }
        //---//
        s.gzindex += copy;
        flush_pending(strm);
        if (s.pending !== 0) {
          s.last_flush = -1;
          return Z_OK$3;
        }
        beg = 0;
        left -= copy;
      }
      // JS specific: s.gzhead.extra may be TypedArray or Array for backward compatibility
      //              TypedArray.slice and TypedArray.from don't exist in IE10-IE11
      let gzhead_extra = new Uint8Array(s.gzhead.extra);
      // zmemcpy(s->pending_buf + s->pending,
      //     s->gzhead->extra + s->gzindex, left);
      s.pending_buf.set(gzhead_extra.subarray(s.gzindex, s.gzindex + left), s.pending);
      s.pending += left;
      //--- HCRC_UPDATE(beg) ---//
      if (s.gzhead.hcrc && s.pending > beg) {
        strm.adler = crc32_1(strm.adler, s.pending_buf, s.pending - beg, beg);
      }
      //---//
      s.gzindex = 0;
    }
    s.status = NAME_STATE;
  }
  if (s.status === NAME_STATE) {
    if (s.gzhead.name/* != Z_NULL*/) {
      let beg = s.pending;   /* start of bytes to update crc */
      let val;
      do {
        if (s.pending === s.pending_buf_size) {
          //--- HCRC_UPDATE(beg) ---//
          if (s.gzhead.hcrc && s.pending > beg) {
            strm.adler = crc32_1(strm.adler, s.pending_buf, s.pending - beg, beg);
          }
          //---//
          flush_pending(strm);
          if (s.pending !== 0) {
            s.last_flush = -1;
            return Z_OK$3;
          }
          beg = 0;
        }
        // JS specific: little magic to add zero terminator to end of string
        if (s.gzindex < s.gzhead.name.length) {
          val = s.gzhead.name.charCodeAt(s.gzindex++) & 0xff;
        } else {
          val = 0;
        }
        put_byte(s, val);
      } while (val !== 0);
      //--- HCRC_UPDATE(beg) ---//
      if (s.gzhead.hcrc && s.pending > beg) {
        strm.adler = crc32_1(strm.adler, s.pending_buf, s.pending - beg, beg);
      }
      //---//
      s.gzindex = 0;
    }
    s.status = COMMENT_STATE;
  }
  if (s.status === COMMENT_STATE) {
    if (s.gzhead.comment/* != Z_NULL*/) {
      let beg = s.pending;   /* start of bytes to update crc */
      let val;
      do {
        if (s.pending === s.pending_buf_size) {
          //--- HCRC_UPDATE(beg) ---//
          if (s.gzhead.hcrc && s.pending > beg) {
            strm.adler = crc32_1(strm.adler, s.pending_buf, s.pending - beg, beg);
          }
          //---//
          flush_pending(strm);
          if (s.pending !== 0) {
            s.last_flush = -1;
            return Z_OK$3;
          }
          beg = 0;
        }
        // JS specific: little magic to add zero terminator to end of string
        if (s.gzindex < s.gzhead.comment.length) {
          val = s.gzhead.comment.charCodeAt(s.gzindex++) & 0xff;
        } else {
          val = 0;
        }
        put_byte(s, val);
      } while (val !== 0);
      //--- HCRC_UPDATE(beg) ---//
      if (s.gzhead.hcrc && s.pending > beg) {
        strm.adler = crc32_1(strm.adler, s.pending_buf, s.pending - beg, beg);
      }
      //---//
    }
    s.status = HCRC_STATE;
  }
  if (s.status === HCRC_STATE) {
    if (s.gzhead.hcrc) {
      if (s.pending + 2 > s.pending_buf_size) {
        flush_pending(strm);
        if (s.pending !== 0) {
          s.last_flush = -1;
          return Z_OK$3;
        }
      }
      put_byte(s, strm.adler & 0xff);
      put_byte(s, (strm.adler >> 8) & 0xff);
      strm.adler = 0; //crc32(0L, Z_NULL, 0);
    }
    s.status = BUSY_STATE;

    /* Compression must start with an empty pending buffer */
    flush_pending(strm);
    if (s.pending !== 0) {
      s.last_flush = -1;
      return Z_OK$3;
    }
  }
//#endif

  /* Start a new block or continue the current one.
   */
  if (strm.avail_in !== 0 || s.lookahead !== 0 ||
    (flush !== Z_NO_FLUSH$2 && s.status !== FINISH_STATE)) {
    let bstate = s.level === 0 ? deflate_stored(s, flush) :
                 s.strategy === Z_HUFFMAN_ONLY ? deflate_huff(s, flush) :
                 s.strategy === Z_RLE ? deflate_rle(s, flush) :
                 configuration_table[s.level].func(s, flush);

    if (bstate === BS_FINISH_STARTED || bstate === BS_FINISH_DONE) {
      s.status = FINISH_STATE;
    }
    if (bstate === BS_NEED_MORE || bstate === BS_FINISH_STARTED) {
      if (strm.avail_out === 0) {
        s.last_flush = -1;
        /* avoid BUF_ERROR next call, see above */
      }
      return Z_OK$3;
      /* If flush != Z_NO_FLUSH && avail_out == 0, the next call
       * of deflate should use the same flush parameter to make sure
       * that the flush is complete. So we don't have to output an
       * empty block here, this will be done at next call. This also
       * ensures that for a very small output buffer, we emit at most
       * one empty block.
       */
    }
    if (bstate === BS_BLOCK_DONE) {
      if (flush === Z_PARTIAL_FLUSH) {
        _tr_align(s);
      }
      else if (flush !== Z_BLOCK$1) { /* FULL_FLUSH or SYNC_FLUSH */

        _tr_stored_block(s, 0, 0, false);
        /* For a full flush, this empty block will be recognized
         * as a special marker by inflate_sync().
         */
        if (flush === Z_FULL_FLUSH$1) {
          /*** CLEAR_HASH(s); ***/             /* forget history */
          zero(s.head); // Fill with NIL (= 0);

          if (s.lookahead === 0) {
            s.strstart = 0;
            s.block_start = 0;
            s.insert = 0;
          }
        }
      }
      flush_pending(strm);
      if (strm.avail_out === 0) {
        s.last_flush = -1; /* avoid BUF_ERROR at next call, see above */
        return Z_OK$3;
      }
    }
  }

  if (flush !== Z_FINISH$3) { return Z_OK$3; }
  if (s.wrap <= 0) { return Z_STREAM_END$3; }

  /* Write the trailer */
  if (s.wrap === 2) {
    put_byte(s, strm.adler & 0xff);
    put_byte(s, (strm.adler >> 8) & 0xff);
    put_byte(s, (strm.adler >> 16) & 0xff);
    put_byte(s, (strm.adler >> 24) & 0xff);
    put_byte(s, strm.total_in & 0xff);
    put_byte(s, (strm.total_in >> 8) & 0xff);
    put_byte(s, (strm.total_in >> 16) & 0xff);
    put_byte(s, (strm.total_in >> 24) & 0xff);
  }
  else
  {
    putShortMSB(s, strm.adler >>> 16);
    putShortMSB(s, strm.adler & 0xffff);
  }

  flush_pending(strm);
  /* If avail_out is zero, the application will call deflate again
   * to flush the rest.
   */
  if (s.wrap > 0) { s.wrap = -s.wrap; }
  /* write the trailer only once! */
  return s.pending !== 0 ? Z_OK$3 : Z_STREAM_END$3;
};


const deflateEnd = (strm) => {

  if (deflateStateCheck(strm)) {
    return Z_STREAM_ERROR$2;
  }

  const status = strm.state.status;

  strm.state = null;

  return status === BUSY_STATE ? err(strm, Z_DATA_ERROR$2) : Z_OK$3;
};


/* =========================================================================
 * Initializes the compression dictionary from the given byte
 * sequence without producing any compressed output.
 */
const deflateSetDictionary = (strm, dictionary) => {

  let dictLength = dictionary.length;

  if (deflateStateCheck(strm)) {
    return Z_STREAM_ERROR$2;
  }

  const s = strm.state;
  const wrap = s.wrap;

  if (wrap === 2 || (wrap === 1 && s.status !== INIT_STATE) || s.lookahead) {
    return Z_STREAM_ERROR$2;
  }

  /* when using zlib wrappers, compute Adler-32 for provided dictionary */
  if (wrap === 1) {
    /* adler32(strm->adler, dictionary, dictLength); */
    strm.adler = adler32_1(strm.adler, dictionary, dictLength, 0);
  }

  s.wrap = 0;   /* avoid computing Adler-32 in read_buf */

  /* if dictionary would fill window, just replace the history */
  if (dictLength >= s.w_size) {
    if (wrap === 0) {            /* already empty otherwise */
      /*** CLEAR_HASH(s); ***/
      zero(s.head); // Fill with NIL (= 0);
      s.strstart = 0;
      s.block_start = 0;
      s.insert = 0;
    }
    /* use the tail */
    // dictionary = dictionary.slice(dictLength - s.w_size);
    let tmpDict = new Uint8Array(s.w_size);
    tmpDict.set(dictionary.subarray(dictLength - s.w_size, dictLength), 0);
    dictionary = tmpDict;
    dictLength = s.w_size;
  }
  /* insert dictionary into window and hash */
  const avail = strm.avail_in;
  const next = strm.next_in;
  const input = strm.input;
  strm.avail_in = dictLength;
  strm.next_in = 0;
  strm.input = dictionary;
  fill_window(s);
  while (s.lookahead >= MIN_MATCH) {
    let str = s.strstart;
    let n = s.lookahead - (MIN_MATCH - 1);
    do {
      /* UPDATE_HASH(s, s->ins_h, s->window[str + MIN_MATCH-1]); */
      s.ins_h = HASH(s, s.ins_h, s.window[str + MIN_MATCH - 1]);

      s.prev[str & s.w_mask] = s.head[s.ins_h];

      s.head[s.ins_h] = str;
      str++;
    } while (--n);
    s.strstart = str;
    s.lookahead = MIN_MATCH - 1;
    fill_window(s);
  }
  s.strstart += s.lookahead;
  s.block_start = s.strstart;
  s.insert = s.lookahead;
  s.lookahead = 0;
  s.match_length = s.prev_length = MIN_MATCH - 1;
  s.match_available = 0;
  strm.next_in = next;
  strm.input = input;
  strm.avail_in = avail;
  s.wrap = wrap;
  return Z_OK$3;
};


var deflateInit_1 = deflateInit;
var deflateInit2_1 = deflateInit2;
var deflateReset_1 = deflateReset;
var deflateResetKeep_1 = deflateResetKeep;
var deflateSetHeader_1 = deflateSetHeader;
var deflate_2$1 = deflate$2;
var deflateEnd_1 = deflateEnd;
var deflateSetDictionary_1 = deflateSetDictionary;
var deflateInfo = 'pako deflate (from Nodeca project)';

/* Not implemented
module.exports.deflateBound = deflateBound;
module.exports.deflateCopy = deflateCopy;
module.exports.deflateGetDictionary = deflateGetDictionary;
module.exports.deflateParams = deflateParams;
module.exports.deflatePending = deflatePending;
module.exports.deflatePrime = deflatePrime;
module.exports.deflateTune = deflateTune;
*/

var deflate_1$2 = {
	deflateInit: deflateInit_1,
	deflateInit2: deflateInit2_1,
	deflateReset: deflateReset_1,
	deflateResetKeep: deflateResetKeep_1,
	deflateSetHeader: deflateSetHeader_1,
	deflate: deflate_2$1,
	deflateEnd: deflateEnd_1,
	deflateSetDictionary: deflateSetDictionary_1,
	deflateInfo: deflateInfo
};

const _has = (obj, key) => {
  return Object.prototype.hasOwnProperty.call(obj, key);
};

var assign = function (obj /*from1, from2, from3, ...*/) {
  const sources = Array.prototype.slice.call(arguments, 1);
  while (sources.length) {
    const source = sources.shift();
    if (!source) { continue; }

    if (typeof source !== 'object') {
      throw new TypeError(source + 'must be non-object');
    }

    for (const p in source) {
      if (_has(source, p)) {
        obj[p] = source[p];
      }
    }
  }

  return obj;
};


// Join array of chunks to single array.
var flattenChunks = (chunks) => {
  // calculate data length
  let len = 0;

  for (let i = 0, l = chunks.length; i < l; i++) {
    len += chunks[i].length;
  }

  // join chunks
  const result = new Uint8Array(len);

  for (let i = 0, pos = 0, l = chunks.length; i < l; i++) {
    let chunk = chunks[i];
    result.set(chunk, pos);
    pos += chunk.length;
  }

  return result;
};

var common = {
	assign: assign,
	flattenChunks: flattenChunks
};

// String encode/decode helpers


// Quick check if we can use fast array to bin string conversion
//
// - apply(Array) can fail on Android 2.2
// - apply(Uint8Array) can fail on iOS 5.1 Safari
//
let STR_APPLY_UIA_OK = true;

try { String.fromCharCode.apply(null, new Uint8Array(1)); } catch (__) { STR_APPLY_UIA_OK = false; }


// Table with utf8 lengths (calculated by first byte of sequence)
// Note, that 5 & 6-byte values and some 4-byte values can not be represented in JS,
// because max possible codepoint is 0x10ffff
const _utf8len = new Uint8Array(256);
for (let q = 0; q < 256; q++) {
  _utf8len[q] = (q >= 252 ? 6 : q >= 248 ? 5 : q >= 240 ? 4 : q >= 224 ? 3 : q >= 192 ? 2 : 1);
}
_utf8len[254] = _utf8len[254] = 1; // Invalid sequence start


// convert string to array (typed, when possible)
var string2buf = (str) => {
  if (typeof TextEncoder === 'function' && TextEncoder.prototype.encode) {
    return new TextEncoder().encode(str);
  }

  let buf, c, c2, m_pos, i, str_len = str.length, buf_len = 0;

  // count binary size
  for (m_pos = 0; m_pos < str_len; m_pos++) {
    c = str.charCodeAt(m_pos);
    if ((c & 0xfc00) === 0xd800 && (m_pos + 1 < str_len)) {
      c2 = str.charCodeAt(m_pos + 1);
      if ((c2 & 0xfc00) === 0xdc00) {
        c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
        m_pos++;
      }
    }
    buf_len += c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0x10000 ? 3 : 4;
  }

  // allocate buffer
  buf = new Uint8Array(buf_len);

  // convert
  for (i = 0, m_pos = 0; i < buf_len; m_pos++) {
    c = str.charCodeAt(m_pos);
    if ((c & 0xfc00) === 0xd800 && (m_pos + 1 < str_len)) {
      c2 = str.charCodeAt(m_pos + 1);
      if ((c2 & 0xfc00) === 0xdc00) {
        c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
        m_pos++;
      }
    }
    if (c < 0x80) {
      /* one byte */
      buf[i++] = c;
    } else if (c < 0x800) {
      /* two bytes */
      buf[i++] = 0xC0 | (c >>> 6);
      buf[i++] = 0x80 | (c & 0x3f);
    } else if (c < 0x10000) {
      /* three bytes */
      buf[i++] = 0xE0 | (c >>> 12);
      buf[i++] = 0x80 | (c >>> 6 & 0x3f);
      buf[i++] = 0x80 | (c & 0x3f);
    } else {
      /* four bytes */
      buf[i++] = 0xf0 | (c >>> 18);
      buf[i++] = 0x80 | (c >>> 12 & 0x3f);
      buf[i++] = 0x80 | (c >>> 6 & 0x3f);
      buf[i++] = 0x80 | (c & 0x3f);
    }
  }

  return buf;
};

// Helper
const buf2binstring = (buf, len) => {
  // On Chrome, the arguments in a function call that are allowed is `65534`.
  // If the length of the buffer is smaller than that, we can use this optimization,
  // otherwise we will take a slower path.
  if (len < 65534) {
    if (buf.subarray && STR_APPLY_UIA_OK) {
      return String.fromCharCode.apply(null, buf.length === len ? buf : buf.subarray(0, len));
    }
  }

  let result = '';
  for (let i = 0; i < len; i++) {
    result += String.fromCharCode(buf[i]);
  }
  return result;
};


// convert array to string
var buf2string = (buf, max) => {
  const len = max || buf.length;

  if (typeof TextDecoder === 'function' && TextDecoder.prototype.decode) {
    return new TextDecoder().decode(buf.subarray(0, max));
  }

  let i, out;

  // Reserve max possible length (2 words per char)
  // NB: by unknown reasons, Array is significantly faster for
  //     String.fromCharCode.apply than Uint16Array.
  const utf16buf = new Array(len * 2);

  for (out = 0, i = 0; i < len;) {
    let c = buf[i++];
    // quick process ascii
    if (c < 0x80) { utf16buf[out++] = c; continue; }

    let c_len = _utf8len[c];
    // skip 5 & 6 byte codes
    if (c_len > 4) { utf16buf[out++] = 0xfffd; i += c_len - 1; continue; }

    // apply mask on first byte
    c &= c_len === 2 ? 0x1f : c_len === 3 ? 0x0f : 0x07;
    // join the rest
    while (c_len > 1 && i < len) {
      c = (c << 6) | (buf[i++] & 0x3f);
      c_len--;
    }

    // terminated by end of string?
    if (c_len > 1) { utf16buf[out++] = 0xfffd; continue; }

    if (c < 0x10000) {
      utf16buf[out++] = c;
    } else {
      c -= 0x10000;
      utf16buf[out++] = 0xd800 | ((c >> 10) & 0x3ff);
      utf16buf[out++] = 0xdc00 | (c & 0x3ff);
    }
  }

  return buf2binstring(utf16buf, out);
};


// Calculate max possible position in utf8 buffer,
// that will not break sequence. If that's not possible
// - (very small limits) return max size as is.
//
// buf[] - utf8 bytes array
// max   - length limit (mandatory);
var utf8border = (buf, max) => {

  max = max || buf.length;
  if (max > buf.length) { max = buf.length; }

  // go back from last position, until start of sequence found
  let pos = max - 1;
  while (pos >= 0 && (buf[pos] & 0xC0) === 0x80) { pos--; }

  // Very small and broken sequence,
  // return max, because we should return something anyway.
  if (pos < 0) { return max; }

  // If we came to start of buffer - that means buffer is too small,
  // return max too.
  if (pos === 0) { return max; }

  return (pos + _utf8len[buf[pos]] > max) ? pos : max;
};

var strings = {
	string2buf: string2buf,
	buf2string: buf2string,
	utf8border: utf8border
};

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

function ZStream() {
  /* next input byte */
  this.input = null; // JS specific, because we have no pointers
  this.next_in = 0;
  /* number of bytes available at input */
  this.avail_in = 0;
  /* total number of input bytes read so far */
  this.total_in = 0;
  /* next output byte should be put there */
  this.output = null; // JS specific, because we have no pointers
  this.next_out = 0;
  /* remaining free space at output */
  this.avail_out = 0;
  /* total number of bytes output so far */
  this.total_out = 0;
  /* last error message, NULL if no error */
  this.msg = ''/*Z_NULL*/;
  /* not visible by applications */
  this.state = null;
  /* best guess about the data type: binary or text */
  this.data_type = 2/*Z_UNKNOWN*/;
  /* adler32 value of the uncompressed data */
  this.adler = 0;
}

var zstream = ZStream;

const toString$1 = Object.prototype.toString;

/* Public constants ==========================================================*/
/* ===========================================================================*/

const {
  Z_NO_FLUSH: Z_NO_FLUSH$1, Z_SYNC_FLUSH, Z_FULL_FLUSH, Z_FINISH: Z_FINISH$2,
  Z_OK: Z_OK$2, Z_STREAM_END: Z_STREAM_END$2,
  Z_DEFAULT_COMPRESSION,
  Z_DEFAULT_STRATEGY,
  Z_DEFLATED: Z_DEFLATED$1
} = constants$2;

/* ===========================================================================*/


/**
 * class Deflate
 *
 * Generic JS-style wrapper for zlib calls. If you don't need
 * streaming behaviour - use more simple functions: [[deflate]],
 * [[deflateRaw]] and [[gzip]].
 **/

/* internal
 * Deflate.chunks -> Array
 *
 * Chunks of output data, if [[Deflate#onData]] not overridden.
 **/

/**
 * Deflate.result -> Uint8Array
 *
 * Compressed result, generated by default [[Deflate#onData]]
 * and [[Deflate#onEnd]] handlers. Filled after you push last chunk
 * (call [[Deflate#push]] with `Z_FINISH` / `true` param).
 **/

/**
 * Deflate.err -> Number
 *
 * Error code after deflate finished. 0 (Z_OK) on success.
 * You will not need it in real life, because deflate errors
 * are possible only on wrong options or bad `onData` / `onEnd`
 * custom handlers.
 **/

/**
 * Deflate.msg -> String
 *
 * Error message, if [[Deflate.err]] != 0
 **/


/**
 * new Deflate(options)
 * - options (Object): zlib deflate options.
 *
 * Creates new deflator instance with specified params. Throws exception
 * on bad params. Supported options:
 *
 * - `level`
 * - `windowBits`
 * - `memLevel`
 * - `strategy`
 * - `dictionary`
 *
 * [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced)
 * for more information on these.
 *
 * Additional options, for internal needs:
 *
 * - `chunkSize` - size of generated data chunks (16K by default)
 * - `raw` (Boolean) - do raw deflate
 * - `gzip` (Boolean) - create gzip wrapper
 * - `header` (Object) - custom header for gzip
 *   - `text` (Boolean) - true if compressed data believed to be text
 *   - `time` (Number) - modification time, unix timestamp
 *   - `os` (Number) - operation system code
 *   - `extra` (Array) - array of bytes with extra data (max 65536)
 *   - `name` (String) - file name (binary string)
 *   - `comment` (String) - comment (binary string)
 *   - `hcrc` (Boolean) - true if header crc should be added
 *
 * ##### Example:
 *
 * ```javascript
 * const pako = require('pako')
 *   , chunk1 = new Uint8Array([1,2,3,4,5,6,7,8,9])
 *   , chunk2 = new Uint8Array([10,11,12,13,14,15,16,17,18,19]);
 *
 * const deflate = new pako.Deflate({ level: 3});
 *
 * deflate.push(chunk1, false);
 * deflate.push(chunk2, true);  // true -> last chunk
 *
 * if (deflate.err) { throw new Error(deflate.err); }
 *
 * console.log(deflate.result);
 * ```
 **/
function Deflate$1(options) {
  this.options = common.assign({
    level: Z_DEFAULT_COMPRESSION,
    method: Z_DEFLATED$1,
    chunkSize: 16384,
    windowBits: 15,
    memLevel: 8,
    strategy: Z_DEFAULT_STRATEGY
  }, options || {});

  let opt = this.options;

  if (opt.raw && (opt.windowBits > 0)) {
    opt.windowBits = -opt.windowBits;
  }

  else if (opt.gzip && (opt.windowBits > 0) && (opt.windowBits < 16)) {
    opt.windowBits += 16;
  }

  this.err    = 0;      // error code, if happens (0 = Z_OK)
  this.msg    = '';     // error message
  this.ended  = false;  // used to avoid multiple onEnd() calls
  this.chunks = [];     // chunks of compressed data

  this.strm = new zstream();
  this.strm.avail_out = 0;

  let status = deflate_1$2.deflateInit2(
    this.strm,
    opt.level,
    opt.method,
    opt.windowBits,
    opt.memLevel,
    opt.strategy
  );

  if (status !== Z_OK$2) {
    throw new Error(messages[status]);
  }

  if (opt.header) {
    deflate_1$2.deflateSetHeader(this.strm, opt.header);
  }

  if (opt.dictionary) {
    let dict;
    // Convert data if needed
    if (typeof opt.dictionary === 'string') {
      // If we need to compress text, change encoding to utf8.
      dict = strings.string2buf(opt.dictionary);
    } else if (toString$1.call(opt.dictionary) === '[object ArrayBuffer]') {
      dict = new Uint8Array(opt.dictionary);
    } else {
      dict = opt.dictionary;
    }

    status = deflate_1$2.deflateSetDictionary(this.strm, dict);

    if (status !== Z_OK$2) {
      throw new Error(messages[status]);
    }

    this._dict_set = true;
  }
}

/**
 * Deflate#push(data[, flush_mode]) -> Boolean
 * - data (Uint8Array|ArrayBuffer|String): input data. Strings will be
 *   converted to utf8 byte sequence.
 * - flush_mode (Number|Boolean): 0..6 for corresponding Z_NO_FLUSH..Z_TREE modes.
 *   See constants. Skipped or `false` means Z_NO_FLUSH, `true` means Z_FINISH.
 *
 * Sends input data to deflate pipe, generating [[Deflate#onData]] calls with
 * new compressed chunks. Returns `true` on success. The last data block must
 * have `flush_mode` Z_FINISH (or `true`). That will flush internal pending
 * buffers and call [[Deflate#onEnd]].
 *
 * On fail call [[Deflate#onEnd]] with error code and return false.
 *
 * ##### Example
 *
 * ```javascript
 * push(chunk, false); // push one of data chunks
 * ...
 * push(chunk, true);  // push last chunk
 * ```
 **/
Deflate$1.prototype.push = function (data, flush_mode) {
  const strm = this.strm;
  const chunkSize = this.options.chunkSize;
  let status, _flush_mode;

  if (this.ended) { return false; }

  if (flush_mode === ~~flush_mode) _flush_mode = flush_mode;
  else _flush_mode = flush_mode === true ? Z_FINISH$2 : Z_NO_FLUSH$1;

  // Convert data if needed
  if (typeof data === 'string') {
    // If we need to compress text, change encoding to utf8.
    strm.input = strings.string2buf(data);
  } else if (toString$1.call(data) === '[object ArrayBuffer]') {
    strm.input = new Uint8Array(data);
  } else {
    strm.input = data;
  }

  strm.next_in = 0;
  strm.avail_in = strm.input.length;

  for (;;) {
    if (strm.avail_out === 0) {
      strm.output = new Uint8Array(chunkSize);
      strm.next_out = 0;
      strm.avail_out = chunkSize;
    }

    // Make sure avail_out > 6 to avoid repeating markers
    if ((_flush_mode === Z_SYNC_FLUSH || _flush_mode === Z_FULL_FLUSH) && strm.avail_out <= 6) {
      this.onData(strm.output.subarray(0, strm.next_out));
      strm.avail_out = 0;
      continue;
    }

    status = deflate_1$2.deflate(strm, _flush_mode);

    // Ended => flush and finish
    if (status === Z_STREAM_END$2) {
      if (strm.next_out > 0) {
        this.onData(strm.output.subarray(0, strm.next_out));
      }
      status = deflate_1$2.deflateEnd(this.strm);
      this.onEnd(status);
      this.ended = true;
      return status === Z_OK$2;
    }

    // Flush if out buffer full
    if (strm.avail_out === 0) {
      this.onData(strm.output);
      continue;
    }

    // Flush if requested and has data
    if (_flush_mode > 0 && strm.next_out > 0) {
      this.onData(strm.output.subarray(0, strm.next_out));
      strm.avail_out = 0;
      continue;
    }

    if (strm.avail_in === 0) break;
  }

  return true;
};


/**
 * Deflate#onData(chunk) -> Void
 * - chunk (Uint8Array): output data.
 *
 * By default, stores data blocks in `chunks[]` property and glue
 * those in `onEnd`. Override this handler, if you need another behaviour.
 **/
Deflate$1.prototype.onData = function (chunk) {
  this.chunks.push(chunk);
};


/**
 * Deflate#onEnd(status) -> Void
 * - status (Number): deflate status. 0 (Z_OK) on success,
 *   other if not.
 *
 * Called once after you tell deflate that the input stream is
 * complete (Z_FINISH). By default - join collected chunks,
 * free memory and fill `results` / `err` properties.
 **/
Deflate$1.prototype.onEnd = function (status) {
  // On success - join
  if (status === Z_OK$2) {
    this.result = common.flattenChunks(this.chunks);
  }
  this.chunks = [];
  this.err = status;
  this.msg = this.strm.msg;
};

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

// See state defs from inflate.js
const BAD$1 = 16209;       /* got a data error -- remain here until reset */
const TYPE$1 = 16191;      /* i: waiting for type bits, including last-flag bit */

/*
   Decode literal, length, and distance codes and write out the resulting
   literal and match bytes until either not enough input or output is
   available, an end-of-block is encountered, or a data error is encountered.
   When large enough input and output buffers are supplied to inflate(), for
   example, a 16K input buffer and a 64K output buffer, more than 95% of the
   inflate execution time is spent in this routine.

   Entry assumptions:

        state.mode === LEN
        strm.avail_in >= 6
        strm.avail_out >= 258
        start >= strm.avail_out
        state.bits < 8

   On return, state.mode is one of:

        LEN -- ran out of enough output space or enough available input
        TYPE -- reached end of block code, inflate() to interpret next block
        BAD -- error in block data

   Notes:

    - The maximum input bits used by a length/distance pair is 15 bits for the
      length code, 5 bits for the length extra, 15 bits for the distance code,
      and 13 bits for the distance extra.  This totals 48 bits, or six bytes.
      Therefore if strm.avail_in >= 6, then there is enough input to avoid
      checking for available input while decoding.

    - The maximum bytes that a single length/distance pair can output is 258
      bytes, which is the maximum length that can be coded.  inflate_fast()
      requires strm.avail_out >= 258 for each loop to avoid checking for
      output space.
 */
var inffast = function inflate_fast(strm, start) {
  let _in;                    /* local strm.input */
  let last;                   /* have enough input while in < last */
  let _out;                   /* local strm.output */
  let beg;                    /* inflate()'s initial strm.output */
  let end;                    /* while out < end, enough space available */
//#ifdef INFLATE_STRICT
  let dmax;                   /* maximum distance from zlib header */
//#endif
  let wsize;                  /* window size or zero if not using window */
  let whave;                  /* valid bytes in the window */
  let wnext;                  /* window write index */
  // Use `s_window` instead `window`, avoid conflict with instrumentation tools
  let s_window;               /* allocated sliding window, if wsize != 0 */
  let hold;                   /* local strm.hold */
  let bits;                   /* local strm.bits */
  let lcode;                  /* local strm.lencode */
  let dcode;                  /* local strm.distcode */
  let lmask;                  /* mask for first level of length codes */
  let dmask;                  /* mask for first level of distance codes */
  let here;                   /* retrieved table entry */
  let op;                     /* code bits, operation, extra bits, or */
                              /*  window position, window bytes to copy */
  let len;                    /* match length, unused bytes */
  let dist;                   /* match distance */
  let from;                   /* where to copy match from */
  let from_source;


  let input, output; // JS specific, because we have no pointers

  /* copy state to local variables */
  const state = strm.state;
  //here = state.here;
  _in = strm.next_in;
  input = strm.input;
  last = _in + (strm.avail_in - 5);
  _out = strm.next_out;
  output = strm.output;
  beg = _out - (start - strm.avail_out);
  end = _out + (strm.avail_out - 257);
//#ifdef INFLATE_STRICT
  dmax = state.dmax;
//#endif
  wsize = state.wsize;
  whave = state.whave;
  wnext = state.wnext;
  s_window = state.window;
  hold = state.hold;
  bits = state.bits;
  lcode = state.lencode;
  dcode = state.distcode;
  lmask = (1 << state.lenbits) - 1;
  dmask = (1 << state.distbits) - 1;


  /* decode literals and length/distances until end-of-block or not enough
     input data or output space */

  top:
  do {
    if (bits < 15) {
      hold += input[_in++] << bits;
      bits += 8;
      hold += input[_in++] << bits;
      bits += 8;
    }

    here = lcode[hold & lmask];

    dolen:
    for (;;) { // Goto emulation
      op = here >>> 24/*here.bits*/;
      hold >>>= op;
      bits -= op;
      op = (here >>> 16) & 0xff/*here.op*/;
      if (op === 0) {                          /* literal */
        //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ?
        //        "inflate:         literal '%c'\n" :
        //        "inflate:         literal 0x%02x\n", here.val));
        output[_out++] = here & 0xffff/*here.val*/;
      }
      else if (op & 16) {                     /* length base */
        len = here & 0xffff/*here.val*/;
        op &= 15;                           /* number of extra bits */
        if (op) {
          if (bits < op) {
            hold += input[_in++] << bits;
            bits += 8;
          }
          len += hold & ((1 << op) - 1);
          hold >>>= op;
          bits -= op;
        }
        //Tracevv((stderr, "inflate:         length %u\n", len));
        if (bits < 15) {
          hold += input[_in++] << bits;
          bits += 8;
          hold += input[_in++] << bits;
          bits += 8;
        }
        here = dcode[hold & dmask];

        dodist:
        for (;;) { // goto emulation
          op = here >>> 24/*here.bits*/;
          hold >>>= op;
          bits -= op;
          op = (here >>> 16) & 0xff/*here.op*/;

          if (op & 16) {                      /* distance base */
            dist = here & 0xffff/*here.val*/;
            op &= 15;                       /* number of extra bits */
            if (bits < op) {
              hold += input[_in++] << bits;
              bits += 8;
              if (bits < op) {
                hold += input[_in++] << bits;
                bits += 8;
              }
            }
            dist += hold & ((1 << op) - 1);
//#ifdef INFLATE_STRICT
            if (dist > dmax) {
              strm.msg = 'invalid distance too far back';
              state.mode = BAD$1;
              break top;
            }
//#endif
            hold >>>= op;
            bits -= op;
            //Tracevv((stderr, "inflate:         distance %u\n", dist));
            op = _out - beg;                /* max distance in output */
            if (dist > op) {                /* see if copy from window */
              op = dist - op;               /* distance back in window */
              if (op > whave) {
                if (state.sane) {
                  strm.msg = 'invalid distance too far back';
                  state.mode = BAD$1;
                  break top;
                }

// (!) This block is disabled in zlib defaults,
// don't enable it for binary compatibility
//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR
//                if (len <= op - whave) {
//                  do {
//                    output[_out++] = 0;
//                  } while (--len);
//                  continue top;
//                }
//                len -= op - whave;
//                do {
//                  output[_out++] = 0;
//                } while (--op > whave);
//                if (op === 0) {
//                  from = _out - dist;
//                  do {
//                    output[_out++] = output[from++];
//                  } while (--len);
//                  continue top;
//                }
//#endif
              }
              from = 0; // window index
              from_source = s_window;
              if (wnext === 0) {           /* very common case */
                from += wsize - op;
                if (op < len) {         /* some from window */
                  len -= op;
                  do {
                    output[_out++] = s_window[from++];
                  } while (--op);
                  from = _out - dist;  /* rest from output */
                  from_source = output;
                }
              }
              else if (wnext < op) {      /* wrap around window */
                from += wsize + wnext - op;
                op -= wnext;
                if (op < len) {         /* some from end of window */
                  len -= op;
                  do {
                    output[_out++] = s_window[from++];
                  } while (--op);
                  from = 0;
                  if (wnext < len) {  /* some from start of window */
                    op = wnext;
                    len -= op;
                    do {
                      output[_out++] = s_window[from++];
                    } while (--op);
                    from = _out - dist;      /* rest from output */
                    from_source = output;
                  }
                }
              }
              else {                      /* contiguous in window */
                from += wnext - op;
                if (op < len) {         /* some from window */
                  len -= op;
                  do {
                    output[_out++] = s_window[from++];
                  } while (--op);
                  from = _out - dist;  /* rest from output */
                  from_source = output;
                }
              }
              while (len > 2) {
                output[_out++] = from_source[from++];
                output[_out++] = from_source[from++];
                output[_out++] = from_source[from++];
                len -= 3;
              }
              if (len) {
                output[_out++] = from_source[from++];
                if (len > 1) {
                  output[_out++] = from_source[from++];
                }
              }
            }
            else {
              from = _out - dist;          /* copy direct from output */
              do {                        /* minimum length is three */
                output[_out++] = output[from++];
                output[_out++] = output[from++];
                output[_out++] = output[from++];
                len -= 3;
              } while (len > 2);
              if (len) {
                output[_out++] = output[from++];
                if (len > 1) {
                  output[_out++] = output[from++];
                }
              }
            }
          }
          else if ((op & 64) === 0) {          /* 2nd level distance code */
            here = dcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
            continue dodist;
          }
          else {
            strm.msg = 'invalid distance code';
            state.mode = BAD$1;
            break top;
          }

          break; // need to emulate goto via "continue"
        }
      }
      else if ((op & 64) === 0) {              /* 2nd level length code */
        here = lcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
        continue dolen;
      }
      else if (op & 32) {                     /* end-of-block */
        //Tracevv((stderr, "inflate:         end of block\n"));
        state.mode = TYPE$1;
        break top;
      }
      else {
        strm.msg = 'invalid literal/length code';
        state.mode = BAD$1;
        break top;
      }

      break; // need to emulate goto via "continue"
    }
  } while (_in < last && _out < end);

  /* return unused bytes (on entry, bits < 8, so in won't go too far back) */
  len = bits >> 3;
  _in -= len;
  bits -= len << 3;
  hold &= (1 << bits) - 1;

  /* update state and return */
  strm.next_in = _in;
  strm.next_out = _out;
  strm.avail_in = (_in < last ? 5 + (last - _in) : 5 - (_in - last));
  strm.avail_out = (_out < end ? 257 + (end - _out) : 257 - (_out - end));
  state.hold = hold;
  state.bits = bits;
  return;
};

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

const MAXBITS = 15;
const ENOUGH_LENS$1 = 852;
const ENOUGH_DISTS$1 = 592;
//const ENOUGH = (ENOUGH_LENS+ENOUGH_DISTS);

const CODES$1 = 0;
const LENS$1 = 1;
const DISTS$1 = 2;

const lbase = new Uint16Array([ /* Length codes 257..285 base */
  3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
  35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0
]);

const lext = new Uint8Array([ /* Length codes 257..285 extra */
  16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18,
  19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 16, 72, 78
]);

const dbase = new Uint16Array([ /* Distance codes 0..29 base */
  1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
  257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,
  8193, 12289, 16385, 24577, 0, 0
]);

const dext = new Uint8Array([ /* Distance codes 0..29 extra */
  16, 16, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22,
  23, 23, 24, 24, 25, 25, 26, 26, 27, 27,
  28, 28, 29, 29, 64, 64
]);

const inflate_table = (type, lens, lens_index, codes, table, table_index, work, opts) =>
{
  const bits = opts.bits;
      //here = opts.here; /* table entry for duplication */

  let len = 0;               /* a code's length in bits */
  let sym = 0;               /* index of code symbols */
  let min = 0, max = 0;          /* minimum and maximum code lengths */
  let root = 0;              /* number of index bits for root table */
  let curr = 0;              /* number of index bits for current table */
  let drop = 0;              /* code bits to drop for sub-table */
  let left = 0;                   /* number of prefix codes available */
  let used = 0;              /* code entries in table used */
  let huff = 0;              /* Huffman code */
  let incr;              /* for incrementing code, index */
  let fill;              /* index for replicating entries */
  let low;               /* low bits for current root entry */
  let mask;              /* mask for low root bits */
  let next;             /* next available space in table */
  let base = null;     /* base value table to use */
//  let shoextra;    /* extra bits table to use */
  let match;                  /* use base and extra for symbol >= match */
  const count = new Uint16Array(MAXBITS + 1); //[MAXBITS+1];    /* number of codes of each length */
  const offs = new Uint16Array(MAXBITS + 1); //[MAXBITS+1];     /* offsets in table for each length */
  let extra = null;

  let here_bits, here_op, here_val;

  /*
   Process a set of code lengths to create a canonical Huffman code.  The
   code lengths are lens[0..codes-1].  Each length corresponds to the
   symbols 0..codes-1.  The Huffman code is generated by first sorting the
   symbols by length from short to long, and retaining the symbol order
   for codes with equal lengths.  Then the code starts with all zero bits
   for the first code of the shortest length, and the codes are integer
   increments for the same length, and zeros are appended as the length
   increases.  For the deflate format, these bits are stored backwards
   from their more natural integer increment ordering, and so when the
   decoding tables are built in the large loop below, the integer codes
   are incremented backwards.

   This routine assumes, but does not check, that all of the entries in
   lens[] are in the range 0..MAXBITS.  The caller must assure this.
   1..MAXBITS is interpreted as that code length.  zero means that that
   symbol does not occur in this code.

   The codes are sorted by computing a count of codes for each length,
   creating from that a table of starting indices for each length in the
   sorted table, and then entering the symbols in order in the sorted
   table.  The sorted table is work[], with that space being provided by
   the caller.

   The length counts are used for other purposes as well, i.e. finding
   the minimum and maximum length codes, determining if there are any
   codes at all, checking for a valid set of lengths, and looking ahead
   at length counts to determine sub-table sizes when building the
   decoding tables.
   */

  /* accumulate lengths for codes (assumes lens[] all in 0..MAXBITS) */
  for (len = 0; len <= MAXBITS; len++) {
    count[len] = 0;
  }
  for (sym = 0; sym < codes; sym++) {
    count[lens[lens_index + sym]]++;
  }

  /* bound code lengths, force root to be within code lengths */
  root = bits;
  for (max = MAXBITS; max >= 1; max--) {
    if (count[max] !== 0) { break; }
  }
  if (root > max) {
    root = max;
  }
  if (max === 0) {                     /* no symbols to code at all */
    //table.op[opts.table_index] = 64;  //here.op = (var char)64;    /* invalid code marker */
    //table.bits[opts.table_index] = 1;   //here.bits = (var char)1;
    //table.val[opts.table_index++] = 0;   //here.val = (var short)0;
    table[table_index++] = (1 << 24) | (64 << 16) | 0;


    //table.op[opts.table_index] = 64;
    //table.bits[opts.table_index] = 1;
    //table.val[opts.table_index++] = 0;
    table[table_index++] = (1 << 24) | (64 << 16) | 0;

    opts.bits = 1;
    return 0;     /* no symbols, but wait for decoding to report error */
  }
  for (min = 1; min < max; min++) {
    if (count[min] !== 0) { break; }
  }
  if (root < min) {
    root = min;
  }

  /* check for an over-subscribed or incomplete set of lengths */
  left = 1;
  for (len = 1; len <= MAXBITS; len++) {
    left <<= 1;
    left -= count[len];
    if (left < 0) {
      return -1;
    }        /* over-subscribed */
  }
  if (left > 0 && (type === CODES$1 || max !== 1)) {
    return -1;                      /* incomplete set */
  }

  /* generate offsets into symbol table for each length for sorting */
  offs[1] = 0;
  for (len = 1; len < MAXBITS; len++) {
    offs[len + 1] = offs[len] + count[len];
  }

  /* sort symbols by length, by symbol order within each length */
  for (sym = 0; sym < codes; sym++) {
    if (lens[lens_index + sym] !== 0) {
      work[offs[lens[lens_index + sym]]++] = sym;
    }
  }

  /*
   Create and fill in decoding tables.  In this loop, the table being
   filled is at next and has curr index bits.  The code being used is huff
   with length len.  That code is converted to an index by dropping drop
   bits off of the bottom.  For codes where len is less than drop + curr,
   those top drop + curr - len bits are incremented through all values to
   fill the table with replicated entries.

   root is the number of index bits for the root table.  When len exceeds
   root, sub-tables are created pointed to by the root entry with an index
   of the low root bits of huff.  This is saved in low to check for when a
   new sub-table should be started.  drop is zero when the root table is
   being filled, and drop is root when sub-tables are being filled.

   When a new sub-table is needed, it is necessary to look ahead in the
   code lengths to determine what size sub-table is needed.  The length
   counts are used for this, and so count[] is decremented as codes are
   entered in the tables.

   used keeps track of how many table entries have been allocated from the
   provided *table space.  It is checked for LENS and DIST tables against
   the constants ENOUGH_LENS and ENOUGH_DISTS to guard against changes in
   the initial root table size constants.  See the comments in inftrees.h
   for more information.

   sym increments through all symbols, and the loop terminates when
   all codes of length max, i.e. all codes, have been processed.  This
   routine permits incomplete codes, so another loop after this one fills
   in the rest of the decoding tables with invalid code markers.
   */

  /* set up for code type */
  // poor man optimization - use if-else instead of switch,
  // to avoid deopts in old v8
  if (type === CODES$1) {
    base = extra = work;    /* dummy value--not used */
    match = 20;

  } else if (type === LENS$1) {
    base = lbase;
    extra = lext;
    match = 257;

  } else {                    /* DISTS */
    base = dbase;
    extra = dext;
    match = 0;
  }

  /* initialize opts for loop */
  huff = 0;                   /* starting code */
  sym = 0;                    /* starting code symbol */
  len = min;                  /* starting code length */
  next = table_index;              /* current table to fill in */
  curr = root;                /* current table index bits */
  drop = 0;                   /* current bits to drop from code for index */
  low = -1;                   /* trigger new sub-table when len > root */
  used = 1 << root;          /* use root table entries */
  mask = used - 1;            /* mask for comparing low */

  /* check available table space */
  if ((type === LENS$1 && used > ENOUGH_LENS$1) ||
    (type === DISTS$1 && used > ENOUGH_DISTS$1)) {
    return 1;
  }

  /* process all codes and make table entries */
  for (;;) {
    /* create table entry */
    here_bits = len - drop;
    if (work[sym] + 1 < match) {
      here_op = 0;
      here_val = work[sym];
    }
    else if (work[sym] >= match) {
      here_op = extra[work[sym] - match];
      here_val = base[work[sym] - match];
    }
    else {
      here_op = 32 + 64;         /* end of block */
      here_val = 0;
    }

    /* replicate for those indices with low len bits equal to huff */
    incr = 1 << (len - drop);
    fill = 1 << curr;
    min = fill;                 /* save offset to next table */
    do {
      fill -= incr;
      table[next + (huff >> drop) + fill] = (here_bits << 24) | (here_op << 16) | here_val |0;
    } while (fill !== 0);

    /* backwards increment the len-bit code huff */
    incr = 1 << (len - 1);
    while (huff & incr) {
      incr >>= 1;
    }
    if (incr !== 0) {
      huff &= incr - 1;
      huff += incr;
    } else {
      huff = 0;
    }

    /* go to next symbol, update count, len */
    sym++;
    if (--count[len] === 0) {
      if (len === max) { break; }
      len = lens[lens_index + work[sym]];
    }

    /* create new sub-table if needed */
    if (len > root && (huff & mask) !== low) {
      /* if first time, transition to sub-tables */
      if (drop === 0) {
        drop = root;
      }

      /* increment past last table */
      next += min;            /* here min is 1 << curr */

      /* determine length of next table */
      curr = len - drop;
      left = 1 << curr;
      while (curr + drop < max) {
        left -= count[curr + drop];
        if (left <= 0) { break; }
        curr++;
        left <<= 1;
      }

      /* check for enough space */
      used += 1 << curr;
      if ((type === LENS$1 && used > ENOUGH_LENS$1) ||
        (type === DISTS$1 && used > ENOUGH_DISTS$1)) {
        return 1;
      }

      /* point entry in root table to sub-table */
      low = huff & mask;
      /*table.op[low] = curr;
      table.bits[low] = root;
      table.val[low] = next - opts.table_index;*/
      table[low] = (root << 24) | (curr << 16) | (next - table_index) |0;
    }
  }

  /* fill in remaining table entry if code is incomplete (guaranteed to have
   at most one remaining entry, since if the code is incomplete, the
   maximum code length that was allowed to get this far is one bit) */
  if (huff !== 0) {
    //table.op[next + huff] = 64;            /* invalid code marker */
    //table.bits[next + huff] = len - drop;
    //table.val[next + huff] = 0;
    table[next + huff] = ((len - drop) << 24) | (64 << 16) |0;
  }

  /* set return parameters */
  //opts.table_index += used;
  opts.bits = root;
  return 0;
};


var inftrees = inflate_table;

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.






const CODES = 0;
const LENS = 1;
const DISTS = 2;

/* Public constants ==========================================================*/
/* ===========================================================================*/

const {
  Z_FINISH: Z_FINISH$1, Z_BLOCK, Z_TREES,
  Z_OK: Z_OK$1, Z_STREAM_END: Z_STREAM_END$1, Z_NEED_DICT: Z_NEED_DICT$1, Z_STREAM_ERROR: Z_STREAM_ERROR$1, Z_DATA_ERROR: Z_DATA_ERROR$1, Z_MEM_ERROR: Z_MEM_ERROR$1, Z_BUF_ERROR,
  Z_DEFLATED
} = constants$2;


/* STATES ====================================================================*/
/* ===========================================================================*/


const    HEAD = 16180;       /* i: waiting for magic header */
const    FLAGS = 16181;      /* i: waiting for method and flags (gzip) */
const    TIME = 16182;       /* i: waiting for modification time (gzip) */
const    OS = 16183;         /* i: waiting for extra flags and operating system (gzip) */
const    EXLEN = 16184;      /* i: waiting for extra length (gzip) */
const    EXTRA = 16185;      /* i: waiting for extra bytes (gzip) */
const    NAME = 16186;       /* i: waiting for end of file name (gzip) */
const    COMMENT = 16187;    /* i: waiting for end of comment (gzip) */
const    HCRC = 16188;       /* i: waiting for header crc (gzip) */
const    DICTID = 16189;    /* i: waiting for dictionary check value */
const    DICT = 16190;      /* waiting for inflateSetDictionary() call */
const        TYPE = 16191;      /* i: waiting for type bits, including last-flag bit */
const        TYPEDO = 16192;    /* i: same, but skip check to exit inflate on new block */
const        STORED = 16193;    /* i: waiting for stored size (length and complement) */
const        COPY_ = 16194;     /* i/o: same as COPY below, but only first time in */
const        COPY = 16195;      /* i/o: waiting for input or output to copy stored block */
const        TABLE = 16196;     /* i: waiting for dynamic block table lengths */
const        LENLENS = 16197;   /* i: waiting for code length code lengths */
const        CODELENS = 16198;  /* i: waiting for length/lit and distance code lengths */
const            LEN_ = 16199;      /* i: same as LEN below, but only first time in */
const            LEN = 16200;       /* i: waiting for length/lit/eob code */
const            LENEXT = 16201;    /* i: waiting for length extra bits */
const            DIST = 16202;      /* i: waiting for distance code */
const            DISTEXT = 16203;   /* i: waiting for distance extra bits */
const            MATCH = 16204;     /* o: waiting for output space to copy string */
const            LIT = 16205;       /* o: waiting for output space to write literal */
const    CHECK = 16206;     /* i: waiting for 32-bit check value */
const    LENGTH = 16207;    /* i: waiting for 32-bit length (gzip) */
const    DONE = 16208;      /* finished check, done -- remain here until reset */
const    BAD = 16209;       /* got a data error -- remain here until reset */
const    MEM = 16210;       /* got an inflate() memory error -- remain here until reset */
const    SYNC = 16211;      /* looking for synchronization bytes to restart inflate() */

/* ===========================================================================*/



const ENOUGH_LENS = 852;
const ENOUGH_DISTS = 592;
//const ENOUGH =  (ENOUGH_LENS+ENOUGH_DISTS);

const MAX_WBITS = 15;
/* 32K LZ77 window */
const DEF_WBITS = MAX_WBITS;


const zswap32 = (q) => {

  return  (((q >>> 24) & 0xff) +
          ((q >>> 8) & 0xff00) +
          ((q & 0xff00) << 8) +
          ((q & 0xff) << 24));
};


function InflateState() {
  this.strm = null;           /* pointer back to this zlib stream */
  this.mode = 0;              /* current inflate mode */
  this.last = false;          /* true if processing last block */
  this.wrap = 0;              /* bit 0 true for zlib, bit 1 true for gzip,
                                 bit 2 true to validate check value */
  this.havedict = false;      /* true if dictionary provided */
  this.flags = 0;             /* gzip header method and flags (0 if zlib), or
                                 -1 if raw or no header yet */
  this.dmax = 0;              /* zlib header max distance (INFLATE_STRICT) */
  this.check = 0;             /* protected copy of check value */
  this.total = 0;             /* protected copy of output count */
  // TODO: may be {}
  this.head = null;           /* where to save gzip header information */

  /* sliding window */
  this.wbits = 0;             /* log base 2 of requested window size */
  this.wsize = 0;             /* window size or zero if not using window */
  this.whave = 0;             /* valid bytes in the window */
  this.wnext = 0;             /* window write index */
  this.window = null;         /* allocated sliding window, if needed */

  /* bit accumulator */
  this.hold = 0;              /* input bit accumulator */
  this.bits = 0;              /* number of bits in "in" */

  /* for string and stored block copying */
  this.length = 0;            /* literal or length of data to copy */
  this.offset = 0;            /* distance back to copy string from */

  /* for table and code decoding */
  this.extra = 0;             /* extra bits needed */

  /* fixed and dynamic code tables */
  this.lencode = null;          /* starting table for length/literal codes */
  this.distcode = null;         /* starting table for distance codes */
  this.lenbits = 0;           /* index bits for lencode */
  this.distbits = 0;          /* index bits for distcode */

  /* dynamic table building */
  this.ncode = 0;             /* number of code length code lengths */
  this.nlen = 0;              /* number of length code lengths */
  this.ndist = 0;             /* number of distance code lengths */
  this.have = 0;              /* number of code lengths in lens[] */
  this.next = null;              /* next available space in codes[] */

  this.lens = new Uint16Array(320); /* temporary storage for code lengths */
  this.work = new Uint16Array(288); /* work area for code table building */

  /*
   because we don't have pointers in js, we use lencode and distcode directly
   as buffers so we don't need codes
  */
  //this.codes = new Int32Array(ENOUGH);       /* space for code tables */
  this.lendyn = null;              /* dynamic table for length/literal codes (JS specific) */
  this.distdyn = null;             /* dynamic table for distance codes (JS specific) */
  this.sane = 0;                   /* if false, allow invalid distance too far */
  this.back = 0;                   /* bits back of last unprocessed length/lit */
  this.was = 0;                    /* initial length of match */
}


const inflateStateCheck = (strm) => {

  if (!strm) {
    return 1;
  }
  const state = strm.state;
  if (!state || state.strm !== strm ||
    state.mode < HEAD || state.mode > SYNC) {
    return 1;
  }
  return 0;
};


const inflateResetKeep = (strm) => {

  if (inflateStateCheck(strm)) { return Z_STREAM_ERROR$1; }
  const state = strm.state;
  strm.total_in = strm.total_out = state.total = 0;
  strm.msg = ''; /*Z_NULL*/
  if (state.wrap) {       /* to support ill-conceived Java test suite */
    strm.adler = state.wrap & 1;
  }
  state.mode = HEAD;
  state.last = 0;
  state.havedict = 0;
  state.flags = -1;
  state.dmax = 32768;
  state.head = null/*Z_NULL*/;
  state.hold = 0;
  state.bits = 0;
  //state.lencode = state.distcode = state.next = state.codes;
  state.lencode = state.lendyn = new Int32Array(ENOUGH_LENS);
  state.distcode = state.distdyn = new Int32Array(ENOUGH_DISTS);

  state.sane = 1;
  state.back = -1;
  //Tracev((stderr, "inflate: reset\n"));
  return Z_OK$1;
};


const inflateReset = (strm) => {

  if (inflateStateCheck(strm)) { return Z_STREAM_ERROR$1; }
  const state = strm.state;
  state.wsize = 0;
  state.whave = 0;
  state.wnext = 0;
  return inflateResetKeep(strm);

};


const inflateReset2 = (strm, windowBits) => {
  let wrap;

  /* get the state */
  if (inflateStateCheck(strm)) { return Z_STREAM_ERROR$1; }
  const state = strm.state;

  /* extract wrap request from windowBits parameter */
  if (windowBits < 0) {
    wrap = 0;
    windowBits = -windowBits;
  }
  else {
    wrap = (windowBits >> 4) + 5;
    if (windowBits < 48) {
      windowBits &= 15;
    }
  }

  /* set number of window bits, free window if different */
  if (windowBits && (windowBits < 8 || windowBits > 15)) {
    return Z_STREAM_ERROR$1;
  }
  if (state.window !== null && state.wbits !== windowBits) {
    state.window = null;
  }

  /* update state and reset the rest of it */
  state.wrap = wrap;
  state.wbits = windowBits;
  return inflateReset(strm);
};


const inflateInit2 = (strm, windowBits) => {

  if (!strm) { return Z_STREAM_ERROR$1; }
  //strm.msg = Z_NULL;                 /* in case we return an error */

  const state = new InflateState();

  //if (state === Z_NULL) return Z_MEM_ERROR;
  //Tracev((stderr, "inflate: allocated\n"));
  strm.state = state;
  state.strm = strm;
  state.window = null/*Z_NULL*/;
  state.mode = HEAD;     /* to pass state test in inflateReset2() */
  const ret = inflateReset2(strm, windowBits);
  if (ret !== Z_OK$1) {
    strm.state = null/*Z_NULL*/;
  }
  return ret;
};


const inflateInit = (strm) => {

  return inflateInit2(strm, DEF_WBITS);
};


/*
 Return state with length and distance decoding tables and index sizes set to
 fixed code decoding.  Normally this returns fixed tables from inffixed.h.
 If BUILDFIXED is defined, then instead this routine builds the tables the
 first time it's called, and returns those tables the first time and
 thereafter.  This reduces the size of the code by about 2K bytes, in
 exchange for a little execution time.  However, BUILDFIXED should not be
 used for threaded applications, since the rewriting of the tables and virgin
 may not be thread-safe.
 */
let virgin = true;

let lenfix, distfix; // We have no pointers in JS, so keep tables separate


const fixedtables = (state) => {

  /* build fixed huffman tables if first call (may not be thread safe) */
  if (virgin) {
    lenfix = new Int32Array(512);
    distfix = new Int32Array(32);

    /* literal/length table */
    let sym = 0;
    while (sym < 144) { state.lens[sym++] = 8; }
    while (sym < 256) { state.lens[sym++] = 9; }
    while (sym < 280) { state.lens[sym++] = 7; }
    while (sym < 288) { state.lens[sym++] = 8; }

    inftrees(LENS,  state.lens, 0, 288, lenfix,   0, state.work, { bits: 9 });

    /* distance table */
    sym = 0;
    while (sym < 32) { state.lens[sym++] = 5; }

    inftrees(DISTS, state.lens, 0, 32,   distfix, 0, state.work, { bits: 5 });

    /* do this just once */
    virgin = false;
  }

  state.lencode = lenfix;
  state.lenbits = 9;
  state.distcode = distfix;
  state.distbits = 5;
};


/*
 Update the window with the last wsize (normally 32K) bytes written before
 returning.  If window does not exist yet, create it.  This is only called
 when a window is already in use, or when output has been written during this
 inflate call, but the end of the deflate stream has not been reached yet.
 It is also called to create a window for dictionary data when a dictionary
 is loaded.

 Providing output buffers larger than 32K to inflate() should provide a speed
 advantage, since only the last 32K of output is copied to the sliding window
 upon return from inflate(), and since all distances after the first 32K of
 output will fall in the output data, making match copies simpler and faster.
 The advantage may be dependent on the size of the processor's data caches.
 */
const updatewindow = (strm, src, end, copy) => {

  let dist;
  const state = strm.state;

  /* if it hasn't been done already, allocate space for the window */
  if (state.window === null) {
    state.wsize = 1 << state.wbits;
    state.wnext = 0;
    state.whave = 0;

    state.window = new Uint8Array(state.wsize);
  }

  /* copy state->wsize or less output bytes into the circular window */
  if (copy >= state.wsize) {
    state.window.set(src.subarray(end - state.wsize, end), 0);
    state.wnext = 0;
    state.whave = state.wsize;
  }
  else {
    dist = state.wsize - state.wnext;
    if (dist > copy) {
      dist = copy;
    }
    //zmemcpy(state->window + state->wnext, end - copy, dist);
    state.window.set(src.subarray(end - copy, end - copy + dist), state.wnext);
    copy -= dist;
    if (copy) {
      //zmemcpy(state->window, end - copy, copy);
      state.window.set(src.subarray(end - copy, end), 0);
      state.wnext = copy;
      state.whave = state.wsize;
    }
    else {
      state.wnext += dist;
      if (state.wnext === state.wsize) { state.wnext = 0; }
      if (state.whave < state.wsize) { state.whave += dist; }
    }
  }
  return 0;
};


const inflate$2 = (strm, flush) => {

  let state;
  let input, output;          // input/output buffers
  let next;                   /* next input INDEX */
  let put;                    /* next output INDEX */
  let have, left;             /* available input and output */
  let hold;                   /* bit buffer */
  let bits;                   /* bits in bit buffer */
  let _in, _out;              /* save starting available input and output */
  let copy;                   /* number of stored or match bytes to copy */
  let from;                   /* where to copy match bytes from */
  let from_source;
  let here = 0;               /* current decoding table entry */
  let here_bits, here_op, here_val; // paked "here" denormalized (JS specific)
  //let last;                   /* parent table entry */
  let last_bits, last_op, last_val; // paked "last" denormalized (JS specific)
  let len;                    /* length to copy for repeats, bits to drop */
  let ret;                    /* return code */
  const hbuf = new Uint8Array(4);    /* buffer for gzip header crc calculation */
  let opts;

  let n; // temporary variable for NEED_BITS

  const order = /* permutation of code lengths */
    new Uint8Array([ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ]);


  if (inflateStateCheck(strm) || !strm.output ||
      (!strm.input && strm.avail_in !== 0)) {
    return Z_STREAM_ERROR$1;
  }

  state = strm.state;
  if (state.mode === TYPE) { state.mode = TYPEDO; }    /* skip check */


  //--- LOAD() ---
  put = strm.next_out;
  output = strm.output;
  left = strm.avail_out;
  next = strm.next_in;
  input = strm.input;
  have = strm.avail_in;
  hold = state.hold;
  bits = state.bits;
  //---

  _in = have;
  _out = left;
  ret = Z_OK$1;

  inf_leave: // goto emulation
  for (;;) {
    switch (state.mode) {
      case HEAD:
        if (state.wrap === 0) {
          state.mode = TYPEDO;
          break;
        }
        //=== NEEDBITS(16);
        while (bits < 16) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        if ((state.wrap & 2) && hold === 0x8b1f) {  /* gzip header */
          if (state.wbits === 0) {
            state.wbits = 15;
          }
          state.check = 0/*crc32(0L, Z_NULL, 0)*/;
          //=== CRC2(state.check, hold);
          hbuf[0] = hold & 0xff;
          hbuf[1] = (hold >>> 8) & 0xff;
          state.check = crc32_1(state.check, hbuf, 2, 0);
          //===//

          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
          state.mode = FLAGS;
          break;
        }
        if (state.head) {
          state.head.done = false;
        }
        if (!(state.wrap & 1) ||   /* check if zlib header allowed */
          (((hold & 0xff)/*BITS(8)*/ << 8) + (hold >> 8)) % 31) {
          strm.msg = 'incorrect header check';
          state.mode = BAD;
          break;
        }
        if ((hold & 0x0f)/*BITS(4)*/ !== Z_DEFLATED) {
          strm.msg = 'unknown compression method';
          state.mode = BAD;
          break;
        }
        //--- DROPBITS(4) ---//
        hold >>>= 4;
        bits -= 4;
        //---//
        len = (hold & 0x0f)/*BITS(4)*/ + 8;
        if (state.wbits === 0) {
          state.wbits = len;
        }
        if (len > 15 || len > state.wbits) {
          strm.msg = 'invalid window size';
          state.mode = BAD;
          break;
        }

        // !!! pako patch. Force use `options.windowBits` if passed.
        // Required to always use max window size by default.
        state.dmax = 1 << state.wbits;
        //state.dmax = 1 << len;

        state.flags = 0;               /* indicate zlib header */
        //Tracev((stderr, "inflate:   zlib header ok\n"));
        strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/;
        state.mode = hold & 0x200 ? DICTID : TYPE;
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        break;
      case FLAGS:
        //=== NEEDBITS(16); */
        while (bits < 16) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        state.flags = hold;
        if ((state.flags & 0xff) !== Z_DEFLATED) {
          strm.msg = 'unknown compression method';
          state.mode = BAD;
          break;
        }
        if (state.flags & 0xe000) {
          strm.msg = 'unknown header flags set';
          state.mode = BAD;
          break;
        }
        if (state.head) {
          state.head.text = ((hold >> 8) & 1);
        }
        if ((state.flags & 0x0200) && (state.wrap & 4)) {
          //=== CRC2(state.check, hold);
          hbuf[0] = hold & 0xff;
          hbuf[1] = (hold >>> 8) & 0xff;
          state.check = crc32_1(state.check, hbuf, 2, 0);
          //===//
        }
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        state.mode = TIME;
        /* falls through */
      case TIME:
        //=== NEEDBITS(32); */
        while (bits < 32) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        if (state.head) {
          state.head.time = hold;
        }
        if ((state.flags & 0x0200) && (state.wrap & 4)) {
          //=== CRC4(state.check, hold)
          hbuf[0] = hold & 0xff;
          hbuf[1] = (hold >>> 8) & 0xff;
          hbuf[2] = (hold >>> 16) & 0xff;
          hbuf[3] = (hold >>> 24) & 0xff;
          state.check = crc32_1(state.check, hbuf, 4, 0);
          //===
        }
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        state.mode = OS;
        /* falls through */
      case OS:
        //=== NEEDBITS(16); */
        while (bits < 16) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        if (state.head) {
          state.head.xflags = (hold & 0xff);
          state.head.os = (hold >> 8);
        }
        if ((state.flags & 0x0200) && (state.wrap & 4)) {
          //=== CRC2(state.check, hold);
          hbuf[0] = hold & 0xff;
          hbuf[1] = (hold >>> 8) & 0xff;
          state.check = crc32_1(state.check, hbuf, 2, 0);
          //===//
        }
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        state.mode = EXLEN;
        /* falls through */
      case EXLEN:
        if (state.flags & 0x0400) {
          //=== NEEDBITS(16); */
          while (bits < 16) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          state.length = hold;
          if (state.head) {
            state.head.extra_len = hold;
          }
          if ((state.flags & 0x0200) && (state.wrap & 4)) {
            //=== CRC2(state.check, hold);
            hbuf[0] = hold & 0xff;
            hbuf[1] = (hold >>> 8) & 0xff;
            state.check = crc32_1(state.check, hbuf, 2, 0);
            //===//
          }
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
        }
        else if (state.head) {
          state.head.extra = null/*Z_NULL*/;
        }
        state.mode = EXTRA;
        /* falls through */
      case EXTRA:
        if (state.flags & 0x0400) {
          copy = state.length;
          if (copy > have) { copy = have; }
          if (copy) {
            if (state.head) {
              len = state.head.extra_len - state.length;
              if (!state.head.extra) {
                // Use untyped array for more convenient processing later
                state.head.extra = new Uint8Array(state.head.extra_len);
              }
              state.head.extra.set(
                input.subarray(
                  next,
                  // extra field is limited to 65536 bytes
                  // - no need for additional size check
                  next + copy
                ),
                /*len + copy > state.head.extra_max - len ? state.head.extra_max : copy,*/
                len
              );
              //zmemcpy(state.head.extra + len, next,
              //        len + copy > state.head.extra_max ?
              //        state.head.extra_max - len : copy);
            }
            if ((state.flags & 0x0200) && (state.wrap & 4)) {
              state.check = crc32_1(state.check, input, copy, next);
            }
            have -= copy;
            next += copy;
            state.length -= copy;
          }
          if (state.length) { break inf_leave; }
        }
        state.length = 0;
        state.mode = NAME;
        /* falls through */
      case NAME:
        if (state.flags & 0x0800) {
          if (have === 0) { break inf_leave; }
          copy = 0;
          do {
            // TODO: 2 or 1 bytes?
            len = input[next + copy++];
            /* use constant limit because in js we should not preallocate memory */
            if (state.head && len &&
                (state.length < 65536 /*state.head.name_max*/)) {
              state.head.name += String.fromCharCode(len);
            }
          } while (len && copy < have);

          if ((state.flags & 0x0200) && (state.wrap & 4)) {
            state.check = crc32_1(state.check, input, copy, next);
          }
          have -= copy;
          next += copy;
          if (len) { break inf_leave; }
        }
        else if (state.head) {
          state.head.name = null;
        }
        state.length = 0;
        state.mode = COMMENT;
        /* falls through */
      case COMMENT:
        if (state.flags & 0x1000) {
          if (have === 0) { break inf_leave; }
          copy = 0;
          do {
            len = input[next + copy++];
            /* use constant limit because in js we should not preallocate memory */
            if (state.head && len &&
                (state.length < 65536 /*state.head.comm_max*/)) {
              state.head.comment += String.fromCharCode(len);
            }
          } while (len && copy < have);
          if ((state.flags & 0x0200) && (state.wrap & 4)) {
            state.check = crc32_1(state.check, input, copy, next);
          }
          have -= copy;
          next += copy;
          if (len) { break inf_leave; }
        }
        else if (state.head) {
          state.head.comment = null;
        }
        state.mode = HCRC;
        /* falls through */
      case HCRC:
        if (state.flags & 0x0200) {
          //=== NEEDBITS(16); */
          while (bits < 16) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          if ((state.wrap & 4) && hold !== (state.check & 0xffff)) {
            strm.msg = 'header crc mismatch';
            state.mode = BAD;
            break;
          }
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
        }
        if (state.head) {
          state.head.hcrc = ((state.flags >> 9) & 1);
          state.head.done = true;
        }
        strm.adler = state.check = 0;
        state.mode = TYPE;
        break;
      case DICTID:
        //=== NEEDBITS(32); */
        while (bits < 32) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        strm.adler = state.check = zswap32(hold);
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        state.mode = DICT;
        /* falls through */
      case DICT:
        if (state.havedict === 0) {
          //--- RESTORE() ---
          strm.next_out = put;
          strm.avail_out = left;
          strm.next_in = next;
          strm.avail_in = have;
          state.hold = hold;
          state.bits = bits;
          //---
          return Z_NEED_DICT$1;
        }
        strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/;
        state.mode = TYPE;
        /* falls through */
      case TYPE:
        if (flush === Z_BLOCK || flush === Z_TREES) { break inf_leave; }
        /* falls through */
      case TYPEDO:
        if (state.last) {
          //--- BYTEBITS() ---//
          hold >>>= bits & 7;
          bits -= bits & 7;
          //---//
          state.mode = CHECK;
          break;
        }
        //=== NEEDBITS(3); */
        while (bits < 3) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        state.last = (hold & 0x01)/*BITS(1)*/;
        //--- DROPBITS(1) ---//
        hold >>>= 1;
        bits -= 1;
        //---//

        switch ((hold & 0x03)/*BITS(2)*/) {
          case 0:                             /* stored block */
            //Tracev((stderr, "inflate:     stored block%s\n",
            //        state.last ? " (last)" : ""));
            state.mode = STORED;
            break;
          case 1:                             /* fixed block */
            fixedtables(state);
            //Tracev((stderr, "inflate:     fixed codes block%s\n",
            //        state.last ? " (last)" : ""));
            state.mode = LEN_;             /* decode codes */
            if (flush === Z_TREES) {
              //--- DROPBITS(2) ---//
              hold >>>= 2;
              bits -= 2;
              //---//
              break inf_leave;
            }
            break;
          case 2:                             /* dynamic block */
            //Tracev((stderr, "inflate:     dynamic codes block%s\n",
            //        state.last ? " (last)" : ""));
            state.mode = TABLE;
            break;
          case 3:
            strm.msg = 'invalid block type';
            state.mode = BAD;
        }
        //--- DROPBITS(2) ---//
        hold >>>= 2;
        bits -= 2;
        //---//
        break;
      case STORED:
        //--- BYTEBITS() ---// /* go to byte boundary */
        hold >>>= bits & 7;
        bits -= bits & 7;
        //---//
        //=== NEEDBITS(32); */
        while (bits < 32) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        if ((hold & 0xffff) !== ((hold >>> 16) ^ 0xffff)) {
          strm.msg = 'invalid stored block lengths';
          state.mode = BAD;
          break;
        }
        state.length = hold & 0xffff;
        //Tracev((stderr, "inflate:       stored length %u\n",
        //        state.length));
        //=== INITBITS();
        hold = 0;
        bits = 0;
        //===//
        state.mode = COPY_;
        if (flush === Z_TREES) { break inf_leave; }
        /* falls through */
      case COPY_:
        state.mode = COPY;
        /* falls through */
      case COPY:
        copy = state.length;
        if (copy) {
          if (copy > have) { copy = have; }
          if (copy > left) { copy = left; }
          if (copy === 0) { break inf_leave; }
          //--- zmemcpy(put, next, copy); ---
          output.set(input.subarray(next, next + copy), put);
          //---//
          have -= copy;
          next += copy;
          left -= copy;
          put += copy;
          state.length -= copy;
          break;
        }
        //Tracev((stderr, "inflate:       stored end\n"));
        state.mode = TYPE;
        break;
      case TABLE:
        //=== NEEDBITS(14); */
        while (bits < 14) {
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
        }
        //===//
        state.nlen = (hold & 0x1f)/*BITS(5)*/ + 257;
        //--- DROPBITS(5) ---//
        hold >>>= 5;
        bits -= 5;
        //---//
        state.ndist = (hold & 0x1f)/*BITS(5)*/ + 1;
        //--- DROPBITS(5) ---//
        hold >>>= 5;
        bits -= 5;
        //---//
        state.ncode = (hold & 0x0f)/*BITS(4)*/ + 4;
        //--- DROPBITS(4) ---//
        hold >>>= 4;
        bits -= 4;
        //---//
//#ifndef PKZIP_BUG_WORKAROUND
        if (state.nlen > 286 || state.ndist > 30) {
          strm.msg = 'too many length or distance symbols';
          state.mode = BAD;
          break;
        }
//#endif
        //Tracev((stderr, "inflate:       table sizes ok\n"));
        state.have = 0;
        state.mode = LENLENS;
        /* falls through */
      case LENLENS:
        while (state.have < state.ncode) {
          //=== NEEDBITS(3);
          while (bits < 3) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          state.lens[order[state.have++]] = (hold & 0x07);//BITS(3);
          //--- DROPBITS(3) ---//
          hold >>>= 3;
          bits -= 3;
          //---//
        }
        while (state.have < 19) {
          state.lens[order[state.have++]] = 0;
        }
        // We have separate tables & no pointers. 2 commented lines below not needed.
        //state.next = state.codes;
        //state.lencode = state.next;
        // Switch to use dynamic table
        state.lencode = state.lendyn;
        state.lenbits = 7;

        opts = { bits: state.lenbits };
        ret = inftrees(CODES, state.lens, 0, 19, state.lencode, 0, state.work, opts);
        state.lenbits = opts.bits;

        if (ret) {
          strm.msg = 'invalid code lengths set';
          state.mode = BAD;
          break;
        }
        //Tracev((stderr, "inflate:       code lengths ok\n"));
        state.have = 0;
        state.mode = CODELENS;
        /* falls through */
      case CODELENS:
        while (state.have < state.nlen + state.ndist) {
          for (;;) {
            here = state.lencode[hold & ((1 << state.lenbits) - 1)];/*BITS(state.lenbits)*/
            here_bits = here >>> 24;
            here_op = (here >>> 16) & 0xff;
            here_val = here & 0xffff;

            if ((here_bits) <= bits) { break; }
            //--- PULLBYTE() ---//
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
            //---//
          }
          if (here_val < 16) {
            //--- DROPBITS(here.bits) ---//
            hold >>>= here_bits;
            bits -= here_bits;
            //---//
            state.lens[state.have++] = here_val;
          }
          else {
            if (here_val === 16) {
              //=== NEEDBITS(here.bits + 2);
              n = here_bits + 2;
              while (bits < n) {
                if (have === 0) { break inf_leave; }
                have--;
                hold += input[next++] << bits;
                bits += 8;
              }
              //===//
              //--- DROPBITS(here.bits) ---//
              hold >>>= here_bits;
              bits -= here_bits;
              //---//
              if (state.have === 0) {
                strm.msg = 'invalid bit length repeat';
                state.mode = BAD;
                break;
              }
              len = state.lens[state.have - 1];
              copy = 3 + (hold & 0x03);//BITS(2);
              //--- DROPBITS(2) ---//
              hold >>>= 2;
              bits -= 2;
              //---//
            }
            else if (here_val === 17) {
              //=== NEEDBITS(here.bits + 3);
              n = here_bits + 3;
              while (bits < n) {
                if (have === 0) { break inf_leave; }
                have--;
                hold += input[next++] << bits;
                bits += 8;
              }
              //===//
              //--- DROPBITS(here.bits) ---//
              hold >>>= here_bits;
              bits -= here_bits;
              //---//
              len = 0;
              copy = 3 + (hold & 0x07);//BITS(3);
              //--- DROPBITS(3) ---//
              hold >>>= 3;
              bits -= 3;
              //---//
            }
            else {
              //=== NEEDBITS(here.bits + 7);
              n = here_bits + 7;
              while (bits < n) {
                if (have === 0) { break inf_leave; }
                have--;
                hold += input[next++] << bits;
                bits += 8;
              }
              //===//
              //--- DROPBITS(here.bits) ---//
              hold >>>= here_bits;
              bits -= here_bits;
              //---//
              len = 0;
              copy = 11 + (hold & 0x7f);//BITS(7);
              //--- DROPBITS(7) ---//
              hold >>>= 7;
              bits -= 7;
              //---//
            }
            if (state.have + copy > state.nlen + state.ndist) {
              strm.msg = 'invalid bit length repeat';
              state.mode = BAD;
              break;
            }
            while (copy--) {
              state.lens[state.have++] = len;
            }
          }
        }

        /* handle error breaks in while */
        if (state.mode === BAD) { break; }

        /* check for end-of-block code (better have one) */
        if (state.lens[256] === 0) {
          strm.msg = 'invalid code -- missing end-of-block';
          state.mode = BAD;
          break;
        }

        /* build code tables -- note: do not change the lenbits or distbits
           values here (9 and 6) without reading the comments in inftrees.h
           concerning the ENOUGH constants, which depend on those values */
        state.lenbits = 9;

        opts = { bits: state.lenbits };
        ret = inftrees(LENS, state.lens, 0, state.nlen, state.lencode, 0, state.work, opts);
        // We have separate tables & no pointers. 2 commented lines below not needed.
        // state.next_index = opts.table_index;
        state.lenbits = opts.bits;
        // state.lencode = state.next;

        if (ret) {
          strm.msg = 'invalid literal/lengths set';
          state.mode = BAD;
          break;
        }

        state.distbits = 6;
        //state.distcode.copy(state.codes);
        // Switch to use dynamic table
        state.distcode = state.distdyn;
        opts = { bits: state.distbits };
        ret = inftrees(DISTS, state.lens, state.nlen, state.ndist, state.distcode, 0, state.work, opts);
        // We have separate tables & no pointers. 2 commented lines below not needed.
        // state.next_index = opts.table_index;
        state.distbits = opts.bits;
        // state.distcode = state.next;

        if (ret) {
          strm.msg = 'invalid distances set';
          state.mode = BAD;
          break;
        }
        //Tracev((stderr, 'inflate:       codes ok\n'));
        state.mode = LEN_;
        if (flush === Z_TREES) { break inf_leave; }
        /* falls through */
      case LEN_:
        state.mode = LEN;
        /* falls through */
      case LEN:
        if (have >= 6 && left >= 258) {
          //--- RESTORE() ---
          strm.next_out = put;
          strm.avail_out = left;
          strm.next_in = next;
          strm.avail_in = have;
          state.hold = hold;
          state.bits = bits;
          //---
          inffast(strm, _out);
          //--- LOAD() ---
          put = strm.next_out;
          output = strm.output;
          left = strm.avail_out;
          next = strm.next_in;
          input = strm.input;
          have = strm.avail_in;
          hold = state.hold;
          bits = state.bits;
          //---

          if (state.mode === TYPE) {
            state.back = -1;
          }
          break;
        }
        state.back = 0;
        for (;;) {
          here = state.lencode[hold & ((1 << state.lenbits) - 1)];  /*BITS(state.lenbits)*/
          here_bits = here >>> 24;
          here_op = (here >>> 16) & 0xff;
          here_val = here & 0xffff;

          if (here_bits <= bits) { break; }
          //--- PULLBYTE() ---//
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
          //---//
        }
        if (here_op && (here_op & 0xf0) === 0) {
          last_bits = here_bits;
          last_op = here_op;
          last_val = here_val;
          for (;;) {
            here = state.lencode[last_val +
                    ((hold & ((1 << (last_bits + last_op)) - 1))/*BITS(last.bits + last.op)*/ >> last_bits)];
            here_bits = here >>> 24;
            here_op = (here >>> 16) & 0xff;
            here_val = here & 0xffff;

            if ((last_bits + here_bits) <= bits) { break; }
            //--- PULLBYTE() ---//
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
            //---//
          }
          //--- DROPBITS(last.bits) ---//
          hold >>>= last_bits;
          bits -= last_bits;
          //---//
          state.back += last_bits;
        }
        //--- DROPBITS(here.bits) ---//
        hold >>>= here_bits;
        bits -= here_bits;
        //---//
        state.back += here_bits;
        state.length = here_val;
        if (here_op === 0) {
          //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ?
          //        "inflate:         literal '%c'\n" :
          //        "inflate:         literal 0x%02x\n", here.val));
          state.mode = LIT;
          break;
        }
        if (here_op & 32) {
          //Tracevv((stderr, "inflate:         end of block\n"));
          state.back = -1;
          state.mode = TYPE;
          break;
        }
        if (here_op & 64) {
          strm.msg = 'invalid literal/length code';
          state.mode = BAD;
          break;
        }
        state.extra = here_op & 15;
        state.mode = LENEXT;
        /* falls through */
      case LENEXT:
        if (state.extra) {
          //=== NEEDBITS(state.extra);
          n = state.extra;
          while (bits < n) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          state.length += hold & ((1 << state.extra) - 1)/*BITS(state.extra)*/;
          //--- DROPBITS(state.extra) ---//
          hold >>>= state.extra;
          bits -= state.extra;
          //---//
          state.back += state.extra;
        }
        //Tracevv((stderr, "inflate:         length %u\n", state.length));
        state.was = state.length;
        state.mode = DIST;
        /* falls through */
      case DIST:
        for (;;) {
          here = state.distcode[hold & ((1 << state.distbits) - 1)];/*BITS(state.distbits)*/
          here_bits = here >>> 24;
          here_op = (here >>> 16) & 0xff;
          here_val = here & 0xffff;

          if ((here_bits) <= bits) { break; }
          //--- PULLBYTE() ---//
          if (have === 0) { break inf_leave; }
          have--;
          hold += input[next++] << bits;
          bits += 8;
          //---//
        }
        if ((here_op & 0xf0) === 0) {
          last_bits = here_bits;
          last_op = here_op;
          last_val = here_val;
          for (;;) {
            here = state.distcode[last_val +
                    ((hold & ((1 << (last_bits + last_op)) - 1))/*BITS(last.bits + last.op)*/ >> last_bits)];
            here_bits = here >>> 24;
            here_op = (here >>> 16) & 0xff;
            here_val = here & 0xffff;

            if ((last_bits + here_bits) <= bits) { break; }
            //--- PULLBYTE() ---//
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
            //---//
          }
          //--- DROPBITS(last.bits) ---//
          hold >>>= last_bits;
          bits -= last_bits;
          //---//
          state.back += last_bits;
        }
        //--- DROPBITS(here.bits) ---//
        hold >>>= here_bits;
        bits -= here_bits;
        //---//
        state.back += here_bits;
        if (here_op & 64) {
          strm.msg = 'invalid distance code';
          state.mode = BAD;
          break;
        }
        state.offset = here_val;
        state.extra = (here_op) & 15;
        state.mode = DISTEXT;
        /* falls through */
      case DISTEXT:
        if (state.extra) {
          //=== NEEDBITS(state.extra);
          n = state.extra;
          while (bits < n) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          state.offset += hold & ((1 << state.extra) - 1)/*BITS(state.extra)*/;
          //--- DROPBITS(state.extra) ---//
          hold >>>= state.extra;
          bits -= state.extra;
          //---//
          state.back += state.extra;
        }
//#ifdef INFLATE_STRICT
        if (state.offset > state.dmax) {
          strm.msg = 'invalid distance too far back';
          state.mode = BAD;
          break;
        }
//#endif
        //Tracevv((stderr, "inflate:         distance %u\n", state.offset));
        state.mode = MATCH;
        /* falls through */
      case MATCH:
        if (left === 0) { break inf_leave; }
        copy = _out - left;
        if (state.offset > copy) {         /* copy from window */
          copy = state.offset - copy;
          if (copy > state.whave) {
            if (state.sane) {
              strm.msg = 'invalid distance too far back';
              state.mode = BAD;
              break;
            }
// (!) This block is disabled in zlib defaults,
// don't enable it for binary compatibility
//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR
//          Trace((stderr, "inflate.c too far\n"));
//          copy -= state.whave;
//          if (copy > state.length) { copy = state.length; }
//          if (copy > left) { copy = left; }
//          left -= copy;
//          state.length -= copy;
//          do {
//            output[put++] = 0;
//          } while (--copy);
//          if (state.length === 0) { state.mode = LEN; }
//          break;
//#endif
          }
          if (copy > state.wnext) {
            copy -= state.wnext;
            from = state.wsize - copy;
          }
          else {
            from = state.wnext - copy;
          }
          if (copy > state.length) { copy = state.length; }
          from_source = state.window;
        }
        else {                              /* copy from output */
          from_source = output;
          from = put - state.offset;
          copy = state.length;
        }
        if (copy > left) { copy = left; }
        left -= copy;
        state.length -= copy;
        do {
          output[put++] = from_source[from++];
        } while (--copy);
        if (state.length === 0) { state.mode = LEN; }
        break;
      case LIT:
        if (left === 0) { break inf_leave; }
        output[put++] = state.length;
        left--;
        state.mode = LEN;
        break;
      case CHECK:
        if (state.wrap) {
          //=== NEEDBITS(32);
          while (bits < 32) {
            if (have === 0) { break inf_leave; }
            have--;
            // Use '|' instead of '+' to make sure that result is signed
            hold |= input[next++] << bits;
            bits += 8;
          }
          //===//
          _out -= left;
          strm.total_out += _out;
          state.total += _out;
          if ((state.wrap & 4) && _out) {
            strm.adler = state.check =
                /*UPDATE_CHECK(state.check, put - _out, _out);*/
                (state.flags ? crc32_1(state.check, output, _out, put - _out) : adler32_1(state.check, output, _out, put - _out));

          }
          _out = left;
          // NB: crc32 stored as signed 32-bit int, zswap32 returns signed too
          if ((state.wrap & 4) && (state.flags ? hold : zswap32(hold)) !== state.check) {
            strm.msg = 'incorrect data check';
            state.mode = BAD;
            break;
          }
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
          //Tracev((stderr, "inflate:   check matches trailer\n"));
        }
        state.mode = LENGTH;
        /* falls through */
      case LENGTH:
        if (state.wrap && state.flags) {
          //=== NEEDBITS(32);
          while (bits < 32) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          if ((state.wrap & 4) && hold !== (state.total & 0xffffffff)) {
            strm.msg = 'incorrect length check';
            state.mode = BAD;
            break;
          }
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
          //Tracev((stderr, "inflate:   length matches trailer\n"));
        }
        state.mode = DONE;
        /* falls through */
      case DONE:
        ret = Z_STREAM_END$1;
        break inf_leave;
      case BAD:
        ret = Z_DATA_ERROR$1;
        break inf_leave;
      case MEM:
        return Z_MEM_ERROR$1;
      case SYNC:
        /* falls through */
      default:
        return Z_STREAM_ERROR$1;
    }
  }

  // inf_leave <- here is real place for "goto inf_leave", emulated via "break inf_leave"

  /*
     Return from inflate(), updating the total counts and the check value.
     If there was no progress during the inflate() call, return a buffer
     error.  Call updatewindow() to create and/or update the window state.
     Note: a memory error from inflate() is non-recoverable.
   */

  //--- RESTORE() ---
  strm.next_out = put;
  strm.avail_out = left;
  strm.next_in = next;
  strm.avail_in = have;
  state.hold = hold;
  state.bits = bits;
  //---

  if (state.wsize || (_out !== strm.avail_out && state.mode < BAD &&
                      (state.mode < CHECK || flush !== Z_FINISH$1))) {
    if (updatewindow(strm, strm.output, strm.next_out, _out - strm.avail_out)) ;
  }
  _in -= strm.avail_in;
  _out -= strm.avail_out;
  strm.total_in += _in;
  strm.total_out += _out;
  state.total += _out;
  if ((state.wrap & 4) && _out) {
    strm.adler = state.check = /*UPDATE_CHECK(state.check, strm.next_out - _out, _out);*/
      (state.flags ? crc32_1(state.check, output, _out, strm.next_out - _out) : adler32_1(state.check, output, _out, strm.next_out - _out));
  }
  strm.data_type = state.bits + (state.last ? 64 : 0) +
                    (state.mode === TYPE ? 128 : 0) +
                    (state.mode === LEN_ || state.mode === COPY_ ? 256 : 0);
  if (((_in === 0 && _out === 0) || flush === Z_FINISH$1) && ret === Z_OK$1) {
    ret = Z_BUF_ERROR;
  }
  return ret;
};


const inflateEnd = (strm) => {

  if (inflateStateCheck(strm)) {
    return Z_STREAM_ERROR$1;
  }

  let state = strm.state;
  if (state.window) {
    state.window = null;
  }
  strm.state = null;
  return Z_OK$1;
};


const inflateGetHeader = (strm, head) => {

  /* check state */
  if (inflateStateCheck(strm)) { return Z_STREAM_ERROR$1; }
  const state = strm.state;
  if ((state.wrap & 2) === 0) { return Z_STREAM_ERROR$1; }

  /* save header structure */
  state.head = head;
  head.done = false;
  return Z_OK$1;
};


const inflateSetDictionary = (strm, dictionary) => {
  const dictLength = dictionary.length;

  let state;
  let dictid;
  let ret;

  /* check state */
  if (inflateStateCheck(strm)) { return Z_STREAM_ERROR$1; }
  state = strm.state;

  if (state.wrap !== 0 && state.mode !== DICT) {
    return Z_STREAM_ERROR$1;
  }

  /* check for correct dictionary identifier */
  if (state.mode === DICT) {
    dictid = 1; /* adler32(0, null, 0)*/
    /* dictid = adler32(dictid, dictionary, dictLength); */
    dictid = adler32_1(dictid, dictionary, dictLength, 0);
    if (dictid !== state.check) {
      return Z_DATA_ERROR$1;
    }
  }
  /* copy dictionary to window using updatewindow(), which will amend the
   existing dictionary if appropriate */
  ret = updatewindow(strm, dictionary, dictLength, dictLength);
  if (ret) {
    state.mode = MEM;
    return Z_MEM_ERROR$1;
  }
  state.havedict = 1;
  // Tracev((stderr, "inflate:   dictionary set\n"));
  return Z_OK$1;
};


var inflateReset_1 = inflateReset;
var inflateReset2_1 = inflateReset2;
var inflateResetKeep_1 = inflateResetKeep;
var inflateInit_1 = inflateInit;
var inflateInit2_1 = inflateInit2;
var inflate_2$1 = inflate$2;
var inflateEnd_1 = inflateEnd;
var inflateGetHeader_1 = inflateGetHeader;
var inflateSetDictionary_1 = inflateSetDictionary;
var inflateInfo = 'pako inflate (from Nodeca project)';

/* Not implemented
module.exports.inflateCodesUsed = inflateCodesUsed;
module.exports.inflateCopy = inflateCopy;
module.exports.inflateGetDictionary = inflateGetDictionary;
module.exports.inflateMark = inflateMark;
module.exports.inflatePrime = inflatePrime;
module.exports.inflateSync = inflateSync;
module.exports.inflateSyncPoint = inflateSyncPoint;
module.exports.inflateUndermine = inflateUndermine;
module.exports.inflateValidate = inflateValidate;
*/

var inflate_1$2 = {
	inflateReset: inflateReset_1,
	inflateReset2: inflateReset2_1,
	inflateResetKeep: inflateResetKeep_1,
	inflateInit: inflateInit_1,
	inflateInit2: inflateInit2_1,
	inflate: inflate_2$1,
	inflateEnd: inflateEnd_1,
	inflateGetHeader: inflateGetHeader_1,
	inflateSetDictionary: inflateSetDictionary_1,
	inflateInfo: inflateInfo
};

// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//   claim that you wrote the original software. If you use this software
//   in a product, an acknowledgment in the product documentation would be
//   appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
//   misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

function GZheader() {
  /* true if compressed data believed to be text */
  this.text       = 0;
  /* modification time */
  this.time       = 0;
  /* extra flags (not used when writing a gzip file) */
  this.xflags     = 0;
  /* operating system */
  this.os         = 0;
  /* pointer to extra field or Z_NULL if none */
  this.extra      = null;
  /* extra field length (valid if extra != Z_NULL) */
  this.extra_len  = 0; // Actually, we don't need it in JS,
                       // but leave for few code modifications

  //
  // Setup limits is not necessary because in js we should not preallocate memory
  // for inflate use constant limit in 65536 bytes
  //

  /* space at extra (only when reading header) */
  // this.extra_max  = 0;
  /* pointer to zero-terminated file name or Z_NULL */
  this.name       = '';
  /* space at name (only when reading header) */
  // this.name_max   = 0;
  /* pointer to zero-terminated comment or Z_NULL */
  this.comment    = '';
  /* space at comment (only when reading header) */
  // this.comm_max   = 0;
  /* true if there was or will be a header crc */
  this.hcrc       = 0;
  /* true when done reading gzip header (not used when writing a gzip file) */
  this.done       = false;
}

var gzheader = GZheader;

const toString = Object.prototype.toString;

/* Public constants ==========================================================*/
/* ===========================================================================*/

const {
  Z_NO_FLUSH, Z_FINISH,
  Z_OK, Z_STREAM_END, Z_NEED_DICT, Z_STREAM_ERROR, Z_DATA_ERROR, Z_MEM_ERROR
} = constants$2;

/* ===========================================================================*/


/**
 * class Inflate
 *
 * Generic JS-style wrapper for zlib calls. If you don't need
 * streaming behaviour - use more simple functions: [[inflate]]
 * and [[inflateRaw]].
 **/

/* internal
 * inflate.chunks -> Array
 *
 * Chunks of output data, if [[Inflate#onData]] not overridden.
 **/

/**
 * Inflate.result -> Uint8Array|String
 *
 * Uncompressed result, generated by default [[Inflate#onData]]
 * and [[Inflate#onEnd]] handlers. Filled after you push last chunk
 * (call [[Inflate#push]] with `Z_FINISH` / `true` param).
 **/

/**
 * Inflate.err -> Number
 *
 * Error code after inflate finished. 0 (Z_OK) on success.
 * Should be checked if broken data possible.
 **/

/**
 * Inflate.msg -> String
 *
 * Error message, if [[Inflate.err]] != 0
 **/


/**
 * new Inflate(options)
 * - options (Object): zlib inflate options.
 *
 * Creates new inflator instance with specified params. Throws exception
 * on bad params. Supported options:
 *
 * - `windowBits`
 * - `dictionary`
 *
 * [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced)
 * for more information on these.
 *
 * Additional options, for internal needs:
 *
 * - `chunkSize` - size of generated data chunks (16K by default)
 * - `raw` (Boolean) - do raw inflate
 * - `to` (String) - if equal to 'string', then result will be converted
 *   from utf8 to utf16 (javascript) string. When string output requested,
 *   chunk length can differ from `chunkSize`, depending on content.
 *
 * By default, when no options set, autodetect deflate/gzip data format via
 * wrapper header.
 *
 * ##### Example:
 *
 * ```javascript
 * const pako = require('pako')
 * const chunk1 = new Uint8Array([1,2,3,4,5,6,7,8,9])
 * const chunk2 = new Uint8Array([10,11,12,13,14,15,16,17,18,19]);
 *
 * const inflate = new pako.Inflate({ level: 3});
 *
 * inflate.push(chunk1, false);
 * inflate.push(chunk2, true);  // true -> last chunk
 *
 * if (inflate.err) { throw new Error(inflate.err); }
 *
 * console.log(inflate.result);
 * ```
 **/
function Inflate$1(options) {
  this.options = common.assign({
    chunkSize: 1024 * 64,
    windowBits: 15,
    to: ''
  }, options || {});

  const opt = this.options;

  // Force window size for `raw` data, if not set directly,
  // because we have no header for autodetect.
  if (opt.raw && (opt.windowBits >= 0) && (opt.windowBits < 16)) {
    opt.windowBits = -opt.windowBits;
    if (opt.windowBits === 0) { opt.windowBits = -15; }
  }

  // If `windowBits` not defined (and mode not raw) - set autodetect flag for gzip/deflate
  if ((opt.windowBits >= 0) && (opt.windowBits < 16) &&
      !(options && options.windowBits)) {
    opt.windowBits += 32;
  }

  // Gzip header has no info about windows size, we can do autodetect only
  // for deflate. So, if window size not set, force it to max when gzip possible
  if ((opt.windowBits > 15) && (opt.windowBits < 48)) {
    // bit 3 (16) -> gzipped data
    // bit 4 (32) -> autodetect gzip/deflate
    if ((opt.windowBits & 15) === 0) {
      opt.windowBits |= 15;
    }
  }

  this.err    = 0;      // error code, if happens (0 = Z_OK)
  this.msg    = '';     // error message
  this.ended  = false;  // used to avoid multiple onEnd() calls
  this.chunks = [];     // chunks of compressed data

  this.strm   = new zstream();
  this.strm.avail_out = 0;

  let status  = inflate_1$2.inflateInit2(
    this.strm,
    opt.windowBits
  );

  if (status !== Z_OK) {
    throw new Error(messages[status]);
  }

  this.header = new gzheader();

  inflate_1$2.inflateGetHeader(this.strm, this.header);

  // Setup dictionary
  if (opt.dictionary) {
    // Convert data if needed
    if (typeof opt.dictionary === 'string') {
      opt.dictionary = strings.string2buf(opt.dictionary);
    } else if (toString.call(opt.dictionary) === '[object ArrayBuffer]') {
      opt.dictionary = new Uint8Array(opt.dictionary);
    }
    if (opt.raw) { //In raw mode we need to set the dictionary early
      status = inflate_1$2.inflateSetDictionary(this.strm, opt.dictionary);
      if (status !== Z_OK) {
        throw new Error(messages[status]);
      }
    }
  }
}

/**
 * Inflate#push(data[, flush_mode]) -> Boolean
 * - data (Uint8Array|ArrayBuffer): input data
 * - flush_mode (Number|Boolean): 0..6 for corresponding Z_NO_FLUSH..Z_TREE
 *   flush modes. See constants. Skipped or `false` means Z_NO_FLUSH,
 *   `true` means Z_FINISH.
 *
 * Sends input data to inflate pipe, generating [[Inflate#onData]] calls with
 * new output chunks. Returns `true` on success. If end of stream detected,
 * [[Inflate#onEnd]] will be called.
 *
 * `flush_mode` is not needed for normal operation, because end of stream
 * detected automatically. You may try to use it for advanced things, but
 * this functionality was not tested.
 *
 * On fail call [[Inflate#onEnd]] with error code and return false.
 *
 * ##### Example
 *
 * ```javascript
 * push(chunk, false); // push one of data chunks
 * ...
 * push(chunk, true);  // push last chunk
 * ```
 **/
Inflate$1.prototype.push = function (data, flush_mode) {
  const strm = this.strm;
  const chunkSize = this.options.chunkSize;
  const dictionary = this.options.dictionary;
  let status, _flush_mode, last_avail_out;

  if (this.ended) return false;

  if (flush_mode === ~~flush_mode) _flush_mode = flush_mode;
  else _flush_mode = flush_mode === true ? Z_FINISH : Z_NO_FLUSH;

  // Convert data if needed
  if (toString.call(data) === '[object ArrayBuffer]') {
    strm.input = new Uint8Array(data);
  } else {
    strm.input = data;
  }

  strm.next_in = 0;
  strm.avail_in = strm.input.length;

  for (;;) {
    if (strm.avail_out === 0) {
      strm.output = new Uint8Array(chunkSize);
      strm.next_out = 0;
      strm.avail_out = chunkSize;
    }

    status = inflate_1$2.inflate(strm, _flush_mode);

    if (status === Z_NEED_DICT && dictionary) {
      status = inflate_1$2.inflateSetDictionary(strm, dictionary);

      if (status === Z_OK) {
        status = inflate_1$2.inflate(strm, _flush_mode);
      } else if (status === Z_DATA_ERROR) {
        // Replace code with more verbose
        status = Z_NEED_DICT;
      }
    }

    // Skip snyc markers if more data follows and not raw mode
    while (strm.avail_in > 0 &&
           status === Z_STREAM_END &&
           strm.state.wrap > 0 &&
           data[strm.next_in] !== 0)
    {
      inflate_1$2.inflateReset(strm);
      status = inflate_1$2.inflate(strm, _flush_mode);
    }

    switch (status) {
      case Z_STREAM_ERROR:
      case Z_DATA_ERROR:
      case Z_NEED_DICT:
      case Z_MEM_ERROR:
        this.onEnd(status);
        this.ended = true;
        return false;
    }

    // Remember real `avail_out` value, because we may patch out buffer content
    // to align utf8 strings boundaries.
    last_avail_out = strm.avail_out;

    if (strm.next_out) {
      if (strm.avail_out === 0 || status === Z_STREAM_END) {

        if (this.options.to === 'string') {

          let next_out_utf8 = strings.utf8border(strm.output, strm.next_out);

          let tail = strm.next_out - next_out_utf8;
          let utf8str = strings.buf2string(strm.output, next_out_utf8);

          // move tail & realign counters
          strm.next_out = tail;
          strm.avail_out = chunkSize - tail;
          if (tail) strm.output.set(strm.output.subarray(next_out_utf8, next_out_utf8 + tail), 0);

          this.onData(utf8str);

        } else {
          this.onData(strm.output.length === strm.next_out ? strm.output : strm.output.subarray(0, strm.next_out));
        }
      }
    }

    // Must repeat iteration if out buffer is full
    if (status === Z_OK && last_avail_out === 0) continue;

    // Finalize if end of stream reached.
    if (status === Z_STREAM_END) {
      status = inflate_1$2.inflateEnd(this.strm);
      this.onEnd(status);
      this.ended = true;
      return true;
    }

    if (strm.avail_in === 0) break;
  }

  return true;
};


/**
 * Inflate#onData(chunk) -> Void
 * - chunk (Uint8Array|String): output data. When string output requested,
 *   each chunk will be string.
 *
 * By default, stores data blocks in `chunks[]` property and glue
 * those in `onEnd`. Override this handler, if you need another behaviour.
 **/
Inflate$1.prototype.onData = function (chunk) {
  this.chunks.push(chunk);
};


/**
 * Inflate#onEnd(status) -> Void
 * - status (Number): inflate status. 0 (Z_OK) on success,
 *   other if not.
 *
 * Called either after you tell inflate that the input stream is
 * complete (Z_FINISH). By default - join collected chunks,
 * free memory and fill `results` / `err` properties.
 **/
Inflate$1.prototype.onEnd = function (status) {
  // On success - join
  if (status === Z_OK) {
    if (this.options.to === 'string') {
      this.result = this.chunks.join('');
    } else {
      this.result = common.flattenChunks(this.chunks);
    }
  }
  this.chunks = [];
  this.err = status;
  this.msg = this.strm.msg;
};


/**
 * inflate(data[, options]) -> Uint8Array|String
 * - data (Uint8Array|ArrayBuffer): input data to decompress.
 * - options (Object): zlib inflate options.
 *
 * Decompress `data` with inflate/ungzip and `options`. Autodetect
 * format via wrapper header by default. That's why we don't provide
 * separate `ungzip` method.
 *
 * Supported options are:
 *
 * - windowBits
 *
 * [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced)
 * for more information.
 *
 * Sugar (options):
 *
 * - `raw` (Boolean) - say that we work with raw stream, if you don't wish to specify
 *   negative windowBits implicitly.
 * - `to` (String) - if equal to 'string', then result will be converted
 *   from utf8 to utf16 (javascript) string. When string output requested,
 *   chunk length can differ from `chunkSize`, depending on content.
 *
 *
 * ##### Example:
 *
 * ```javascript
 * const pako = require('pako');
 * const input = pako.deflate(new Uint8Array([1,2,3,4,5,6,7,8,9]));
 * let output;
 *
 * try {
 *   output = pako.inflate(input);
 * } catch (err) {
 *   console.log(err);
 * }
 * ```
 **/
function inflate$1(input, options) {
  const inflator = new Inflate$1(options);

  inflator.push(input);

  // That will never happens, if you don't cheat with options :)
  if (inflator.err) throw inflator.msg || messages[inflator.err];

  return inflator.result;
}


/**
 * inflateRaw(data[, options]) -> Uint8Array|String
 * - data (Uint8Array|ArrayBuffer): input data to decompress.
 * - options (Object): zlib inflate options.
 *
 * The same as [[inflate]], but creates raw data, without wrapper
 * (header and adler32 crc).
 **/
function inflateRaw$1(input, options) {
  options = options || {};
  options.raw = true;
  return inflate$1(input, options);
}


/**
 * ungzip(data[, options]) -> Uint8Array|String
 * - data (Uint8Array|ArrayBuffer): input data to decompress.
 * - options (Object): zlib inflate options.
 *
 * Just shortcut to [[inflate]], because it autodetects format
 * by header.content. Done for convenience.
 **/


var Inflate_1$1 = Inflate$1;
var inflate_2 = inflate$1;
var inflateRaw_1$1 = inflateRaw$1;
var ungzip$1 = inflate$1;
var constants = constants$2;

var inflate_1$1 = {
	Inflate: Inflate_1$1,
	inflate: inflate_2,
	inflateRaw: inflateRaw_1$1,
	ungzip: ungzip$1,
	constants: constants
};

const { Inflate, inflate, inflateRaw, ungzip } = inflate_1$1;
var inflateRaw_1 = inflateRaw;
var ungzip_1 = ungzip;

const FEXTRA = 4;  // gzip spec F.EXTRA flag

function isgzipped(data) {
    const b = ArrayBuffer.isView(data) ? data : new Uint8Array(data);
    return b[0] ===31 && b[1] === 139;
}

/**
 * Pako does not properly ungzip block compressed files if > 1 block is present.  Test for bgzip and use wrapper.
 */
function ungzip_blocks(data) {
    const ba = ArrayBuffer.isView(data) ? data : new Uint8Array(data);
    const b = ba[3] & FEXTRA;
    if (b !== 0 && ba[12] === 66 && ba[13] === 67) {
        return unbgzf(ba.buffer);
    } else {
        return ungzip_1(ba);
    }
}

// Uncompress data,  assumed to be series of bgzipped blocks
function unbgzf(data, lim) {

    const oBlockList = [];
    let ptr = 0;
    let totalSize = 0;

    lim = lim || data.byteLength - 18;

    while (ptr < lim) {
        try {
            const ba = ArrayBuffer.isView(data) ? data : new Uint8Array(data, ptr, 18);
            const xlen = (ba[11] << 8) | (ba[10]);
            const flg = ba[3];
            const fextra = flg & FEXTRA;
            const si1 = ba[12];
            const si2 = ba[13];
            const slen = (ba[15] << 8) | (ba[14]);
            const bsize = ((ba[17] << 8) | (ba[16])) + 1;
            const start = 12 + xlen + ptr;    // Start of CDATA
            const bytesLeft = data.byteLength - start;
            const cDataSize = bsize - xlen - 19;
            if (bytesLeft < cDataSize || cDataSize <= 0) break;

            const a = new Uint8Array(data, start, cDataSize);
            const unc = inflateRaw_1(a);

            // const inflate = new Zlib.RawInflate(a);
            // const unc = inflate.decompress();

            ptr += (cDataSize - 1) + 26; //inflate.ip + 26
            totalSize += unc.byteLength;
            oBlockList.push(unc);
        } catch (e) {
            console.error(e);
            break;
        }
    }

    // Concatenate decompressed blocks
    if (oBlockList.length === 1) {
        return oBlockList[0];
    } else {
        const out = new Uint8Array(totalSize);
        let cursor = 0;
        for (let i = 0; i < oBlockList.length; ++i) {
            var b = new Uint8Array(oBlockList[i]);
            arrayCopy(b, 0, out, cursor, b.length);
            cursor += b.length;
        }
        return out;
    }
}

// From Thomas Down's zlib implementation

const testArray = new Uint8Array(1);
const hasSubarray = (typeof testArray.subarray === 'function');

function arrayCopy(src, srcOffset, dest, destOffset, count) {
    if (count === 0) {
        return;
    }
    if (!src) {
        throw "Undef src";
    } else if (!dest) {
        throw "Undef dest";
    }
    if (srcOffset === 0 && count === src.length) {
        arrayCopy_fast(src, dest, destOffset);
    } else if (hasSubarray) {
        arrayCopy_fast(src.subarray(srcOffset, srcOffset + count), dest, destOffset);
    } else if (src.BYTES_PER_ELEMENT === 1 && count > 100) {
        arrayCopy_fast(new Uint8Array(src.buffer, src.byteOffset + srcOffset, count), dest, destOffset);
    } else {
        arrayCopy_slow(src, srcOffset, dest, destOffset, count);
    }
}

function arrayCopy_slow(src, srcOffset, dest, destOffset, count) {
    for (let i = 0; i < count; ++i) {
        dest[destOffset + i] = src[srcOffset + i];
    }
}

function arrayCopy_fast(src, dest, destOffset) {
    dest.set(src, destOffset);
}


/**
 * @param dataURI
 * @returns {Array<number>|Uint8Array}
 */
function decodeDataURI(dataURI, gzip) {

    const split = dataURI.split(',');
    const info = split[0].split(':')[1];
    let dataString = split[1];

    if (info.indexOf('base64') >= 0) {
        dataString = atob(dataString);

        const bytes = new Uint8Array(dataString.length);
        for (let i = 0; i < dataString.length; i++) {
            bytes[i] = dataString.charCodeAt(i);
        }

        let plain;
        if (gzip || info.indexOf('gzip') > 0) {
            plain = ungzip_1(bytes);
        } else {
            plain = bytes;
        }
        return plain
    } else {
        return decodeURIComponent(dataString);      // URL encoded string -- not currently used or tested
    }
}

// Support for oauth token based authorization
// This class supports explicit setting of an oauth token either globally or for specific hosts.
//
// The variable oauth.google.access_token, which becomes igv.oauth.google.access_token on ES5 conversion is
// supported for backward compatibility

const DEFAULT_HOST = "googleapis";

class Oauth {

    constructor() {
        this.oauthTokens = {};
    }


    setToken(token, host) {
        host = host || DEFAULT_HOST;
        this.oauthTokens[host] = token;
    }

    getToken(host) {
        host = host || DEFAULT_HOST;
        let token;
        for (let key of Object.keys(this.oauthTokens)) {
            const regex = wildcardToRegExp(key);
            if (regex.test(host)) {
                token = this.oauthTokens[key];
                break
            }
        }
        return token
    }

    removeToken(host) {
        host = host || DEFAULT_HOST;
        for (let key of Object.keys(this.oauthTokens)) {
            const regex = wildcardToRegExp(key);
            if (regex.test(host)) {
                this.oauthTokens[key] = undefined;
            }
        }
    }

    // Special object for google -- legacy support
    // google: {
    //     setToken: function (token) {
    //         oauth.setToken(token);
    //     }
    // }
}


/**
 * Creates a RegExp from the given string, converting asterisks to .* expressions,
 * and escaping all other characters.
 *
 * credit https://gist.github.com/donmccurdy/6d073ce2c6f3951312dfa45da14a420f
 */
function wildcardToRegExp(s) {
    return new RegExp('^' + s.split(/\*+/).map(regExpEscape).join('.*') + '$')
}

/**
 * RegExp-escapes all characters in the given string.
 *
 * credit https://gist.github.com/donmccurdy/6d073ce2c6f3951312dfa45da14a420f
 */
function regExpEscape(s) {
    return s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
}

function isGoogleURL(url) {
    return (url.includes("googleapis") && !url.includes("urlshortener")) ||
        isGoogleStorageURL(url) ||
        isGoogleDriveURL(url)
}

function isGoogleStorageURL(url) {
    return url.startsWith("gs://") ||
        url.startsWith("https://www.googleapis.com/storage") ||
        url.startsWith("https://storage.cloud.google.com") ||
        url.startsWith("https://storage.googleapis.com");
}

function isGoogleDriveURL(url) {
    return url.indexOf("drive.google.com") >= 0 || url.indexOf("www.googleapis.com/drive") > 0
}

/**
 * Translate gs:// urls to https
 * See https://cloud.google.com/storage/docs/json_api/v1
 * @param gsUrl
 * @returns {string|*}
 */
function translateGoogleCloudURL(gsUrl) {

    let {bucket, object} = parseBucketName(gsUrl);
    object = encode(object);

    const qIdx = gsUrl.indexOf('?');
    const paramString = (qIdx > 0) ? gsUrl.substring(qIdx) + "&alt=media" : "?alt=media";

    return `https://storage.googleapis.com/storage/v1/b/${bucket}/o/${object}${paramString}`
}

/**
 * Parse a google bucket and object name from a google storage URL.  Known forms include
 *
 * gs://BUCKET_NAME/OBJECT_NAME
 * https://storage.googleapis.com/BUCKET_NAME/OBJECT_NAME
 * https://storage.googleapis.com/storage/v1/b/BUCKET_NAME/o/OBJECT_NAME
 * https://www.googleapis.com/storage/v1/b/BUCKET_NAME/o/OBJECT_NAME"
 * https://storage.googleapis.com/download/storage/v1/b/BUCKET_NAME/o/OBJECT_NAME
 *
 * @param url
 */
function parseBucketName(url) {

    let bucket;
    let object;

    if (url.startsWith("gs://")) {
        const i = url.indexOf('/', 5);
        if (i >= 0) {
            bucket = url.substring(5, i);
            const qIdx = url.indexOf('?');
            object = (qIdx < 0) ? url.substring(i + 1) : url.substring(i + 1, qIdx);
        }

    } else if (url.startsWith("https://storage.googleapis.com") || url.startsWith("https://storage.cloud.google.com")) {
        const bucketIdx = url.indexOf("/v1/b/", 8);
        if (bucketIdx > 0) {
            const objIdx = url.indexOf("/o/", bucketIdx);
            if (objIdx > 0) {
                const queryIdx = url.indexOf("?", objIdx);
                bucket = url.substring(bucketIdx + 6, objIdx);
                object = queryIdx > 0 ? url.substring(objIdx + 3, queryIdx) : url.substring(objIdx + 3);
            }

        } else {
            const idx1 = url.indexOf("/", 8);
            const idx2 = url.indexOf("/", idx1+1);
            const idx3 = url.indexOf("?", idx2);
            if (idx2 > 0) {
                bucket = url.substring(idx1+1, idx2);
                object = idx3 < 0 ? url.substring(idx2+1) : url.substring(idx2+1, idx3);
            }
        }

    } else if (url.startsWith("https://www.googleapis.com/storage/v1/b")) {
        const bucketIdx = url.indexOf("/v1/b/", 8);
        const objIdx = url.indexOf("/o/", bucketIdx);
        if (objIdx > 0) {
            const queryIdx = url.indexOf("?", objIdx);
            bucket = url.substring(bucketIdx + 6, objIdx);
            object = queryIdx > 0 ? url.substring(objIdx + 3, queryIdx) : url.substring(objIdx + 3);
        }
    }

    if (bucket && object) {
        return {
            bucket, object
        }
    } else {
        throw Error(`Unrecognized Google Storage URI: ${url}`)
    }

}

/**
 * Percent a GCS object name.  See https://cloud.google.com/storage/docs/request-endpoints
 * Specific characters to encode:
 *   !, #, $, &, ', (, ), *, +, ,, /, :, ;, =, ?, @, [, ], and space characters.
 * @param obj
 */

function encode(objectName) {

    let result = '';
    objectName.split('').forEach(function(letter) {
        if(encodings.has(letter)) {
            result += encodings.get(letter);
        } else {
            result += letter;
        }
    });
    return result;
}

//	%23	%24	%25	%26	%27	%28	%29	%2A	%2B	%2C	%2F	%3A	%3B	%3D	%3F	%40	%5B	%5D
const encodings = new Map();
encodings.set("!", "%21");
encodings.set("#", "%23");
encodings.set("$", "%24");
encodings.set("%", "%25");
encodings.set("&", "%26");
encodings.set("'", "%27");
encodings.set("(", "%28");
encodings.set(")", "%29");
encodings.set("*", "%2A");
encodings.set("+", "%2B");
encodings.set(",", "%2C");
encodings.set("/", "%2F");
encodings.set(":", "%3A");
encodings.set(";", "%3B");
encodings.set("=", "%3D");
encodings.set("?", "%3F");
encodings.set("@", "%40");
encodings.set("[", "%5B");
encodings.set("]", "%5D");
encodings.set(" ", "%20");

// Convenience functions for the gapi oAuth library.

function isInitialized() {
    return window.google && window.google.igv
}

/**
 * Return the current access token if the user is signed in, or undefined otherwise.  This function does not
 * attempt a signIn or request any specfic scopes.
 *
 * @returns access_token || undefined
 */
function getCurrentAccessToken() {
    return (isInitialized() && google.igv.tokenResponse && Date.now() < google.igv.tokenExpiresAt) ?
        google.igv.tokenResponse.access_token :
        undefined
}


let promise;
/**
 * Return a promise for an access token for the given scope.  If the user hasn't authorized the scope request it
 *
 * @param scope
 * @returns {Promise<unknown>}
 */
async function getAccessToken(scope) {

    if (!isInitialized()) {
        throw Error("Google oAuth has not been initialized")
    }

    if (google.igv.tokenResponse &&
        Date.now() < google.igv.tokenExpiresAt &&
        google.accounts.oauth2.hasGrantedAllScopes(google.igv.tokenResponse, scope)) {
        return google.igv.tokenResponse.access_token
    } else {
        const tokenClient = google.igv.tokenClient;
        if(!promise) {
            promise = new Promise((resolve, reject) => {
                try {
                    // Settle this promise in the response callback for requestAccessToken()
                    tokenClient.callback = (tokenResponse) => {
                        if (tokenResponse.error !== undefined) {
                            reject(tokenResponse);
                        }
                        google.igv.tokenResponse = tokenResponse;
                        google.igv.tokenExpiresAt = Date.now() + tokenResponse.expires_in * 1000;
                        resolve(tokenResponse.access_token);
                    };
                    tokenClient.requestAccessToken({scope});
                } catch (err) {
                }
            });
        }
        return promise
    }
}

// gapi.auth2.getAuthInstance().isSignedIn.listen(status => {
//     const user = gapi.auth2.getAuthInstance().currentUser.get()
//     queryGoogleAuthenticationStatus(user, status)
// })

function getScopeForURL(url) {
    if (isGoogleDriveURL(url)) {
        return "https://www.googleapis.com/auth/drive.file"
    } else if (isGoogleStorageURL(url)) {
        return "https://www.googleapis.com/auth/devstorage.read_only"
    } else {
        return 'https://www.googleapis.com/auth/userinfo.profile'
    }
}

function getApiKey() {
    return google.igv.apiKey
}

/**
 * Return information about a specific google drive URL
 *
 * @param googleDriveURL
 * @returns {Promise<any>}
 */
async function getDriveFileInfo(googleDriveURL) {

    const id = getGoogleDriveFileID(googleDriveURL);
    let endPoint = "https://www.googleapis.com/drive/v3/files/" + id + "?supportsTeamDrives=true";
    const apiKey = getApiKey();
    if (apiKey) {
        endPoint += "&key=" + apiKey;
    }
    const response = await fetch(endPoint);
    let json = await response.json();
    if (json.error && json.error.code === 404) {
        let scope = "https://www.googleapis.com/auth/drive.readonly";
        const access_token = await getAccessToken(scope);
        if (access_token) {
            const response = await fetch(endPoint, {
                headers: {
                    'Authorization': `Bearer ${access_token}`
                }
            });
            json = await response.json();
            if (json.error) {
                throw Error(json.error);
            }
        } else {
            throw Error(json.error);
        }
    }
    return json;
}


function getDriveDownloadURL(link) {
    // Return a google drive download url for the sharable link
    //https://drive.google.com/open?id=0B-lleX9c2pZFbDJ4VVRxakJzVGM
    //https://drive.google.com/file/d/1_FC4kCeO8E3V4dJ1yIW7A0sn1yURKIX-/view?usp=sharing
    var id = getGoogleDriveFileID(link);
    return id ? "https://www.googleapis.com/drive/v3/files/" + id + "?alt=media&supportsTeamDrives=true" : link;
}

function getGoogleDriveFileID(link) {

    //https://drive.google.com/file/d/1_FC4kCeO8E3V4dJ1yIW7A0sn1yURKIX-/view?usp=sharing
    //https://www.googleapis.com/drive/v3/files/1w-tvo6p1SH4p1OaQSVxpkV_EJgGIstWF?alt=media&supportsTeamDrives=true"

    if (link.includes("/open?id=")) {
        const i1 = link.indexOf("/open?id=") + 9;
        const i2 = link.indexOf("&");
        if (i1 > 0 && i2 > i1) {
            return link.substring(i1, i2)
        } else if (i1 > 0) {
            return link.substring(i1);
        }

    } else if (link.includes("/file/d/")) {
        const i1 = link.indexOf("/file/d/") + 8;
        const i2 = link.lastIndexOf("/");
        return link.substring(i1, i2);

    } else if (link.startsWith("https://www.googleapis.com/drive")) {
        let i1 = link.indexOf("/files/");
        const i2 = link.indexOf("?");
        if (i1 > 0) {
            i1 += 7;
            return i2 > 0 ?
                link.substring(i1, i2) :
                link.substring(i1)
        }
    }

    throw Error("Unknown Google Drive url format: " + link);


}

// The MIT License (MIT)

/**
 * @constructor
 * @param {Object} options A set op options to pass to the throttle function
 *        @param {number} requestsPerSecond The amount of requests per second
 *                                          the library will limit to
 */
class Throttle {
    constructor(options) {
        this.requestsPerSecond = options.requestsPerSecond || 10;
        this.lastStartTime = 0;
        this.queued = [];
    }

    /**
     * Adds a promise
     * @param {Function} async function to be executed
     * @param {Object} options A set of options.
     * @return {Promise} A promise
     */
    add(asyncFunction, options) {

        var self = this;
        return new Promise(function (resolve, reject) {
            self.queued.push({
                resolve: resolve,
                reject: reject,
                asyncFunction: asyncFunction,
            });
            self.dequeue();
        });
    }

    /**
     * Adds all the promises passed as parameters
     * @param {Function} promises An array of functions that return a promise
     * @param {Object} options A set of options.
     * @param {number} options.signal An AbortSignal object that can be used to abort the returned promise
     * @param {number} options.weight A "weight" of each operation resolving by array of promises
     * @return {Promise} A promise that succeeds when all the promises passed as options do
     */
    addAll(promises, options) {
        var addedPromises = promises.map(function (promise) {
            return this.add(promise, options);
        }.bind(this));

        return Promise.all(addedPromises);
    };

    /**
     * Dequeues a promise
     * @return {void}
     */
    dequeue() {
        if (this.queued.length > 0) {
            var now = new Date(),
                inc = (1000 / this.requestsPerSecond) + 1,
                elapsed = now - this.lastStartTime;

            if (elapsed >= inc) {
                this._execute();
            } else {
                // we have reached the limit, schedule a dequeue operation
                setTimeout(function () {
                    this.dequeue();
                }.bind(this), inc - elapsed);
            }
        }
    }

    /**
     * Executes the promise
     * @private
     * @return {void}
     */
    async _execute() {
        this.lastStartTime = new Date();
        var candidate = this.queued.shift();
        const f = candidate.asyncFunction;
        try {
            const r = await f();
            candidate.resolve(r);
        } catch (e) {
            candidate.reject(e);
        }

    }


}

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

class IGVXhr {

    constructor() {
        this.apiKey = undefined;
        this.googleThrottle = new Throttle({
            requestsPerSecond: 8
        });
        this.RANGE_WARNING_GIVEN = false;
        this.oauth = new Oauth();
    }

    setApiKey(key) {
        this.apiKey = key;
    }

    async loadArrayBuffer(url, options) {
        options = options || {};
        if (!options.responseType) {
            options.responseType = "arraybuffer";
        }
        if (isFile(url)) {
            return this._loadFileSlice(url, options)
        } else {
            return this.load(url, options)
        }
    }

    /**
     * A wrapper around loadArrayBuffer that inflates gzipped data
     * @param url
     * @param options
     * @returns {Promise<Uint8Array>}
     */
     async loadByteArray(url, options) {
        const arraybuffer = await this.loadArrayBuffer(url, options);
        let plain;
        if (isgzipped(arraybuffer)) {
            plain = ungzip_blocks(arraybuffer);
        } else {
            plain = new Uint8Array(arraybuffer);
        }
        return plain
    }


    async loadJson(url, options) {
        options = options || {};
        const method = options.method || (options.sendData ? "POST" : "GET");
        if (method === "POST") {
            options.contentType = "application/json";
        }
        const result = await this.loadString(url, options);
        if (result) {
            return JSON.parse(result)
        } else {
            return result
        }
    }

    async loadString(path, options) {
        options = options || {};
        if (path instanceof File) {
            return this._loadStringFromFile(path, options)
        } else {
            return this._loadStringFromUrl(path, options)
        }
    }

    async load(url, options) {

        options = options || {};
        const urlType = typeof url;

        // Resolve functions, promises, and functions that return promises
        url = await (typeof url === 'function' ? url() : url);

        if (isFile(url)) {
            return this._loadFileSlice(url, options)
        } else if (typeof url.startsWith === 'function') {   // Test for string
            if (url.startsWith("data:")) {
                const buffer = decodeDataURI(url).buffer;
                if (options.range) {
                    const rangeEnd = options.range.size ? options.range.start + options.range.size : buffer.byteLength;
                    return buffer.slice(options.range.start, rangeEnd)
                } else {
                    return buffer
                }
            } else {
                if (url.startsWith("https://drive.google.com")) {
                    url = getDriveDownloadURL(url);
                }
                if (isGoogleDriveURL(url) || url.startsWith("https://www.dropbox.com")) {
                    return this.googleThrottle.add(async () => {
                        return this._loadURL(url, options)
                    })
                } else {
                    return this._loadURL(url, options)
                }
            }
        } else {
            throw Error(`url must be either a 'File', 'string', 'function', or 'Promise'.  Actual type: ${urlType}`)
        }
    }

    async _loadURL(url, options) {

        const self = this;
        const _url = url;   // The unmodified URL, needed in case of an oAuth retry

        url = mapUrl(url);

        options = options || {};

        let oauthToken = options.oauthToken || this.getOauthToken(url);
        if (oauthToken) {
            oauthToken = await (typeof oauthToken === 'function' ? oauthToken() : oauthToken);
        }

        return new Promise(function (resolve, reject) {

            // Various Google tansformations
            if (isGoogleURL(url) && !isGoogleStorageSigned(url)) {
                if (isGoogleStorageURL(url)) {
                    url = translateGoogleCloudURL(url);
                }
                url = addApiKey(url);

                if (isGoogleDriveURL(url)) {
                    addTeamDrive(url);
                }

                // If we have an access token try it, but don't force a signIn or request for scopes yet
                if (!oauthToken) {
                    oauthToken = getCurrentGoogleAccessToken();
                }
            }

            const headers = options.headers || {};
            if (oauthToken) {
                addOauthHeaders(headers, oauthToken);
            }
            const range = options.range;


            const xhr = new XMLHttpRequest();
            const sendData = options.sendData || options.body;
            const method = options.method || (sendData ? "POST" : "GET");
            const responseType = options.responseType;
            const contentType = options.contentType;
            const mimeType = options.mimeType;

            xhr.open(method, url);

            if (options.timeout) {
                xhr.timeout = options.timeout;
            }

            if (range) {
                let rangeEnd = "";
                if (range.size) {
                    rangeEnd = range.start + range.size - 1;
                }
                xhr.setRequestHeader("Range", "bytes=" + range.start + "-" + rangeEnd);
                //      xhr.setRequestHeader("Cache-Control", "no-cache");    <= This can cause CORS issues, disabled for now
            }
            if (contentType) {
                xhr.setRequestHeader("Content-Type", contentType);
            }
            if (mimeType) {
                xhr.overrideMimeType(mimeType);
            }
            if (responseType) {
                xhr.responseType = responseType;
            }
            if (headers) {
                for (let key of Object.keys(headers)) {
                    const value = headers[key];
                    xhr.setRequestHeader(key, value);
                }
            }

            // NOTE: using withCredentials with servers that return "*" for access-allowed-origin will fail
            if (options.withCredentials === true) {
                xhr.withCredentials = true;
            }

            xhr.onload = async function (event) {

                // when the url points to a local file, the status is 0
                if (xhr.status === 0 || (xhr.status >= 200 && xhr.status <= 300)) {
                    if ("HEAD" === options.method) {
                        // Support fetching specific headers.  Attempting to fetch all headers can be problematic with CORS
                        const headers = options.requestedHeaders || ['content-length'];
                        const headerMap = {};
                        for (let h of headers) {
                            headerMap[h] = xhr.getResponseHeader(h);
                        }
                        resolve(headerMap);
                    } else {
                        // Assume "GET" or "POST"
                        if (range && xhr.status !== 206 && range.start !== 0) {

                            // For small files a range starting at 0 can return the whole file => 200
                            // Provide just the slice we asked for, throw out the rest quietly
                            // If file is large warn user
                            if (xhr.response.length > 100000 && !self.RANGE_WARNING_GIVEN) {
                                alert(`Warning: Range header ignored for URL: ${url}.  This can have severe performance impacts.`);
                            }
                            resolve(xhr.response.slice(range.start, range.start + range.size));
                        } else {
                            resolve(xhr.response);
                        }
                    }
                } else if (xhr.status === 416) {
                    handleError(Error(`416 Unsatisfiable Range`));
                } else if (isInitialized() &&
                    ((xhr.status === 404 || xhr.status === 401 || xhr.status === 403) &&
                        isGoogleURL(url)) &&
                    !options.retries) {
                    tryGoogleAuth();

                } else {
                    if (xhr.status === 403) {
                        handleError("Access forbidden: " + url);
                    } else {
                        handleError(xhr.status);
                    }
                }
            };


            xhr.onerror = function (event) {
                if (isGoogleURL(url) && !options.retries) {
                    tryGoogleAuth();
                } else {
                    handleError("Error accessing resource: " + url + " Status: " + xhr.status);
                }
            };

            xhr.ontimeout = function (event) {
                handleError("Timed out");
            };

            xhr.onabort = function (event) {
                reject(event);
            };

            try {
                xhr.send(sendData);
            } catch (e) {
                if (isGoogleURL(url) && !options.retries) {
                    tryGoogleAuth();
                } else {
                    handleError(e);
                }
            }


            function handleError(error) {
                if (reject) {
                    reject(error);
                } else {
                    throw error
                }
            }

            async function tryGoogleAuth() {
                try {
                    const accessToken = await fetchGoogleAccessToken(_url);
                    options.retries = 1;
                    options.oauthToken = accessToken;
                    const response = await self.load(_url, options);
                    resolve(response);
                } catch (e) {
                    if (e.error) {
                        const msg = e.error.startsWith("popup_blocked") ?
                            "Google login popup blocked by browser." :
                            e.error;
                        alert(msg);
                    } else {
                        handleError(e);
                    }
                }
            }
        })

    }

    async _loadFileSlice(localfile, options) {

        let blob = (options && options.range) ?
            localfile.slice(options.range.start, options.range.start + options.range.size) :
            localfile;

        const arrayBuffer = await blob.arrayBuffer();

        if ("arraybuffer" === options.responseType) {
            return arrayBuffer
        } else {
            return arrayBufferToString(arrayBuffer)
        }
    }

    async _loadStringFromFile(localfile, options) {

        const blob = options.range ? localfile.slice(options.range.start, options.range.start + options.range.size) : localfile;
        const arrayBuffer = await blob.arrayBuffer();
        return arrayBufferToString(arrayBuffer)
    }

    async _loadStringFromUrl(url, options) {

        options = options || {};
        options.responseType = "arraybuffer";
        const data = await this.load(url, options);
        return arrayBufferToString(data)
    }

    /**
     * Explicity set an oAuth token for use with given host.  If host is undefined token is used for google api access*
     * @param token
     * @param host
     */
    setOauthToken(token, host) {
        this.oauth.setToken(token, host);
    }

    /**
     * Return an oauth token for the URL if we have one.  This method does not force sign-in, and the token may
     * or may not be valid.  Sign-in is triggered on request failure.
     * *
     * @param url
     * @returns {*}
     */
    getOauthToken(url) {

        // Google is the default provider, don't try to parse host for google URLs
        const host = isGoogleURL(url) ?
            undefined :
            parseUri(url).host;

        // First check the explicit settings (i.e. token set through the API)
        let token = this.oauth.getToken(host);
        if (token) {
            return token
        } else if (host === undefined) {
            // Now try Google oauth tokens previously obtained.  This will return undefined if google oauth is not
            // configured.
            const googleToken = getCurrentGoogleAccessToken();
            if (googleToken && googleToken.expires_at > Date.now()) {
                return googleToken.access_token
            }
        }
    }

    /**
     * Return the content length of the file at the given URL.  This is not guaranteed to succeed, some servers
     * do not support or allow the content-length header.
     *
     * @param url
     * @param options
     * @returns {Promise<unknown>}
     */
    async getContentLength(url, options) {
        if (isFile(url)) {
            return url.size
        } else {
            try {
                options = options || {};
                options.method = 'HEAD';
                options.requestedHeaders = ['content-length'];
                const headerMap = await this._loadURL(url, options);
                const contentLengthString = headerMap['content-length'];
                return contentLengthString ? Number.parseInt(contentLengthString) : 0
            } catch (e) {
                console.error(e);
                return -1
            }
        }
    }

}

function isGoogleStorageSigned(url) {
    return url.indexOf("X-Goog-Signature") > -1
}


/**
 * Return a Google oAuth token, triggering a sign in if required.   This method should not be called until we know
 * a token is required, that is until we've tried the url and received a 401, 403, or 404.
 *
 * @param url
 * @returns the oauth token
 */
async function fetchGoogleAccessToken(url) {
    if (isInitialized()) {
        const scope = getScopeForURL(url);
        const access_token = await getAccessToken(scope);
        return access_token
    } else {
        throw Error(
            `Authorization is required, but Google oAuth has not been initalized. Contact your site administrator for assistance.`)
    }
}

/**
 * Return the current google access token, if one exists.  Do not triger signOn or request additional scopes.
 * @returns {undefined|access_token}
 */
function getCurrentGoogleAccessToken() {
    if (isInitialized()) {
        const access_token = getCurrentAccessToken();
        return access_token
    } else {
        return undefined
    }
}

function addOauthHeaders(headers, acToken) {
    if (acToken) {
        headers["Cache-Control"] = "no-cache";
        headers["Authorization"] = "Bearer " + acToken;
    }
    return headers
}


function addApiKey(url) {
    let apiKey = igvxhr.apiKey;
    if (!apiKey && typeof gapi !== "undefined") {
        apiKey = gapi.apiKey;
    }
    if (apiKey !== undefined && !url.includes("key=")) {
        const paramSeparator = url.includes("?") ? "&" : "?";
        url = url + paramSeparator + "key=" + apiKey;
    }
    return url
}

function addTeamDrive(url) {
    if (url.includes("supportsTeamDrive")) {
        return url
    } else {
        const paramSeparator = url.includes("?") ? "&" : "?";
        url = url + paramSeparator + "supportsTeamDrive=true";
    }
}

/**
 * Perform some well-known url mappings.
 * @param url
 */
function mapUrl(url) {

    if (url.startsWith("https://www.dropbox.com")) {
        return url.replace("//www.dropbox.com", "//dl.dropboxusercontent.com")
    } else if (url.startsWith("https://drive.google.com")) {
        return getDriveDownloadURL(url)
    } else if (url.includes("//www.broadinstitute.org/igvdata")) {
        return url.replace("//www.broadinstitute.org/igvdata", "//data.broadinstitute.org/igvdata")
    } else if (url.includes("//igvdata.broadinstitute.org")) {
        return url.replace("//igvdata.broadinstitute.org", "//s3.amazonaws.com/igv.broadinstitute.org")
    } else if (url.includes("//igv.genepattern.org")) {
        return url.replace("//igv.genepattern.org", "//igv-genepattern-org.s3.amazonaws.com")
    } else if (url.startsWith("ftp://ftp.ncbi.nlm.nih.gov/geo")) {
        return url.replace("ftp://", "https://")
    } else {
        return url
    }
}


function arrayBufferToString(arraybuffer) {

    let plain;
    if (isgzipped(arraybuffer)) {
        plain = ungzip_blocks(arraybuffer);
    } else {
        plain = new Uint8Array(arraybuffer);
    }

    if ('TextDecoder' in getGlobalObject()) {
        return new TextDecoder().decode(plain)
    } else {
        return decodeUTF8(plain)
    }
}

/**
 * Use when TextDecoder is not available (primarily IE).
 *
 * From: https://gist.github.com/Yaffle/5458286
 *
 * @param octets
 * @returns {string}
 */
function decodeUTF8(octets) {
    var string = "";
    var i = 0;
    while (i < octets.length) {
        var octet = octets[i];
        var bytesNeeded = 0;
        var codePoint = 0;
        if (octet <= 0x7F) {
            bytesNeeded = 0;
            codePoint = octet & 0xFF;
        } else if (octet <= 0xDF) {
            bytesNeeded = 1;
            codePoint = octet & 0x1F;
        } else if (octet <= 0xEF) {
            bytesNeeded = 2;
            codePoint = octet & 0x0F;
        } else if (octet <= 0xF4) {
            bytesNeeded = 3;
            codePoint = octet & 0x07;
        }
        if (octets.length - i - bytesNeeded > 0) {
            var k = 0;
            while (k < bytesNeeded) {
                octet = octets[i + k + 1];
                codePoint = (codePoint << 6) | (octet & 0x3F);
                k += 1;
            }
        } else {
            codePoint = 0xFFFD;
            bytesNeeded = octets.length - i;
        }
        string += String.fromCodePoint(codePoint);
        i += bytesNeeded + 1;
    }
    return string
}


function getGlobalObject() {
    if (typeof self !== 'undefined') {
        return self
    }
    if (typeof global !== 'undefined') {
        return global
    } else {
        return window
    }
}


const igvxhr = new IGVXhr();

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/** An implementation of an interval tree, following the explanation.
 * from CLR.
 *
 * Public interface:
 *   Constructor  IntervalTree
 *   Insertion    insert
 *   Search       findOverlapping
 */

var BLACK = 1;

var NIL = {};
NIL.color = BLACK;
NIL.parent = NIL;
NIL.left = NIL;
NIL.right = NIL;

/*
 *  Author: Jim Robinson, 2020
 *
 * Wrapper functions for the Google picker API
 *
 * PREQUISITES
 *    gapi loaded
 *    oauth2 loaded and initialized
 *
 * This wrapper is stateless -- this is important as multiple copies of igv-utils might be present
 * in an application.  All state is held in the gapi library itself.
 */

async function init() {
    return new Promise(function (resolve, reject) {
        gapi.load("picker", {
            callback: resolve,
            onerror: reject
        });
    })
}

async function createDropdownButtonPicker(multipleFileSelection, filePickerHandler) {


    if(typeof gapi === "undefined") {
        throw Error("Google authentication requires the 'gapi' library")
    }

    if(typeof google === "undefined" || !google.picker) {
        await init();
    }

    // We fetch the "drive.readonly" scope because it is neccessary to enable downloading the file
    const access_token = await getAccessToken('https://www.googleapis.com/auth/drive.readonly');
    if (access_token) {

        const view = new google.picker.DocsView(google.picker.ViewId.DOCS);
        view.setIncludeFolders(true);

        const teamView = new google.picker.DocsView(google.picker.ViewId.DOCS);
        teamView.setEnableTeamDrives(true);
        teamView.setIncludeFolders(true);

        let picker;
        if (multipleFileSelection) {
            picker = new google.picker.PickerBuilder()
                .enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
                .setOAuthToken(access_token)
                .addView(view)
                .addView(teamView)
                .enableFeature(google.picker.Feature.SUPPORT_TEAM_DRIVES)
                .setCallback(function (data) {
                    if (data[google.picker.Response.ACTION] === google.picker.Action.PICKED) {
                        filePickerHandler(data[google.picker.Response.DOCUMENTS]);
                    }
                })
                .build();

        } else {
            picker = new google.picker.PickerBuilder()
                .disableFeature(google.picker.Feature.MULTISELECT_ENABLED)
                .setOAuthToken(access_token)
                .addView(view)
                .addView(teamView)
                .enableFeature(google.picker.Feature.SUPPORT_TEAM_DRIVES)
                .setCallback(function (data) {
                    if (data[google.picker.Response.ACTION] === google.picker.Action.PICKED) {
                        filePickerHandler(data[google.picker.Response.DOCUMENTS]);
                    }
                })
                .build();

        }

        picker.setVisible(true);

    } else {
        throw Error("Sign into Google before using picker");
    }
}

function createIcon(name, color) {
    return iconMarkup(name, color)
}

function iconMarkup(name, color) {
    color = color || "currentColor";
    let icon = icons[name];
    if (!icon) {
        console.error(`No icon named: ${name}`);
        icon = icons["question"];
    }

    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttributeNS(null, 'viewBox', '0 0 ' + icon[0] + ' ' + icon[1]);
    const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
    path.setAttributeNS(null, 'fill', color);
    path.setAttributeNS(null, 'd', icon[4]);
    svg.appendChild(path);
    return svg
}

const icons = {
    "check": [512, 512, [], "f00c", "M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"],
    "cog": [512, 512, [], "f013", "M444.788 291.1l42.616 24.599c4.867 2.809 7.126 8.618 5.459 13.985-11.07 35.642-29.97 67.842-54.689 94.586a12.016 12.016 0 0 1-14.832 2.254l-42.584-24.595a191.577 191.577 0 0 1-60.759 35.13v49.182a12.01 12.01 0 0 1-9.377 11.718c-34.956 7.85-72.499 8.256-109.219.007-5.49-1.233-9.403-6.096-9.403-11.723v-49.184a191.555 191.555 0 0 1-60.759-35.13l-42.584 24.595a12.016 12.016 0 0 1-14.832-2.254c-24.718-26.744-43.619-58.944-54.689-94.586-1.667-5.366.592-11.175 5.459-13.985L67.212 291.1a193.48 193.48 0 0 1 0-70.199l-42.616-24.599c-4.867-2.809-7.126-8.618-5.459-13.985 11.07-35.642 29.97-67.842 54.689-94.586a12.016 12.016 0 0 1 14.832-2.254l42.584 24.595a191.577 191.577 0 0 1 60.759-35.13V25.759a12.01 12.01 0 0 1 9.377-11.718c34.956-7.85 72.499-8.256 109.219-.007 5.49 1.233 9.403 6.096 9.403 11.723v49.184a191.555 191.555 0 0 1 60.759 35.13l42.584-24.595a12.016 12.016 0 0 1 14.832 2.254c24.718 26.744 43.619 58.944 54.689 94.586 1.667 5.366-.592 11.175-5.459 13.985L444.788 220.9a193.485 193.485 0 0 1 0 70.2zM336 256c0-44.112-35.888-80-80-80s-80 35.888-80 80 35.888 80 80 80 80-35.888 80-80z"],
    "exclamation": [192, 512, [], "f12a", "M176 432c0 44.112-35.888 80-80 80s-80-35.888-80-80 35.888-80 80-80 80 35.888 80 80zM25.26 25.199l13.6 272C39.499 309.972 50.041 320 62.83 320h66.34c12.789 0 23.331-10.028 23.97-22.801l13.6-272C167.425 11.49 156.496 0 142.77 0H49.23C35.504 0 24.575 11.49 25.26 25.199z"],
    "exclamation-circle": [512, 512, [], "f06a", "M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"],
    "exclamation-triangle": [576, 512, [], "f071", "M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"],
    "minus": [448, 512, [], "f068", "M424 318.2c13.3 0 24-10.7 24-24v-76.4c0-13.3-10.7-24-24-24H24c-13.3 0-24 10.7-24 24v76.4c0 13.3 10.7 24 24 24h400z"],
    "minus-circle": [512, 512, [], "f056", "M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zM124 296c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h264c6.6 0 12 5.4 12 12v56c0 6.6-5.4 12-12 12H124z"],
    "minus-square": [448, 512, [], "f146", "M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zM92 296c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h264c6.6 0 12 5.4 12 12v56c0 6.6-5.4 12-12 12H92z"],
    "plus": [448, 512, [], "f067", "M448 294.2v-76.4c0-13.3-10.7-24-24-24H286.2V56c0-13.3-10.7-24-24-24h-76.4c-13.3 0-24 10.7-24 24v137.8H24c-13.3 0-24 10.7-24 24v76.4c0 13.3 10.7 24 24 24h137.8V456c0 13.3 10.7 24 24 24h76.4c13.3 0 24-10.7 24-24V318.2H424c13.3 0 24-10.7 24-24z"],
    "plus-circle": [512, 512, [], "f055", "M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm144 276c0 6.6-5.4 12-12 12h-92v92c0 6.6-5.4 12-12 12h-56c-6.6 0-12-5.4-12-12v-92h-92c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h92v-92c0-6.6 5.4-12 12-12h56c6.6 0 12 5.4 12 12v92h92c6.6 0 12 5.4 12 12v56z"],
    "plus-square": [448, 512, [], "f0fe", "M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-32 252c0 6.6-5.4 12-12 12h-92v92c0 6.6-5.4 12-12 12h-56c-6.6 0-12-5.4-12-12v-92H92c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h92v-92c0-6.6 5.4-12 12-12h56c6.6 0 12 5.4 12 12v92h92c6.6 0 12 5.4 12 12v56z"],
    "question": [384, 512, [], "f128", "M202.021 0C122.202 0 70.503 32.703 29.914 91.026c-7.363 10.58-5.093 25.086 5.178 32.874l43.138 32.709c10.373 7.865 25.132 6.026 33.253-4.148 25.049-31.381 43.63-49.449 82.757-49.449 30.764 0 68.816 19.799 68.816 49.631 0 22.552-18.617 34.134-48.993 51.164-35.423 19.86-82.299 44.576-82.299 106.405V320c0 13.255 10.745 24 24 24h72.471c13.255 0 24-10.745 24-24v-5.773c0-42.86 125.268-44.645 125.268-160.627C377.504 66.256 286.902 0 202.021 0zM192 373.459c-38.196 0-69.271 31.075-69.271 69.271 0 38.195 31.075 69.27 69.271 69.27s69.271-31.075 69.271-69.271-31.075-69.27-69.271-69.27z"],
    "save": [448, 512, [], "f0c7", "M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z"],
    "search": [512, 512, [], "f002", "M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"],
    "share": [512, 512, [], "f064", "M503.691 189.836L327.687 37.851C312.281 24.546 288 35.347 288 56.015v80.053C127.371 137.907 0 170.1 0 322.326c0 61.441 39.581 122.309 83.333 154.132 13.653 9.931 33.111-2.533 28.077-18.631C66.066 312.814 132.917 274.316 288 272.085V360c0 20.7 24.3 31.453 39.687 18.164l176.004-152c11.071-9.562 11.086-26.753 0-36.328z"],
    "spinner": [512, 512, [], "f110", "M304 48c0 26.51-21.49 48-48 48s-48-21.49-48-48 21.49-48 48-48 48 21.49 48 48zm-48 368c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zm208-208c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zM96 256c0-26.51-21.49-48-48-48S0 229.49 0 256s21.49 48 48 48 48-21.49 48-48zm12.922 99.078c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.491-48-48-48zm294.156 0c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.49-48-48-48zM108.922 60.922c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.491-48-48-48z"],
    "square": [448, 512, [], "f0c8", "M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z"],
    "square-full": [512, 512, [], "f45c", "M512 512H0V0h512v512z"],
    "times": [384, 512, [], "f00d", "M323.1 441l53.9-53.9c9.4-9.4 9.4-24.5 0-33.9L279.8 256l97.2-97.2c9.4-9.4 9.4-24.5 0-33.9L323.1 71c-9.4-9.4-24.5-9.4-33.9 0L192 168.2 94.8 71c-9.4-9.4-24.5-9.4-33.9 0L7 124.9c-9.4 9.4-9.4 24.5 0 33.9l97.2 97.2L7 353.2c-9.4 9.4-9.4 24.5 0 33.9L60.9 441c9.4 9.4 24.5 9.4 33.9 0l97.2-97.2 97.2 97.2c9.3 9.3 24.5 9.3 33.9 0z"],
    "times-circle": [512, 512, [], "f057", "M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm121.6 313.1c4.7 4.7 4.7 12.3 0 17L338 377.6c-4.7 4.7-12.3 4.7-17 0L256 312l-65.1 65.6c-4.7 4.7-12.3 4.7-17 0L134.4 338c-4.7-4.7-4.7-12.3 0-17l65.6-65-65.6-65.1c-4.7-4.7-4.7-12.3 0-17l39.6-39.6c4.7-4.7 12.3-4.7 17 0l65 65.7 65.1-65.6c4.7-4.7 12.3-4.7 17 0l39.6 39.6c4.7 4.7 4.7 12.3 0 17L312 256l65.6 65.1z"],
    "wrench": [512, 512, [], "f0ad", "M481.156 200c9.3 0 15.12 10.155 10.325 18.124C466.295 259.992 420.419 288 368 288c-79.222 0-143.501-63.974-143.997-143.079C223.505 65.469 288.548-.001 368.002 0c52.362.001 98.196 27.949 123.4 69.743C496.24 77.766 490.523 88 481.154 88H376l-40 56 40 56h105.156zm-171.649 93.003L109.255 493.255c-24.994 24.993-65.515 24.994-90.51 0-24.993-24.994-24.993-65.516 0-90.51L218.991 202.5c16.16 41.197 49.303 74.335 90.516 90.503zM104 432c0-13.255-10.745-24-24-24s-24 10.745-24 24 10.745 24 24 24 24-10.745 24-24z"],
};

function attachDialogCloseHandlerWithParent(parent, closeHandler) {

    var container = document.createElement("div");
    parent.appendChild(container);
    container.appendChild(createIcon("times"));
    container.addEventListener('click', function (e) {
        e.preventDefault();
        e.stopPropagation();
        closeHandler();
    });
}

class URLLoadWidget {

    constructor({widgetParent, dataTitle, indexTitle, dataOnly}) {

        dataTitle = dataTitle || 'Data';

        indexTitle = indexTitle || 'Index';

        dataOnly = dataOnly || false;

        // file load widget
        this.container = div$1({class: 'igv-file-load-widget-container'});
        widgetParent.appendChild(this.container);

        const config =
            {
                parent: this.container,
                dataTitle: dataTitle + ' URL',
                indexTitle: indexTitle + ' URL',
                dataOnly
            };


        this.createInputContainer(config);

        // error message container
        this.error_message = div$1({class: 'igv-flw-error-message-container'});
        this.container.appendChild(this.error_message);

        // error message
        this.error_message.appendChild(div$1({class: 'igv-flw-error-message'}));

        // error dismiss button
        attachDialogCloseHandlerWithParent(this.error_message, () => {
            this.dismissErrorMessage();
        });

        this.dismissErrorMessage();

    }

    retrievePaths() {

        const paths = [this.inputData.value];
        if (this.inputIndex && this.inputIndex.value) {
            paths.push(this.inputIndex.value);
        }

        // clear input elements
        this.container.querySelectorAll('.igv-flw-input-row').forEach(div => {
            div.querySelector('input').value = '';
        });

        return paths

    }

    presentErrorMessage(message) {
        this.error_message.querySelector('.igv-flw-error-message').textContent = message;
        show(this.error_message);
    }

    dismissErrorMessage() {
        hide(this.error_message);
        this.error_message.querySelector('.igv-flw-error-message').textContent = '';
    }

    present() {
        show(this.container);
    }

    dismiss() {

        this.dismissErrorMessage();

        // const e = this.container.querySelector('.igv-flw-local-file-name-container');
        // if (e) {
        //     DOMUtils.hide(e);
        // }

        // clear input elements
        this.container.querySelectorAll('.igv-flw-input-row').forEach(div => {
            div.querySelector('input').value = '';
        });


    }

    createInputContainer({parent, dataTitle, indexTitle, dataOnly}) {

        // container
        const container = div$1({class: 'igv-flw-input-container'});
        parent.appendChild(container);

        // data
        const input_data_row = div$1({class: 'igv-flw-input-row'});
        container.appendChild(input_data_row);

        let label;

        // label
        label = div$1({class: 'igv-flw-input-label'});
        input_data_row.appendChild(label);
        label.textContent = dataTitle;

        this.createURLContainer(input_data_row, 'igv-flw-data-url', false);

        if (true === dataOnly) {
            return
        }

        // index
        const input_index_row = div$1({class: 'igv-flw-input-row'});
        container.appendChild(input_index_row);

        // label
        label = div$1({class: 'igv-flw-input-label'});
        input_index_row.appendChild(label);
        label.textContent = indexTitle;

        this.createURLContainer(input_index_row, 'igv-flw-index-url', true);

    }

    createURLContainer(parent, id, isIndexFile) {

        const input = create('input');
        input.setAttribute('type', 'text');
        // input.setAttribute('placeholder', (true === isIndexFile ? 'Enter index URL' : 'Enter data URL'));
        parent.appendChild(input);

        if (isIndexFile) {
            this.inputIndex = input;
        } else {
            this.inputData = input;
        }

    }

}

let didCompleteOneAttempt = false;
let isDropboxEnabled$1 = false;
/**
 * Makes sure that the Dropbox API is loaded and available.  Only one attempt is made to load the API, if it fails
 * the Dropbox buttons are disabled.
 */
async function initializeDropbox() {

    if (true === didCompleteOneAttempt && false === isDropboxEnabled$1) {
        return false
    } else if (true === isDropboxEnabled$1) {
        return true
    } else {
        return new Promise((resolve, reject) => {
            didCompleteOneAttempt = true;

            const dropbox = document.createElement('script');

            // dropbox.setAttribute('src', 'http://localhost:9999');
            const key = igvwebConfig.dropboxAPIKey.startsWith("e43594") ? atob(igvwebConfig.dropboxAPIKey.substring(6)) : igvwebConfig.dropboxAPIKey;
            dropbox.setAttribute('src', 'https://www.dropbox.com/static/api/2/dropins.js');
            dropbox.setAttribute('id', 'dropboxjs');
            dropbox.dataset.appKey = key;
            dropbox.setAttribute('type', "text/javascript");

            document.head.appendChild(dropbox);
            dropbox.addEventListener('load', () => {
                isDropboxEnabled$1 = true;
                resolve(true);
            });

        })
    }
}

/**
 * FileLoad is a base class that handles loading files from local file input, Dropbox, and Google Drive.  Used
 * by SessionFileLoad and GenomeFileLoad.
 */

class FileLoad {

    constructor({localFileInput, dropboxButton, googleDriveButton}) {

        localFileInput.addEventListener('change', async () => {

            if (true === FileLoad.isValidLocalFileInput(localFileInput)) {

                try {
                    await this.loadPaths(Array.from(localFileInput.files));
                } catch (e) {
                    console.error(e);
                    alertSingleton.present(e);
                }
                localFileInput.value = '';
            }

        });

        if (dropboxButton) {
            dropboxButton.addEventListener('click', async () => {

                const result = await initializeDropbox();

                if (true === result) {

                    const config =
                        {
                            success: async dbFiles => {
                                try {
                                    await this.loadPaths(dbFiles.map(dbFile => dbFile.link));
                                } catch (e) {
                                    console.error(e);
                                    alertSingleton.present(e);
                                }
                            },
                            cancel: () => {
                            },
                            linkType: 'preview',
                            multiselect: true,
                            folderselect: false,
                        };

                    Dropbox.choose(config);

                } else {
                    alertSingleton.present('Cannot connect to Dropbox');
                }

            });
        }

        if (googleDriveButton) {
            googleDriveButton.addEventListener('click', () => {
                createDropdownButtonPicker(true, async responses => {

                    try {
                        await this.loadPaths(responses.map(({url}) => url));
                    } catch (e) {
                        console.error(e);
                        alertSingleton.present(e);
                    }
                });
            });

        }

    }

    async loadPaths(paths) {
        //console.log('FileLoad: loadPaths(...)');
    }

    static isValidLocalFileInput(input) {
        return (input.files && input.files.length > 0)
    }

}

class SessionFileLoad extends FileLoad {

    constructor({localFileInput, dropboxButton, googleEnabled, googleDriveButton, loadHandler}) {
        super({localFileInput, dropboxButton, googleEnabled, googleDriveButton});
        this.loadHandler = loadHandler;
    }

    async loadPaths(paths) {

        const path = paths[0];

        try {
            this.loadHandler({url: path});

        } catch (e) {
            throw new Error('Session file did not load' + e.message)
        }
    };

}

function createURLModalElement(id, title) {

    const html =
    `<div id="${id}" class="modal fade" tabindex="-1">

        <div class="modal-dialog modal-lg">
    
            <div class="modal-content">
    
                <div class="modal-header">
                    <div class="modal-title">${title}</div>
    
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
    
                </div>
    
                <div class="modal-body">
                </div>
    
                <div class="modal-footer">
                    <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
                    <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">OK</button>
                </div>
    
            </div>
    
        </div>

    </div>`;

    const fragment = document.createRange().createContextualFragment(html);

    return fragment.firstChild
}

function configureModal(fileLoadWidget, modal, okHandler) {

    const doDismiss = () => {
        fileLoadWidget.dismiss();
        modal.hide();
    };

    const doOK = async () => {

        const result = await okHandler(fileLoadWidget);

        if (true === result) {
            fileLoadWidget.dismiss();
            modal.hide();
        }
    };


    const modalElement = modal._element;

    let dismiss;

    // upper dismiss - x - button
    dismiss = modalElement.querySelector('.modal-header button');
    dismiss.addEventListener('click', doDismiss);

    // lower dismiss - close - button
    dismiss = modalElement.querySelector('.modal-footer button:nth-child(1)');
    dismiss.addEventListener('click', doDismiss);

    // ok - button
    const ok = modalElement.querySelector('.modal-footer button:nth-child(2)');

    ok.addEventListener('click', doOK);

    modalElement.addEventListener('keypress', event => {
        if ('Enter' === event.key) {
            doOK();
        }
    });
}

let fileLoadWidget;
let sessionWidgetModal;

async function createSessionWidgets(rootContainer, browser, options) {

    const prefix = 'igv-webapp';
    const localFileInputId = 'igv-app-dropdown-local-session-file-input';
    const dropboxButtonId = 'igv-app-dropdown-dropbox-session-file-button';
    const googleDriveButtonId = 'igv-app-dropdown-google-drive-session-file-button';
    const urlModalId = 'igv-app-session-url-modal';
    const sessionSaveModalId = 'igv-app-session-save-modal';

    const urlModalElement = createURLModalElement(urlModalId, 'Session URL');
    rootContainer.appendChild(urlModalElement);

    const loadHandler = async config => {
        try {
            await browser.loadSession(config);
        } catch (e) {
            console.error(e);
            alertSingleton.present(e);
        }
    };

    const localFileInput = document.getElementById(`${localFileInputId}`);
    const dropboxButton = document.getElementById(`${dropboxButtonId}`);
    const googleDriveButton = document.getElementById(`${googleDriveButtonId}`);

    const sessionFileLoad = new SessionFileLoad({localFileInput, dropboxButton, googleDriveButton, loadHandler});

    sessionWidgetModal = new bootstrap.Modal(urlModalElement);


    const fileLoadWidgetConfig =
        {
            widgetParent: urlModalElement.querySelector('.modal-body'),
            dataTitle: 'Session',
            indexTitle: undefined,
            dataOnly: true
        };

    fileLoadWidget = new URLLoadWidget(fileLoadWidgetConfig);

    configureModal(fileLoadWidget, sessionWidgetModal, async fileLoadWidget => {
        await sessionFileLoad.loadPaths(fileLoadWidget.retrievePaths());
        return true
    });

    const JSONProvider = () => {
        try {
            return browser.toJSON()
        } catch (e) {
            console.error(e);
            alertSingleton.present(e);
            return undefined
        }
    };
    configureSaveSessionModal(rootContainer, prefix, JSONProvider, sessionSaveModalId);

    if (options.sessionRegistryFile) {
        try {
            const sessionJson = await igvxhr.loadJson(options.sessionRegistryFile);
            createSessionMenu('igv-session-list-divider', sessionJson, loadHandler);
        } catch (e) {
            console.error(e);
            document.getElementById('igv-session-list-divider').style.display = 'none';
        }
    } else {
        document.getElementById('igv-session-list-divider').style.display = 'none';
    }

}

function configureSaveSessionModal(rootContainer, prefix, JSONProvider, sessionSaveModalId) {

    const html =
        `<div id="${sessionSaveModalId}" class="modal fade igv-app-file-save-modal" tabindex="-1">

        <div class="modal-dialog modal-lg">
    
            <div class="modal-content">
    
                <div class="modal-header">
    
                    <div class="modal-title">
                        <div>
                            Save Session File
                        </div>
                    </div>
    
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
    
                <div class="modal-body">
                    <input class="form-control" type="text" placeholder="igv-app-session.json">
    
                    <div>
                        Enter session filename with .json suffix
                    </div>
    
                </div>
    
                <div class="modal-footer">
                    <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
                    <button type="button" class="btn btn-sm btn-secondary">OK</button>
                </div>
    
            </div>
    
        </div>
    
    </div>`;


    const fragment = document.createRange().createContextualFragment(html);
    const modalElement = fragment.firstChild;

    rootContainer.appendChild(modalElement);

    const modal = new bootstrap.Modal(modalElement);

    const inputElement = modalElement.querySelector('input');
    const $input = $(inputElement);

    modalElement.addEventListener('show.bs.modal', () => $input.val(`${prefix}-session.json`));

    const okHandler = () => {

        const extensions = new Set(['json', 'xml']);

        let filename = $input.val();

        if (undefined === filename || '' === filename) {
            filename = $input.attr('placeholder');
        } else if (false === extensions.has(getExtension(filename))) {
            filename = filename + '.json';
        }

        const json = JSONProvider();

        if (json) {
            const jsonString = JSON.stringify(json, null, '\t');
            const data = URL.createObjectURL(new Blob([jsonString], {type: "application/octet-stream"}));
            download(filename, data);
        }

        modal.hide();
    };

    const okElement = modalElement.querySelector('.modal-footer button:nth-child(2)');
    okElement.addEventListener('click', () => okHandler());

    inputElement.addEventListener('keyup', e => {
        // enter key
        if (13 === e.keyCode) {
            okHandler();
        }
    });

    return modal
}

function createSessionMenu(sessionListDivider, sessionJSON, sessionLoader) {

    const id_prefix = 'session_file';

    const searchString = `[id^=${id_prefix}]`;
    const elements = document.querySelectorAll(searchString);
    if (elements.length > 0) {
        for (let i = 0; i < elements.length; i++) {
            elements[i].remove();
        }
    }

    const sessions = sessionJSON['sessions'];

    for (let {label, url, description} of sessions.reverse()) {

        const referenceNode = document.getElementById(sessionListDivider);

        if (!url) {

            const section_id = `${id_prefix}_${guid()}`;
            const html = `<div id="${section_id}" class="dropdown-header">${label}</div>`;
            const fragment = document.createRange().createContextualFragment(html);
            referenceNode.after(fragment.firstChild);

        } else {
            const button_id = `${id_prefix}_${guid()}`;
            const html = `<button id="${button_id}" class="dropdown-item" type="button" ${description ? `title="${description}"` : ''}>${label}</button>`;
            const fragment = document.createRange().createContextualFragment(html);
            referenceNode.after(fragment.firstChild);
            const button = document.getElementById(button_id);
            button.addEventListener('click', () => {
                const config = {};
                const key = true === isFilePath(url) ? 'file' : 'url';
                config[key] = url;

                sessionLoader(config);

            });
        }
    }
}

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2016-2017 The Regents of the University of California
 * Author: Jim Robinson
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

const getDataWrapper = function (data) {

    if (typeof(data) == 'string' || data instanceof String) {
        return new StringDataWrapper(data);
    } else {
        return new ByteArrayDataWrapper(data);
    }
};


// Data might be a string, or an UInt8Array
var StringDataWrapper = function (string) {
    this.data = string;
    this.ptr = 0;
};

StringDataWrapper.prototype.nextLine = function () {
    //return this.split(/\r\n|\n|\r/gm);
    var start = this.ptr,
        idx = this.data.indexOf('\n', start);

    if (idx > 0) {
        this.ptr = idx + 1;   // Advance pointer for next line
        return idx === start ? undefined : this.data.substring(start, idx).trim();
    }
    else {
        // Last line
        this.ptr = this.data.length;
        return (start >= this.data.length) ? undefined : this.data.substring(start).trim();
    }
};

// For use in applications where whitespace carries meaning
// Returns "" for an empty row (not undefined like nextLine), since this is needed in AED
StringDataWrapper.prototype.nextLineNoTrim = function () {
    var start = this.ptr,
        idx = this.data.indexOf('\n', start),
        data = this.data;

    if (idx > 0) {
        this.ptr = idx + 1;   // Advance pointer for next line
        if (idx > start && data.charAt(idx - 1) === '\r') {
            // Trim CR manually in CR/LF sequence
            return data.substring(start, idx - 1);
        }
        return data.substring(start, idx);
    }
    else {
        var length = data.length;
        this.ptr = length;
        // Return undefined only at the very end of the data
        return (start >= length) ? undefined : data.substring(start);
    }
};

var ByteArrayDataWrapper = function (array) {
    this.data = array;
    this.length = this.data.length;
    this.ptr = 0;
};

ByteArrayDataWrapper.prototype.nextLine = function () {

    var c, result;
    result = "";

    if (this.ptr >= this.length) return undefined;

    for (var i = this.ptr; i < this.length; i++) {
        c = String.fromCharCode(this.data[i]);
        if (c === '\r') continue;
        if (c === '\n') break;
        result = result + c;
    }

    this.ptr = i + 1;
    return result;
};

// The ByteArrayDataWrapper does not do any trimming by default, can reuse the function
ByteArrayDataWrapper.prototype.nextLineNoTrim = ByteArrayDataWrapper.prototype.nextLine;

// Default string loader implementation

var stringLoader = {
    loadString: async (path, options) => {
            options = options || {};
            if (path instanceof File) {
                const blob = options.range ? path.slice(options.range.start, options.range.start + options.range.size) : path;
                return blob.text()
            } else {
                const response = await fetch(path);
                return response.text()
            }
    },

    loadJson: async function (url, options) {
        const result = await this.loadString(url, options);
        if (result) {
            return JSON.parse(result)
        } else {
            return result
        }
    }
};

const delimiters = new Set(['\t', ',']);

class GenericDataSource {

    constructor(config) {

        this.stringLoader = config.igvxhr ? config.igvxhr : stringLoader;
        this.columns = config.columns;   // Required for now, could default to all columns
        this.columnDefs = config.columnDefs;       // optional
        this.rowHandler = config.rowHandler;// optional

        this.delimiter = undefined;
        if (config.delimiter) {
            this.delimiter = config.delimiter;
        }

        if (config.data) {
            this.data = config.data;  // Explcitly set table rows as array of json objects
        } else {
            this.url = config.url;     // URL to data source -- required
            this.isJSON = config.isJSON || false;   // optional, defaults to false (tab delimited)
            this.parser = config.parser;                   // optional
            this.filter = config.filter;             // optional
            this.sort = config.sort;                // optional
        }
    }

    async tableColumns() {
        return this.columns
    }

    async tableData() {

        if (undefined === this.data) {

            let str = undefined;
            try {
                str = await this.stringLoader.loadString(this.url);
            } catch (e) {
                console.error(e);
                return undefined
            }

            if (str) {

                let records;
                if (this.parser) {
                    records = this.parser.parse(str);
                } else if (this.isJSON) {
                    records = JSON.parse(str);
                    if (typeof this.filter === 'function') {
                        records = records.filter(this.filter);
                    }
                } else {
                    records = this.parseTabData(str, this.filter);
                }

                if (typeof this.sort === 'function') {
                    records.sort(this.sort);
                }

                // this.data = records
                return records
            }
        } else if (Array.isArray(this.data)) {
            return this.data
        } else if ('json' === GenericDataSource.getExtension(this.data) || delimiters.has(getDelimiter(this.data, this.delimiter))) {

            const extension = GenericDataSource.getExtension(this.data);
            const delimiter = getDelimiter(this.data, this.delimiter);

            let result;
            try {
                result = 'json' === extension ? await this.stringLoader.loadJson(this.data) : await this.stringLoader.loadString(this.data);
            } catch (e) {
                console.error(e);
                return undefined
            }

            if (result) {

                if ('json' === extension) {
                    return result
                } else if (delimiter) {

                    switch (delimiter) {
                        case '\t'   :
                            return this.parseTabData(result)
                        case ','    :
                            return parseCSV(result)
                    }
                }

            }

        }

        return undefined
    }

    parseTabData(str, filter) {

        const dataWrapper = getDataWrapper(str);

        const headerLine = dataWrapper.nextLine();  // Skip header
        const headers = headerLine.split('\t').map(key => key.trim());

        const records = [];
        let line;

        while (line = dataWrapper.nextLine()) {

            const record = {};

            const tokens = line.split(`\t`).map(key => key.trim());
            if (tokens.length !== headers.length) {
                throw Error("Number of values must equal number of headers in file " + this.url)
            }

            for (let i = 0; i < headers.length; i++) {
                record[headers[i]] = tokens[i];
            }

            if (undefined === filter || filter(record)) {
                records.push(record);
            }

        } // while(line)

        return records
    }

    static getExtension(url) {

        const path = (url instanceof File) ? url.name : url;

        // Strip parameters (handles Dropbox URLs)
        let filename = path.toLowerCase();

        let index;

        index = filename.indexOf('?');
        if (index > 0) {
            filename = filename.substr(0, index);
        }

        index = filename.lastIndexOf(".");
        return index < 0 ? filename : filename.substr(1 + index)
    }

}

function getDelimiter(data, delimiter) {
    return delimiter || getDelimiterForExtension(GenericDataSource.getExtension(data))

}

function getDelimiterForExtension(extension) {
    switch (extension) {
        case 'tab' :
            return '\t'
        case 'csv' :
            return ','
        default:
            return undefined
    }
}

function parseCSV(str) {

    const list = str.split('\n');
    const keys = list.shift().split(',').map(key => key.trim());

    return list.map(line => {
        const keyValues = line.split(',').map((value, index) => [keys[index], value.trim()]);
        return Object.fromEntries(new Map(keyValues))
    })

}

class ModalTable {

    constructor(args) {

        this.datasource = args.datasource;
        this.okHandler = args.okHandler;

        this.pageLength = args.pageLength || 10;

        if (args.selectionStyle) {
            this.select = {style: args.selectionStyle};
        } else {
            this.select = true;
        }

        const modelElement = createModalElement$1((args.title || ''), args.id);
        const parent = args.parent || document.querySelector('body');
        parent.appendChild(modelElement);
        this.modalElement = modelElement;

        this.modal = new bootstrap.Modal(modelElement);
        this.datatableContainer = document.getElementById(`${args.id}-datatable-container`);
        this.spinner = document.getElementById(`${args.id}-spinner`);

        modelElement.addEventListener('shown.bs.modal', e => this.buildTable());

        const modalFooter = modelElement.querySelector('.modal-footer');
        const okButton = modalFooter.querySelector('button:nth-child(2)');
        okButton.addEventListener('click', () => {
            const selected = this.getSelectedTableRowsData.call(this, this.$dataTable.$('tr.selected'));
            if (selected && this.okHandler) {
                this.okHandler(selected);
            }
        });

    }

    setTitle(title) {
        const el = this.modalElement.querySelector('.modal-title');
        el.innerText = `${ title }`;
    }

    setDescription(description) {
        // this.modalElement.querySelector('.modal-body').lastElementChild.innerHTML = `${description}`
        this.modalElement.querySelector('.modal-body').firstElementChild.innerHTML = `${description}`;
    }

    remove() {
        this.modalElement.parentNode.removeChild(this.modalElement);
    }

    setDatasource(datasource) {
        this.datasource = datasource;
        this.datatableContainer.innerHTML = '';
        this.$table = undefined;
    }

    async buildTable() {

        if (!this.$table && this.datasource) {

            this.$table = $('<table class="display"></table>');
            this.datatableContainer.appendChild(this.$table.get(0));

            try {
                this.startSpinner();

                const tableData = await this.datasource.tableData();
                const tableColumns = await this.datasource.tableColumns();
                const columnDefs = this.datasource.columnDefs;
                const config =
                    {
                        data: tableData,
                        columns: tableColumns.map(c => {
                            if (columnDefs && columnDefs[c]) {
                                return Object.assign({}, columnDefs[c], {data: c});
                            } else {
                                return {title: c, data: c}
                            }
                        }),
                        pageLength: this.pageLength,
                        select: this.select,
                        autoWidth: false,
                        paging: true,
                        scrollX: true,
                        scrollY: '400px',
                    };


                // API object
                this.api = this.$table.DataTable(config);

                // Preserve sort order. For some reason it gets garbled by default
                // this.api.column( 0 ).data().sort().draw();

                // Adjust column widths
                this.api.columns.adjust().draw();

                // jQuery object
                this.$dataTable = this.$table.dataTable();

                this.tableData = tableData;


            } catch (e) {

            } finally {
                this.stopSpinner();
            }
        }
    }

    getSelectedTableRowsData($rows) {
        const tableData = this.tableData;
        const result = [];
        if ($rows.length > 0) {
            $rows.removeClass('selected');
            const api = this.$table.api();
            $rows.each(function () {
                const index = api.row(this).index();
                result.push(tableData[index]);
            });
            if (typeof this.datasource.rowHandler === 'function') {

                const config = result.map(row => {
                    const thang = this.datasource.rowHandler(row);
                    const filteredKeys = Object.keys(row).filter(key => this.datasource.columns.includes(key));
                    thang.metadata = {};
                    for (let key of filteredKeys) {
                        thang.metadata[ key ] = row[ key ];
                    }
                    return thang
                });

                return config
            } else {
                return result;
            }
        } else {
            return undefined;
        }
    }

    startSpinner() {
        if (this.spinner) this.spinner.style.display = 'block';
    }

    stopSpinner() {
        if (this.spinner) this.spinner.style.display = 'none';
    }

}

function createModalElement$1(title, id) {

    const html =
    `<div id="${id}" class="modal fade" tabindex="-1">
    
        <div class="modal-dialog modal-xl">
    
            <div class="modal-content">
    
                <div class="modal-header">
                    <div class="modal-title">${title}</div>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
    
                <div class="modal-body">
                    <!-- description -->
                    <div style="font-size: 0.9rem; padding-bottom: 0.75rem">
                    </div>

                    <div id="${id}-spinner" class="spinner-border" style="display: none;">
                        <!-- spinner -->
                    </div>
    
                    <div id="${id}-datatable-container">
    
                    </div>
                    
                </div>
    
                <div class="modal-footer">
                    <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
                    <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">OK</button>
                </div>
    
            </div>
    
        </div>
    
    </div>`;

    const fragment = document.createRange().createContextualFragment(html);

    return fragment.firstChild

}

/**
 * Factory function to create a configuration object for the EncodeTrackDatasource given a genomicId and type
 *
 * @param genomeId
 * @param type - 'signals' | 'other
 * @returns {{genomeId: *, selectionHandler: (function(*): *|Uint8Array|BigInt64Array|{color, name, url}[]|Float64Array|Int8Array|Float32Array|Int32Array|Uint32Array|Uint8ClampedArray|BigUint64Array|Int16Array|Uint16Array), hiddenColumns: [string, string, string], addIndexColumn: boolean, parser: undefined, isJSON: boolean, urlPrefix: string, columns: string[], dataSetPath: undefined, titles: {AssayType: string, BioRep: string, OutputType: string, TechRep: string}, suffix: *, dataSetPathPrefix: string}}
 */
function encodeTrackDatasourceConfigurator(genomeId, type) {

    const root = 'https://raw.githubusercontent.com/igvteam/igv-data/refs/heads/main/data/encode/';
    let url;

    switch (type) {
        case 'signals-chip':
            url = `${root}${canonicalId(genomeId)}.signals.chip.txt.gz`;
            break
        case 'signals-other':
            url = `${root}${canonicalId(genomeId)}.signals.other.txt.gz`;
            break
        case 'other':
            url = `${root}${canonicalId(genomeId)}.other.txt.gz`;
            break

    }

    return {
        igvxhr: igvxhr,     // Neccessary to handle gzip files
        isJSON: false,
        url,
        sort: encodeSort,
        columns:
            [
                //'ID',           // hide
                //'Assembly',     // hide
                'Biosample',
                'AssayType',
                'Target',
                'BioRep',
                'TechRep',
                'OutputType',
                'Format',
                'Lab',
                //'HREF',         // hide
                'Accession',
                'Experiment'
            ],
        columnDefs:
            {
                AssayType: {title: 'Assay Type'},
                OutputType: {title: 'Output Type'},
                BioRep: {title: 'Bio Rep'},
                TechRep: {title: 'Tech Rep'}
            },

        rowHandler: row => {
            const name = constructName(row);
            const url = `https://www.encodeproject.org${row['HREF']}`;
            const color = colorForTarget(row['Target']);
            return {name, url, color}
        }

    }
}


function supportsGenome(genomeId) {
    const knownGenomes = new Set(["ce10", "ce11", "dm3", "dm6", "GRCh38", "hg19", "mm9", "mm10"]);
    const id = canonicalId(genomeId);
    return knownGenomes.has(id)
}


function canonicalId(genomeId) {

    switch (genomeId) {
        case "hg38":
        case "hg38_1kg":
            return "GRCh38"
        case "CRCh37":
            return "hg19"
        case "GRCm38":
            return "mm10"
        case "NCBI37":
            return "mm9"
        case "WBcel235":
            return "ce11"
        case "WS220":
            return "ce10"
        default:
            return genomeId
    }
}

function constructName(record) {

    let name = record["Biosample"] || "";

    if (record["Target"]) {
        name += " " + record["Target"];
    }
    if (record["AssayType"].toLowerCase() !== "chip-seq") {
        name += " " + record["AssayType"];
    }


    return name

}


function encodeSort(a, b) {
    var aa1,
        aa2,
        cc1,
        cc2,
        tt1,
        tt2;

    aa1 = a['Assay Type'];
    aa2 = b['Assay Type'];
    cc1 = a['Biosample'];
    cc2 = b['Biosample'];
    tt1 = a['Target'];
    tt2 = b['Target'];

    if (aa1 === aa2) {
        if (cc1 === cc2) {
            if (tt1 === tt2) {
                return 0
            } else if (tt1 < tt2) {
                return -1
            } else {
                return 1
            }
        } else if (cc1 < cc2) {
            return -1
        } else {
            return 1
        }
    } else {
        if (aa1 < aa2) {
            return -1
        } else {
            return 1
        }
    }
}

function colorForTarget(target) {

    const t = target.toLowerCase();
    if (t.startsWith("h3k4")) {
        return "rgb(0,150,0)"
    } else if (t.startsWith("h3k27")) {
        return "rgb(200,0,0)"
    } else if (t.startsWith("h3k36")) {
        return "rgb(0,0,150)"
    } else if (t.startsWith("h3k9")) {
        return "rgb(100,0,0)"
    } else if (t === "ctcf") {
        return "black"
    } else {
        return undefined
    }
}

const createTrackURLModalElement = id => {

    const html =
        `<div id="${id}" class="modal fade" tabindex="-1">

        <div class="modal-dialog modal-lg">
    
            <div class="modal-content">
    
                <div class="modal-header">
                    <div class="modal-title">Track URL</div>
    
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
    
                </div>
    
                <div class="modal-body">
                </div>
    
                <div class="modal-footer">
                    <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
                    <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">OK</button>
                </div>
    
            </div>
    
        </div>

    </div>`;

    const fragment = document.createRange().createContextualFragment(html);

    return fragment.firstChild

};

class MultipleTrackLoadHelper {

    constructor(fileLoadHandler) {

        this.fileLoadHandler = fileLoadHandler;

    }


    async loadPaths(paths) {

        const fileLoadHandler = this.fileLoadHandler;

        try {
            // Search for index files  (.bai, .csi, .tbi, .idx)
            const indexLUT = new Map();

            const dataPaths = [];
            for (let path of paths) {

                const name = await MultipleTrackLoadHelper.getFilename(path);
                const extension = getExtension(name);

                if (indexExtensions.has(extension)) {

                    // key is the data file name
                    const key = createIndexLUTKey(name, extension);
                    indexLUT.set(key, {
                        indexURL: path,
                        indexFilename: MultipleTrackLoadHelper.isGoogleDrivePath(path) ? name : undefined
                    });
                } else {
                    dataPaths.push(path);
                }

            }

            const configurations = [];

            for (let dataPath of dataPaths) {

                const filename = await MultipleTrackLoadHelper.getFilename(dataPath);

                if (indexLUT.has(filename)) {

                    const {indexURL, indexFilename} = indexLUT.get(filename);
                    configurations.push({
                        url: dataPath,
                        filename,
                        indexURL,
                        indexFilename,
                        name: filename,
                        _derivedName: true
                    });

                } else if (requireIndex.has(getExtension(filename))) {
                    throw new Error(`Unable to load track file ${filename} - you must select both ${filename} and its corresponding index file`)
                } else {
                    configurations.push({url: dataPath, filename, name: filename, _derivedName: true});
                }

            }

            if (configurations) {
                fileLoadHandler(configurations);
            }

        } catch (e) {
            console.error(e);
            alertSingleton.present(e.message);
        }

    }

    static async getFilename(path) {

        if (path instanceof File) {
            return path.name
        } else if (isGoogleDriveURL(path)) {
            const info = await getDriveFileInfo(path);
            return info.name || info.originalFileName
        } else {
            const result = parseUri(path);
            return result.file
        }

    }

    static isGoogleDrivePath(path) {
        return path instanceof File ? false : isGoogleDriveURL(path)
    }

}


const indexExtensions = new Set(['bai', 'csi', 'tbi', 'idx', 'crai', 'fai']);

const requireIndex = new Set(['bam', 'cram', 'fa', 'fasta']);

const createIndexLUTKey = (name, extension) => {

    let key = name.substring(0, name.length - (extension.length + 1));

    // bam and cram files (.bai, .crai) have 2 conventions:
    // <data>.bam.bai
    // <data>.bai - we will support this one

    if ('bai' === extension && !key.endsWith('bam')) {
        return `${key}.bam`
    } else if ('crai' === extension && !key.endsWith('cram')) {
        return `${key}.cram`
    } else {
        return key
    }

};

function createTrackSelectionModal({id, label = '', sections, description = '', okHandler}) {
    const html = `
  <div id="${id}" class="modal fade igv-app-track-select-modal" tabindex="-1">
    <div class="modal-dialog modal-lg">
      <div class="modal-content">
        <div class="modal-header flex-column position-sticky top-0 bg-white z-index-1">
          <h5 class="modal-title text-center w-100">${label}</h5>
          <div class="additional-html w-100">${description}</div>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        </div>
        <div class="modal-body overflow-auto" style="max-height: 70vh;">
          ${renderSections(sections, 0)}
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
          <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">OK</button>
        </div>
      </div>
    </div>
  </div>
`;

    function renderSections(sections, level) {
        return sections.map((section, index) => {
            //console.log(`Rendering section: ${section.label}, level: ${level}`);
            return `
    <div class="mb-3">
    
      ${level === 0 ? `
      <div class="bg-light text-center py-2 d-flex justify-content-between align-items-center" role="button" data-bs-toggle="collapse" data-bs-target="#collapseSection${index}" aria-expanded="true" aria-controls="collapseSection${index}">
        <span style="font-size: 1.2rem;">${section.label}</span>
        <span id="collapseIcon${index}" class="bi bi-dash"></span>
      </div>` : ''}        

      ${level === 0 ?
                `<div class="collapse show mt-3" id="collapseSection${index}">` :
                `<fieldset class="border rounded-3 p-3">
          <legend class="form-check-label float-none w-auto px-3" style="font-size: 1.0rem;">
             ${section.label}
          </legend><div>`}
        <form>
          <div class="container">
            <div class="row g-2">
              ${section.tracks ? section.tracks.map((track) => `
                <div class="col-6 col-md-4">
                  <div class="form-check d-flex align-items-center">
                    <input class="form-check-input" type="checkbox" id="${track._id}" ${track._checked ? 'checked' : ''} ${track.disabled ? 'disabled' : ''}>
                    <label class="form-check-label ms-2 me-2" for="${track._id}">
                      ${track.name}
                    </label>
                    ${track.html ? `
                      <a href="${track.html}" target="_blank" style="color: darkblue;">
                        <i class="bi bi-info-circle"></i>
                      </a>` : ''}
                  </div>
                </div>
              `).join('') : ''}
            </div>
            ${section.children ? `<div class="mt-3">${renderSections(section.children, level + 1)}</div>` : ''}
          </div>
        </form>
      ${level === 0 ? `</div>` : '</div></fieldset>'}
    </div>`
        }).join('')
    }

    const tempDiv = document.createElement('div');
    tempDiv.innerHTML = html.trim();
    const modalElement = tempDiv.firstChild;
    document.body.appendChild(modalElement);

    const modal = new bootstrap.Modal(modalElement, {
        backdrop: 'static',
        keyboard: false
    });

    let modalAction = null; // Variable to track the action

    // Add event listeners to buttons
    document.querySelector(`#${id} .btn-secondary`).addEventListener('click', () => {
        modalAction = 'ok';
    });
    document.querySelector(`#${id} .btn-outline-secondary`).addEventListener('click', () => {
        modalAction = 'cancel';
    });

    const trackMap = new Map();
    const fillTrackMap = (sections) => {
        sections.forEach(section => {
            if (section.tracks) {
                section.tracks.forEach(track => {
                    trackMap.set(track._id, track);
                });
            }
            if (section.children) {
                fillTrackMap(section.children);
            }
        });
    };
    fillTrackMap(sections);


    // Listen for the modal close event
    modalElement.addEventListener('hidden.bs.modal', () => {
        if (modalAction === 'ok') {
            const checkedCheckboxes = Array.from(modalElement.querySelectorAll('input[type="checkbox"]:checked'));
            const checkedTracks =
                checkedCheckboxes
                    .map(checkbox => {
                        const trackConfig = trackMap.get(checkbox.id);
                        return trackConfig
                    });
            const uncheckedCheckboxes = Array.from(modalElement.querySelectorAll('input[type="checkbox"]:not(:checked)'));
            const uncheckedTracks =
                uncheckedCheckboxes
                    .map(checkbox => {
                        const trackConfig = trackMap.get(checkbox.id);
                        return trackConfig
                    });

            if (okHandler) {
                okHandler(checkedTracks, uncheckedTracks);
            }
        }

        // Clean up resources
        modal.dispose(); // Dispose of the Bootstrap modal instance
        modalElement.remove(); // Remove the modal element from the DOM
        modalAction = null; // Reset the action
    });

    return modal
}

/**
 * Helper functions and objects for supporting the "Tracks" menu.  Exports 2 functions:
 *
 * - createTrackWidgets(igvMain, browser, config) - creates the widgets and adds them to the igvMain element
 * - trackMenuGenomeChange(browser, genome) - updates the track menu from genome change events
 */

const id_prefix = 'genome_specific_';

let encodeModalTables = [];
let customModalTables = [];
let trackLoadHandler;

const encodeTrackModalIds = ['igv-app-encode-signals-chip-modal', 'igv-app-encode-signals-other-modal', 'igv-app-encode-others-modal'];

let trackRegistry;

async function createTrackWidgets(igvMain, browser, config) {

    if (!trackRegistry && config.trackRegistryFile) {
        try {
            trackRegistry = await igvxhr.loadJson(config.trackRegistryFile);
        } catch (e) {
            alertSingleton.present(`Error retrieving track registry file: ${config.trackRegistryFile}`);
        }
    }

    trackLoadHandler = async configurations => {
        try {
            const trackConfigs = configurations.filter(c => c.format !== 'sampleinfo');
            const sampleInfoConfigs = configurations.filter(c => c.format === 'sampleinfo');
            await Promise.all([
                browser.loadTrackList(trackConfigs),
                ...sampleInfoConfigs.map(config => browser.loadSampleInfo(config))
            ]);
        } catch (e) {
            console.error(e);
            alertSingleton.present(e);
        }
    };
    const trackLoadHelper = new MultipleTrackLoadHelper(trackLoadHandler);

    // Local files
    const localFileInput = document.getElementById('igv-app-dropdown-local-track-file-input');
    localFileInput.addEventListener('change', async () => {
        if (localFileInput.files && localFileInput.files.length > 0) {
            const {files} = localFileInput;
            const paths = Array.from(files);
            localFileInput.value = '';
            await trackLoadHelper.loadPaths(paths);
        }
    });

    // Load from URL
    const urlModalId = 'igv-app-track-from-url-modal';
    const urlModalElement = createTrackURLModalElement(urlModalId);
    igvMain.appendChild(urlModalElement);

    const urlLoadModal = new bootstrap.Modal(urlModalElement);

    const urlLoadWidget = new URLLoadWidget({
        widgetParent: urlModalElement.querySelector('.modal-body'),
        dataTitle: 'Track',
        indexTitle: 'Index',
        dataOnly: false
    });

    configureModal(urlLoadWidget, urlLoadModal, async urlLoadWidget => {
        const paths = urlLoadWidget.retrievePaths();
        await trackLoadHelper.loadPaths(paths);
        return true
    });


    // Dropbox
    const dropboxButton = document.getElementById('igv-app-dropdown-dropbox-track-file-button');
    if (dropboxButton) {
        dropboxButton.addEventListener('click', async () => {

            const result = await initializeDropbox();

            if (true === result) {
                const obj =
                    {
                        success: dbFiles => trackLoadHelper.loadPaths(dbFiles.map(({link}) => link)),
                        cancel: () => {
                        },
                        linkType: "preview",
                        multiselect: true,
                        folderselect: false,
                    };

                Dropbox.choose(obj);

            } else {
                alertSingleton.present('Cannot connect to Dropbox');
            }
        });
    }

    // Google Drive
    const googleDriveButton = document.getElementById('igv-app-dropdown-google-drive-track-file-button');
    if (googleDriveButton) {
        googleDriveButton.addEventListener('click', () => {
            createDropdownButtonPicker(true,
                async responses => await trackLoadHelper.loadPaths(responses.map(({name, url}) => url)));
        });
    }


    // Prepare ENCODE modal tables -- these are resued for all genomes that support ENCODE
    for (let modalID of encodeTrackModalIds) {
        const encodeModalTableConfig =
            {
                id: modalID,
                title: 'ENCODE',
                selectionStyle: 'multi',
                pageLength: 100,
                okHandler: trackLoadHandler
            };
        encodeModalTables.push(new ModalTable(encodeModalTableConfig));
    }

}

async function trackMenuGenomeChange(browser, genome) {

    // Remove existing items
    const $dropdownMenu = $('#igv-app-track-dropdown-menu');
    discardTrackMenuItems($dropdownMenu);
    customModalTables.forEach(modalTable => modalTable.remove());
    customModalTables = [];

    const genomeID = genome.id;

    const $divider = $dropdownMenu.find('#igv-app-annotations-section');

    if (true === supportsGenome(genomeID)) {
        addEncodeButtons(genomeID, $divider);
    }


    const trackMenuConfigurations = trackRegistry ? await getTrackMenuConfigurationsFromRegistry(genome.id) : [];

    const genomeHubURLs = genome.getHubURLs() || [];
    const configs = [];

    // Process hubs one at a time to prevent  a single failure from blocking others
    if (genomeHubURLs) {
        for (const url of genomeHubURLs) {
            try {
                configs.push(await prepHubConfig(url, genomeID));
            } catch (e) {
                console.error(`Error loading hub configuration from URL: ${url}`, e);
            }
        }
    }

    if (trackMenuConfigurations) {
        configs.push(...trackMenuConfigurations);
    }


    for (let config of configs.reverse()) {

        if ('---' === config) {
            // Add a separator
            const divider = document.createElement('div');
            divider.className = 'dropdown-divider';
            $divider[0].parentNode.insertBefore(divider, $divider[0].nextSibling);

        } else if (config.customModalTable) {

            // Custom data table modal
            createDropdownButton($divider, config.label, id_prefix)
                .on('click', () => config.customModalTable.modal.show());

        } else {

            // Track selection modal
            createDropdownButton($divider, config.label, id_prefix)

                .on('click', () => {

                    // Collect url-name pairs for loaded tracks with urls.  This will serve as unique IDs to compare with track configs
                    const loadedIDs = browser ? new Set(browser.findTracks(t => t.url).map(t => `${t.url}-${t.name}`)) : new Set();

                    // Annotate track config objects with a unique ID comprised of url + name
                    const annotateTracks = (section) => {
                        if (section.tracks) {
                            for (const track of section.tracks) {
                                track._id = `${track.url}-${track.name}`;
                                track._checked = loadedIDs.has(track._id);
                            }
                        }
                        if (section.children) for (const child of section.children) {
                            annotateTracks(child);
                        }
                    };
                    for (const group of config.sections) {
                        annotateTracks(group);
                    }

                    config.okHandler = (checkedTracks, uncheckedTracks) => {

                        // Remove tracks that are unchecked
                        const uncheckedIDs = new Set(uncheckedTracks.map(s => s._id));
                        const toUnload = new Set(Array.from(loadedIDs).filter(id => uncheckedIDs.has(id)));
                        browser.findTracks(track => toUnload.has(`${track.url}-${track.name}`))
                            .forEach(track => browser.removeTrack(track));

                        const trackConfigs = checkedTracks.filter(config => !loadedIDs.has(`${config.url}-${config.name}`));
                        if (trackConfigs.length > 0) {
                            try {
                                trackLoadHandler(trackConfigs);
                            } catch (e) {
                                console.error(e);
                                alertSingleton.present(e);
                            }
                        }
                    };
                    config.cancelHandler = () => {
                        modal.hide();
                    };

                    const modal = createTrackSelectionModal(config);
                    modal.show();
                });
        }
    }

}

function prepRegistryConfig(registry) {

    if ('custom-data-modal' === registry.type) {
        const customModalTable = new ModalTable({
            id: `igv-custom-modal-${Math.random().toString(36).substring(2, 9)}`,
            title: registry.label,
            okHandler: trackLoadHandler,
            selectionStyle: 'multi',
            pageLength: 100,
            datasource: new GenericDataSource(registry),
            description: registry.description
        });
        customModalTables.push(customModalTable);
        registry.customModalTable = customModalTable;
        return registry
    } else {
        return {
            id: `_${Math.random().toString(36).substring(2, 9)}`,
            label: registry.label,
            description: registry.description,
            sections: [{
                label: registry.label,
                tracks: registry.tracks
            }],
        }
    }
}


async function prepHubConfig(hubURL, genomeID) {
    const hub = await index.loadHub(hubURL);
    let descriptionUrl = hub.getDescriptionUrl();
    const groups = await hub.getGroupedTrackConfigurations(genomeID);
    return {
        id: `_${Math.random().toString(36).substring(2, 9)}`,
        label: `${hub.getShortLabel()}`,
        description: descriptionUrl ? `<a target=_blank href="${descriptionUrl}">${hub.getLongLabel()}</a>` : '',
        sections: groups
    }
}


function addEncodeButtons(genomeID, $divider) {

    encodeModalTables[0].setDatasource(new GenericDataSource(encodeTrackDatasourceConfigurator(genomeID, 'signals-chip')));
    encodeModalTables[1].setDatasource(new GenericDataSource(encodeTrackDatasourceConfigurator(genomeID, 'signals-other')));
    encodeModalTables[2].setDatasource(new GenericDataSource(encodeTrackDatasourceConfigurator(genomeID, 'other')));

    const description = "<a href=https://www.encodeproject.org/ target=_blank>Encylopedia of Genomic Elements</a>";
    encodeModalTables[0].setDescription(description);
    encodeModalTables[1].setDescription(description);
    encodeModalTables[2].setDescription(description);

    createDropdownButton($divider, 'ENCODE Other', id_prefix)
        .on('click', () => encodeModalTables[2].modal.show());

    createDropdownButton($divider, 'ENCODE Signals - Other', id_prefix)
        .on('click', () => encodeModalTables[1].modal.show());

    createDropdownButton($divider, 'ENCODE Signals - ChIP', id_prefix)
        .on('click', () => encodeModalTables[0].modal.show());
}


/**
 * Called upon a genome change.
 *
 * @param genomeID
 * @param trackConfigurations
 * @param $dropdownMenu
 * @param trackLoadHandler
 * @returns {Promise<void>}
 */

function createDropdownButton($divider, buttonText, id_prefix) {
    const $button = $('<button>', {class: 'dropdown-item', type: 'button'});
    $button.text(`${buttonText} ...`);
    $button.attr('id', `${id_prefix}${buttonText.toLowerCase().split(' ').join('_')}`);
    $button.insertAfter($divider);
    return $button
}


async function getTrackMenuConfigurationsFromRegistry(genomeID) {

    const configs = [];
    if (trackRegistry) {
        const paths = trackRegistry[genomeID];
        if (paths) {
            // Load track configurations, which can be either JSON files or hub URLs, one at a time to prevent a single failure from blocking others
            for (let path of paths) {
                try {
                    if (path === '---') {
                        configs.push('---');
                    } else if (path.endsWith('.json')) {
                        configs.push(prepRegistryConfig(await igvxhr.loadJson(path)));
                    } else {
                        configs.push(await prepHubConfig(path, genomeID));
                    }
                } catch (e) {
                    console.error(`Error loading track configuration from registry: ${path}`, e);
                }
            }
        }
    }

    // No registry or no paths
    return configs
}


function discardTrackMenuItems($dropdownMenu) {

    $dropdownMenu.find('.dropdown-divider');
    const searchString = '[id^=' + id_prefix + ']';
    const $found = $dropdownMenu.find(searchString);
    $found.remove();

}

const dropboxButtonImageLiteral =
    `<svg width="75px" height="64px" viewBox="0 0 75 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
        <title>Shape</title>
        <desc>Created with Sketch.</desc>
        <defs></defs>
        <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
            <g id="dropbox" fill="#0061FF" fill-rule="nonzero">
                <path d="M37.6,12 L18.8,24 L37.6,36 L18.8,48 L1.42108547e-14,35.9 L18.8,23.9 L1.42108547e-14,12 L18.8,0 L37.6,12 Z M18.7,51.8 L37.5,39.8 L56.3,51.8 L37.5,63.8 L18.7,51.8 Z M37.6,35.9 L56.4,23.9 L37.6,12 L56.3,0 L75.1,12 L56.3,24 L75.1,36 L56.3,48 L37.6,35.9 Z" id="Shape"></path>
            </g>
        </g>
    </svg>`;

const googleDriveImageLiteral =
    `<svg viewBox="0 0 139 120.4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><radialGradient id="a" cx="-254.81979" cy="705.83588" gradientTransform="matrix(2.827 1.6322 -1.6322 2.827 2092.1199 -1494.5786)" gradientUnits="userSpaceOnUse" r="82.978401"><stop offset="0" stop-color="#4387fd"/><stop offset=".65" stop-color="#3078f0"/><stop offset=".9099" stop-color="#2b72ea"/><stop offset="1" stop-color="#286ee6"/></radialGradient><radialGradient id="b" cx="-254.8174" cy="705.83691" gradientTransform="matrix(2.827 1.6322 -1.6322 2.827 2092.1199 -1494.5786)" gradientUnits="userSpaceOnUse" r="82.973"><stop offset="0" stop-color="#ffd24d"/><stop offset="1" stop-color="#f6c338"/></radialGradient><path d="m24.2 120.4-24.2-41.9 45.3-78.5 24.2 41.9z" fill="#0da960"/><path d="m24.2 120.4 24.2-41.9h90.6l-24.2 41.9z" fill="url(#a)"/><path d="m139 78.5h-48.4l-45.3-78.5h48.4z" fill="url(#b)"/><path d="m69.5 78.5h-21.1l10.5-18.3-34.7 60.2z" fill="#2d6fdd"/><path d="m90.6 78.5h48.4l-58.9-18.3z" fill="#e5b93c"/><path d="m58.9 60.2 10.6-18.3-24.2-41.9z" fill="#0c9b57"/></svg>`;

const dropboxButtonImageBase64 = () => window.btoa(dropboxButtonImageLiteral);

const googleDriveButtonImageBase64 = () => window.btoa(googleDriveImageLiteral);

const dropboxDropdownItem = id => {

    return `<div class="dropdown-item">
                <div id="${id}" class="igv-app-dropdown-item-cloud-storage">
                    <div>Dropbox File</div>
                    <div>
                        <img src="data:image/svg+xml;base64,${dropboxButtonImageBase64()}" width="18" height="18">
                    </div>
                </div>
            </div>`
};

const googleDriveDropdownItem = id => {

    return `<div class="dropdown-item">
                <div id="${id}" class="igv-app-dropdown-item-cloud-storage">
                    <div>Google Drive File</div>
                    <div>
                        <img src="data:image/svg+xml;base64,${googleDriveButtonImageBase64()}" width="18" height="18">
                    </div>
                </div>
            </div>`
};

const Globals = {};

// accession	assembly	scientific name	common name	taxonId	GenArk clade

const genarkColumns =
    [
        'accession',
        'assembly',
        'scientificName',
        'commonName',
        'taxonId',
        'genArkClade',

    ];

const genarkParser =
    {
        parse: string => {
            const lines = string.split('\n');

            // columns
            // const headerLines = lines.filter(line => line.startsWith('#'))
            // const columns = headerLines.pop().split('\t')
            // const cooked = columns.shift().split('').filter(char => (/[a-zA-Z]/).test(char)).join('')
            // columns.unshift(cooked)

            // records
            let dataLines = lines.filter(line => !line.startsWith('#')).map(line => line.split(`\t`));
            dataLines = dataLines.filter(tokens => 6 === tokens.length);

            const records = [];
            for (const tokens of dataLines) {
                const record = {};
                for (let i = 0; i < tokens.length; i++) {
                    record[genarkColumns[i]] = tokens[i];
                }

                records.push(record);
            }

            return records
        }
    };


function genarkDatasourceConfigurator() {

    const url = 'https://hgdownload.soe.ucsc.edu/hubs/UCSC_GI.assemblyHubList.txt';

    return {
        isJSON: false,
        url,
        columns: genarkColumns,
        columnDefs:
            {
                accession: {title: 'Accession'},
                assembly: {title: 'Assembly'},
                scientificName: {title: 'Scientific Name'},
                commonName: {title: 'Common Name'},
                genArkClade: {title: 'GenArk clade'},
            },

        parser: genarkParser

    }
}

class GenomeFileLoad extends FileLoad {

    constructor({localFileInput, dropboxButton, googleDriveButton, loadHandler}) {
        super({localFileInput, dropboxButton, googleDriveButton});
        this.loadHandler = loadHandler;
    }

    /**
     * Load a reference genome, which can be  is a JSON, hub.txt, .gbk, .2bit, or a fasta file with an index.  The
     * function takes an array of paths, which be either url or file paths.  With the exception of the fasta file
     * the paths array should only contain one element.  The fasta file can be a single file, or a pair of files.
     *
     * @param paths
     * @returns {Promise<void>}
     */
    async loadPaths(paths) {

        let configuration = undefined;

        if(paths.length == 0) {
            return
        }

        const filenames = [];
        for(let p of paths) {
            const filename = await MultipleTrackLoadHelper.getFilename(p);
            filenames.push(filename);
        }
        
        const path1 = paths[0];
        if(1 === paths.length) {
            const extension = getExtension(path1);
            if ('json' === extension) {
                configuration = await igvxhr.loadJson(path1);
            } else if (filenames[0].endsWith("hub.txt") ) {
                configuration = {url: path1};
            } else if ('gbk' === extension) {
                configuration = {gbkURL: path1};
            }
            //else if ('2bit' === extension) {
            //    configuration = {twoBitURL: paths[0]}
            //}
            else {
                // Assume this is a fasta file.  There is no standard extension, and no way to really know for sure.
                // If we can determine the file size and it is not too large relax requirement for an index file
                let fileSize = await igvxhr.getContentLength(path1);
                if (fileSize > 0 && fileSize < 10000000) {
                    configuration = {fastaURL: path1};
                } else {
                    if(confirm("Missing index file.  This fasta is large, or its size unknown, and loading without an index file may be slow or freeze the browser.  Proceed?")) {
                        configuration = {fastaURL: path1};
                    } else {
                        return
                    }
                }
            }
        } else {   // 2 paths entered, assume they are a fasta and index file
            const path2 = paths[1];

            if (await this.isGzipped(filenames[0]) || await this.isGzipped(filenames[1])) {
                throw new Error('Genome did not load - Gzipped fasta files with indexes are not supported')
            }
            const [ext1, ext2] = filenames.map(file => getExtension(file));
            if ('fai' === ext1) {
                configuration = {fastaURL: path2, indexURL: path1};
            } else if ('fai' === ext2) {
                configuration = {fastaURL: path1, indexURL: path2};
            } else {
                throw new Error('Genome did not load - did not detect index file (expected extension .fai)')
            }
        }

        if (undefined === configuration) {
            throw new Error('Genome requires either a single JSON, UCSC hub.txt, genbank ".gbk", or a FASTA & index file')
        } else {
            this.loadHandler(configuration);
        }
    }

    async isGzipped(name) {
        return name.endsWith(".gz")
    }

}

/**
 * Helper functions and objects for the genome dropdown menu.  Exports a single function:
 *
 * -  createGenomeWidgets(igvMain, browser, genomes)
 */

const MAX_CUSTOM_GENOMES = 5;

let predefinedGenomeIds;
let predefinedGenomes;
let genarkModalTable;
let genomeWidgetModal;
let predefinedGenomesMap = new Map();

async function createGenomeWidgets(igvMain, browser, genomes) {

    const urlModalId = 'igv-app-genome-from-url-modal';
    const genarkModalId = 'igv-app-genome-genark-modal';

    const genarkModalTableConfig =
        {
            id: genarkModalId,
            title: 'UCSC GenArk',
            selectionStyle: 'single',
            pageLength: 100,
            okHandler: result => {
                const {accession} = result[0];
                loadGenome({genarkAccession: accession});
            }
        };
    genarkModalTable = new ModalTable(genarkModalTableConfig);

    const dataSource = new GenericDataSource(genarkDatasourceConfigurator());
    genarkModalTable.setDatasource(dataSource);

    // URL modal
    const urlModalElement = createURLModalElement(urlModalId, 'Genome URL');
    igvMain.appendChild(urlModalElement);

    // File widget
    const fileLoadWidget = new URLLoadWidget({
        widgetParent: urlModalElement.querySelector('.modal-body'),
        dataTitle: 'Genome',
        indexTitle: 'Index',
        dataOnly: false
    });

    const genomeFileLoad = new GenomeFileLoad(
        {
            localFileInput: document.getElementById('igv-app-dropdown-local-genome-file-input'),
            dropboxButton: document.getElementById('igv-app-dropdown-dropbox-genome-file-button'),
            googleDriveButton: document.getElementById('igv-app-dropdown-google-drive-genome-file-button'),
            loadHandler: async configuration => {
                if (configuration.id !== browser.genome.id) {
                    await loadGenome(configuration);
                }
            }
        });

    // Configures both file widget and url modal, a bit confusing
    genomeWidgetModal = new bootstrap.Modal(urlModalElement);
    configureModal(fileLoadWidget, genomeWidgetModal, async fileLoadWidget => {

        try {
            await genomeFileLoad.loadPaths(fileLoadWidget.retrievePaths());
        } catch (e) {
            console.error(e);
            alertSingleton.present(e);
        }

    });

    initializeGenomeWidgets(genomes);
}

/**
 * Initialize the genome selection widget with pre-defined and user-defined genomes.  Because of the way these
 * items are added in 'genomeDropdownLayout' they are added in reverse order.
 *
 * @param genomes
 * @returns {Promise<void>}
 */
async function initializeGenomeWidgets(genomes) {
    try {
        predefinedGenomes = genomes.reverse(); // Default genome list
        predefinedGenomeIds = new Set(predefinedGenomes.map(g => g.id));
        for(const genomeJson of genomes) {
            if (genomeJson.id) {
                predefinedGenomesMap.set(genomeJson.id, genomeJson);
            }
        }
        updateGenomeList();

    } catch (e) {
        alertSingleton.present(e.message);
    }
}

function getRecentGenomes() {
    const customGenomeString = localStorage.getItem("recentGenomes");
    return customGenomeString ? JSON.parse(customGenomeString).reverse() : []
}


function updateGenomeList() {

    const dropdownMenu = document.getElementById('igv-app-genome-dropdown-menu');

    // NOTE:  MUST USE ID HERE, THERE CAN BE MULTIPLE DIVIDERS.
    const divider = dropdownMenu.querySelector('#igv-app-genome-dropdown-divider');

    // discard all buttons following the divider div
    let sibling = divider.nextElementSibling;
    while (sibling) {
        const nextSibling = sibling.nextElementSibling;
        sibling.remove();
        sibling = nextSibling;
    }

    const addEntryFor = (genomeJson) => {
        const key = genomeJson.id;
        let value = genomeJson;

        const button = document.createElement('button');
        button.className = 'dropdown-item';
        button.type = 'button';
        button.textContent = value.name;

        divider.insertAdjacentElement('afterend', button);

        button.dataset.id = key;
        button.addEventListener('click', async () => {
            const id = button.dataset.id;
            if (id !== Globals.browser.genome.id) {
                if(predefinedGenomesMap.has(id)) {
                    value = predefinedGenomesMap.get(id);
                }
                await loadGenome(value);
            }
        });
    };

    // TODO -- why do we need to add everthing in reverse?

    if (predefinedGenomes && predefinedGenomes.length > 0) {
        for (let genomeJson of predefinedGenomes) {
            addEntryFor(genomeJson);
        }
    }

    const recentGenomes = getRecentGenomes();
    if (recentGenomes && recentGenomes.length > 0) {
        const divider2 = document.createElement('div');
        divider2.className = 'dropdown-divider';
        divider.insertAdjacentElement('afterend', divider2);
        for (let genomeJson of recentGenomes) {
            addEntryFor(genomeJson);
        }
    }
}


async function loadGenome(genomeConfiguration) {

    let g = undefined;
    try {
        g = await Globals.browser.loadGenome(genomeConfiguration);
        if (g.id) {
            try {

                // Last loaded genome ID, reloaded automatically on next page load
                localStorage.setItem("genomeID", g.id);

                // Update the recent genomes list
                if (isString(genomeConfiguration)) {
                    genomeConfiguration = {id: genomeConfiguration};
                } else {
                    if (!genomeConfiguration.id) genomeConfiguration.id = g.id;
                }
                if (!genomeConfiguration.name) {
                    genomeConfiguration.name = g.name;
                }


                const recentGenomeList = localStorage.getItem("recentGenomes");
                let recentGenomes = recentGenomeList ? JSON.parse(recentGenomeList) : [];
                recentGenomes = recentGenomes.filter(r => r.id !== g.id);  // If already present, replace
                recentGenomes.unshift(genomeConfiguration);
                if (recentGenomes.length > MAX_CUSTOM_GENOMES) {
                    recentGenomes = recentGenomes.slice(0, MAX_CUSTOM_GENOMES);
                }
                localStorage.setItem("recentGenomes", JSON.stringify(recentGenomes));
                updateGenomeList();


            } catch (e) {
                console.error(e);
            }
        }

    } catch (e) {
        console.error(e);
        alertSingleton.present(e);
    }

}

/**
 * @fileoverview
 * - Using the 'QRCode for Javascript library'
 * - Fixed dataset of 'QRCode for Javascript library' for support full-spec.
 * - this library has no dependencies.
 *
 * @author davidshimjs
 * @see <a href="http://www.d-project.com/" target="_blank">http://www.d-project.com/</a>
 * @see <a href="http://jeromeetienne.github.com/jquery-qrcode/" target="_blank">http://jeromeetienne.github.com/jquery-qrcode/</a>
 */


//---------------------------------------------------------------------
// QRCode for JavaScript
//
// Copyright (c) 2009 Kazuhiko Arase
//
// URL: http://www.d-project.com/
//
// Licensed under the MIT license:
//   http://www.opensource.org/licenses/mit-license.php
//
// The word "QR Code" is registered trademark of
// DENSO WAVE INCORPORATED
//   http://www.denso-wave.com/qrcode/faqpatent-e.html
//
//---------------------------------------------------------------------
function QR8bitByte(data) {
    this.mode = QRMode.MODE_8BIT_BYTE;
    this.data = data;
    this.parsedData = [];

    // Added to support UTF-8 Characters
    for (var i = 0, l = this.data.length; i < l; i++) {
        var byteArray = [];
        var code = this.data.charCodeAt(i);

        if (code > 0x10000) {
            byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18);
            byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12);
            byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6);
            byteArray[3] = 0x80 | (code & 0x3F);
        } else if (code > 0x800) {
            byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12);
            byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6);
            byteArray[2] = 0x80 | (code & 0x3F);
        } else if (code > 0x80) {
            byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6);
            byteArray[1] = 0x80 | (code & 0x3F);
        } else {
            byteArray[0] = code;
        }

        this.parsedData.push(byteArray);
    }

    this.parsedData = Array.prototype.concat.apply([], this.parsedData);

    if (this.parsedData.length != this.data.length) {
        this.parsedData.unshift(191);
        this.parsedData.unshift(187);
        this.parsedData.unshift(239);
    }
}

QR8bitByte.prototype = {
    getLength: function (buffer) {
        return this.parsedData.length
    },
    write: function (buffer) {
        for (var i = 0, l = this.parsedData.length; i < l; i++) {
            buffer.put(this.parsedData[i], 8);
        }
    }
};

function QRCodeModel(typeNumber, errorCorrectLevel) {
    this.typeNumber = typeNumber;
    this.errorCorrectLevel = errorCorrectLevel;
    this.modules = null;
    this.moduleCount = 0;
    this.dataCache = null;
    this.dataList = [];
}

QRCodeModel.prototype = {
    addData: function (data) {
        var newData = new QR8bitByte(data);
        this.dataList.push(newData);
        this.dataCache = null;
    }, isDark: function (row, col) {
        if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) {
            throw new Error(row + "," + col)
        }
        return this.modules[row][col]
    }, getModuleCount: function () {
        return this.moduleCount
    }, make: function () {
        this.makeImpl(false, this.getBestMaskPattern());
    }, makeImpl: function (test, maskPattern) {
        this.moduleCount = this.typeNumber * 4 + 17;
        this.modules = new Array(this.moduleCount);
        for (var row = 0; row < this.moduleCount; row++) {
            this.modules[row] = new Array(this.moduleCount);
            for (var col = 0; col < this.moduleCount; col++) {
                this.modules[row][col] = null;
            }
        }
        this.setupPositionProbePattern(0, 0);
        this.setupPositionProbePattern(this.moduleCount - 7, 0);
        this.setupPositionProbePattern(0, this.moduleCount - 7);
        this.setupPositionAdjustPattern();
        this.setupTimingPattern();
        this.setupTypeInfo(test, maskPattern);
        if (this.typeNumber >= 7) {
            this.setupTypeNumber(test);
        }
        if (this.dataCache == null) {
            this.dataCache = QRCodeModel.createData(this.typeNumber, this.errorCorrectLevel, this.dataList);
        }
        this.mapData(this.dataCache, maskPattern);
    }, setupPositionProbePattern: function (row, col) {
        for (var r = -1; r <= 7; r++) {
            if (row + r <= -1 || this.moduleCount <= row + r) continue
            for (var c = -1; c <= 7; c++) {
                if (col + c <= -1 || this.moduleCount <= col + c) continue
                if ((0 <= r && r <= 6 && (c == 0 || c == 6)) || (0 <= c && c <= 6 && (r == 0 || r == 6)) || (2 <= r && r <= 4 && 2 <= c && c <= 4)) {
                    this.modules[row + r][col + c] = true;
                } else {
                    this.modules[row + r][col + c] = false;
                }
            }
        }
    }, getBestMaskPattern: function () {
        var minLostPoint = 0;
        var pattern = 0;
        for (var i = 0; i < 8; i++) {
            this.makeImpl(true, i);
            var lostPoint = QRUtil.getLostPoint(this);
            if (i == 0 || minLostPoint > lostPoint) {
                minLostPoint = lostPoint;
                pattern = i;
            }
        }
        return pattern
    }, createMovieClip: function (target_mc, instance_name, depth) {
        var qr_mc = target_mc.createEmptyMovieClip(instance_name, depth);
        var cs = 1;
        this.make();
        for (var row = 0; row < this.modules.length; row++) {
            var y = row * cs;
            for (var col = 0; col < this.modules[row].length; col++) {
                var x = col * cs;
                var dark = this.modules[row][col];
                if (dark) {
                    qr_mc.beginFill(0, 100);
                    qr_mc.moveTo(x, y);
                    qr_mc.lineTo(x + cs, y);
                    qr_mc.lineTo(x + cs, y + cs);
                    qr_mc.lineTo(x, y + cs);
                    qr_mc.endFill();
                }
            }
        }
        return qr_mc
    }, setupTimingPattern: function () {
        for (var r = 8; r < this.moduleCount - 8; r++) {
            if (this.modules[r][6] != null) {
                continue
            }
            this.modules[r][6] = (r % 2 == 0);
        }
        for (var c = 8; c < this.moduleCount - 8; c++) {
            if (this.modules[6][c] != null) {
                continue
            }
            this.modules[6][c] = (c % 2 == 0);
        }
    }, setupPositionAdjustPattern: function () {
        var pos = QRUtil.getPatternPosition(this.typeNumber);
        for (var i = 0; i < pos.length; i++) {
            for (var j = 0; j < pos.length; j++) {
                var row = pos[i];
                var col = pos[j];
                if (this.modules[row][col] != null) {
                    continue
                }
                for (var r = -2; r <= 2; r++) {
                    for (var c = -2; c <= 2; c++) {
                        if (r == -2 || r == 2 || c == -2 || c == 2 || (r == 0 && c == 0)) {
                            this.modules[row + r][col + c] = true;
                        } else {
                            this.modules[row + r][col + c] = false;
                        }
                    }
                }
            }
        }
    }, setupTypeNumber: function (test) {
        var bits = QRUtil.getBCHTypeNumber(this.typeNumber);
        for (var i = 0; i < 18; i++) {
            var mod = (!test && ((bits >> i) & 1) == 1);
            this.modules[Math.floor(i / 3)][i % 3 + this.moduleCount - 8 - 3] = mod;
        }
        for (var i = 0; i < 18; i++) {
            var mod = (!test && ((bits >> i) & 1) == 1);
            this.modules[i % 3 + this.moduleCount - 8 - 3][Math.floor(i / 3)] = mod;
        }
    }, setupTypeInfo: function (test, maskPattern) {
        var data = (this.errorCorrectLevel << 3) | maskPattern;
        var bits = QRUtil.getBCHTypeInfo(data);
        for (var i = 0; i < 15; i++) {
            var mod = (!test && ((bits >> i) & 1) == 1);
            if (i < 6) {
                this.modules[i][8] = mod;
            } else if (i < 8) {
                this.modules[i + 1][8] = mod;
            } else {
                this.modules[this.moduleCount - 15 + i][8] = mod;
            }
        }
        for (var i = 0; i < 15; i++) {
            var mod = (!test && ((bits >> i) & 1) == 1);
            if (i < 8) {
                this.modules[8][this.moduleCount - i - 1] = mod;
            } else if (i < 9) {
                this.modules[8][15 - i - 1 + 1] = mod;
            } else {
                this.modules[8][15 - i - 1] = mod;
            }
        }
        this.modules[this.moduleCount - 8][8] = (!test);
    }, mapData: function (data, maskPattern) {
        var inc = -1;
        var row = this.moduleCount - 1;
        var bitIndex = 7;
        var byteIndex = 0;
        for (var col = this.moduleCount - 1; col > 0; col -= 2) {
            if (col == 6) col--;
            while (true) {
                for (var c = 0; c < 2; c++) {
                    if (this.modules[row][col - c] == null) {
                        var dark = false;
                        if (byteIndex < data.length) {
                            dark = (((data[byteIndex] >>> bitIndex) & 1) == 1);
                        }
                        var mask = QRUtil.getMask(maskPattern, row, col - c);
                        if (mask) {
                            dark = !dark;
                        }
                        this.modules[row][col - c] = dark;
                        bitIndex--;
                        if (bitIndex == -1) {
                            byteIndex++;
                            bitIndex = 7;
                        }
                    }
                }
                row += inc;
                if (row < 0 || this.moduleCount <= row) {
                    row -= inc;
                    inc = -inc;
                    break
                }
            }
        }
    }
};
QRCodeModel.PAD0 = 0xEC;
QRCodeModel.PAD1 = 0x11;
QRCodeModel.createData = function (typeNumber, errorCorrectLevel, dataList) {
    var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel);
    var buffer = new QRBitBuffer();
    for (var i = 0; i < dataList.length; i++) {
        var data = dataList[i];
        buffer.put(data.mode, 4);
        buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber));
        data.write(buffer);
    }
    var totalDataCount = 0;
    for (var i = 0; i < rsBlocks.length; i++) {
        totalDataCount += rsBlocks[i].dataCount;
    }
    if (buffer.getLengthInBits() > totalDataCount * 8) {
        throw new Error("code length overflow. ("
            + buffer.getLengthInBits()
            + ">"
            + totalDataCount * 8
            + ")")
    }
    if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
        buffer.put(0, 4);
    }
    while (buffer.getLengthInBits() % 8 != 0) {
        buffer.putBit(false);
    }
    while (true) {
        if (buffer.getLengthInBits() >= totalDataCount * 8) {
            break
        }
        buffer.put(QRCodeModel.PAD0, 8);
        if (buffer.getLengthInBits() >= totalDataCount * 8) {
            break
        }
        buffer.put(QRCodeModel.PAD1, 8);
    }
    return QRCodeModel.createBytes(buffer, rsBlocks)
};
QRCodeModel.createBytes = function (buffer, rsBlocks) {
    var offset = 0;
    var maxDcCount = 0;
    var maxEcCount = 0;
    var dcdata = new Array(rsBlocks.length);
    var ecdata = new Array(rsBlocks.length);
    for (var r = 0; r < rsBlocks.length; r++) {
        var dcCount = rsBlocks[r].dataCount;
        var ecCount = rsBlocks[r].totalCount - dcCount;
        maxDcCount = Math.max(maxDcCount, dcCount);
        maxEcCount = Math.max(maxEcCount, ecCount);
        dcdata[r] = new Array(dcCount);
        for (var i = 0; i < dcdata[r].length; i++) {
            dcdata[r][i] = 0xff & buffer.buffer[i + offset];
        }
        offset += dcCount;
        var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
        var rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1);
        var modPoly = rawPoly.mod(rsPoly);
        ecdata[r] = new Array(rsPoly.getLength() - 1);
        for (var i = 0; i < ecdata[r].length; i++) {
            var modIndex = i + modPoly.getLength() - ecdata[r].length;
            ecdata[r][i] = (modIndex >= 0) ? modPoly.get(modIndex) : 0;
        }
    }
    var totalCodeCount = 0;
    for (var i = 0; i < rsBlocks.length; i++) {
        totalCodeCount += rsBlocks[i].totalCount;
    }
    var data = new Array(totalCodeCount);
    var index = 0;
    for (var i = 0; i < maxDcCount; i++) {
        for (var r = 0; r < rsBlocks.length; r++) {
            if (i < dcdata[r].length) {
                data[index++] = dcdata[r][i];
            }
        }
    }
    for (var i = 0; i < maxEcCount; i++) {
        for (var r = 0; r < rsBlocks.length; r++) {
            if (i < ecdata[r].length) {
                data[index++] = ecdata[r][i];
            }
        }
    }
    return data
};
var QRMode = {MODE_NUMBER: 1 << 0, MODE_ALPHA_NUM: 1 << 1, MODE_8BIT_BYTE: 1 << 2, MODE_KANJI: 1 << 3};
var QRErrorCorrectLevel = {L: 1, M: 0, Q: 3, H: 2};
var QRMaskPattern = {
    PATTERN000: 0,
    PATTERN001: 1,
    PATTERN010: 2,
    PATTERN011: 3,
    PATTERN100: 4,
    PATTERN101: 5,
    PATTERN110: 6,
    PATTERN111: 7
};
var QRUtil = {
    PATTERN_POSITION_TABLE: [[], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50], [6, 30, 54], [6, 32, 58], [6, 34, 62], [6, 26, 46, 66], [6, 26, 48, 70], [6, 26, 50, 74], [6, 30, 54, 78], [6, 30, 56, 82], [6, 30, 58, 86], [6, 34, 62, 90], [6, 28, 50, 72, 94], [6, 26, 50, 74, 98], [6, 30, 54, 78, 102], [6, 28, 54, 80, 106], [6, 32, 58, 84, 110], [6, 30, 58, 86, 114], [6, 34, 62, 90, 118], [6, 26, 50, 74, 98, 122], [6, 30, 54, 78, 102, 126], [6, 26, 52, 78, 104, 130], [6, 30, 56, 82, 108, 134], [6, 34, 60, 86, 112, 138], [6, 30, 58, 86, 114, 142], [6, 34, 62, 90, 118, 146], [6, 30, 54, 78, 102, 126, 150], [6, 24, 50, 76, 102, 128, 154], [6, 28, 54, 80, 106, 132, 158], [6, 32, 58, 84, 110, 136, 162], [6, 26, 54, 82, 110, 138, 166], [6, 30, 58, 86, 114, 142, 170]],
    G15: (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0),
    G18: (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0),
    G15_MASK: (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1),
    getBCHTypeInfo: function (data) {
        var d = data << 10;
        while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) {
            d ^= (QRUtil.G15 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15)));
        }
        return ((data << 10) | d) ^ QRUtil.G15_MASK
    },
    getBCHTypeNumber: function (data) {
        var d = data << 12;
        while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) {
            d ^= (QRUtil.G18 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18)));
        }
        return (data << 12) | d
    },
    getBCHDigit: function (data) {
        var digit = 0;
        while (data != 0) {
            digit++;
            data >>>= 1;
        }
        return digit
    },
    getPatternPosition: function (typeNumber) {
        return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1]
    },
    getMask: function (maskPattern, i, j) {
        switch (maskPattern) {
            case QRMaskPattern.PATTERN000:
                return (i + j) % 2 == 0
            case QRMaskPattern.PATTERN001:
                return i % 2 == 0
            case QRMaskPattern.PATTERN010:
                return j % 3 == 0
            case QRMaskPattern.PATTERN011:
                return (i + j) % 3 == 0
            case QRMaskPattern.PATTERN100:
                return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0
            case QRMaskPattern.PATTERN101:
                return (i * j) % 2 + (i * j) % 3 == 0
            case QRMaskPattern.PATTERN110:
                return ((i * j) % 2 + (i * j) % 3) % 2 == 0
            case QRMaskPattern.PATTERN111:
                return ((i * j) % 3 + (i + j) % 2) % 2 == 0
            default:
                throw new Error("bad maskPattern:" + maskPattern)
        }
    },
    getErrorCorrectPolynomial: function (errorCorrectLength) {
        var a = new QRPolynomial([1], 0);
        for (var i = 0; i < errorCorrectLength; i++) {
            a = a.multiply(new QRPolynomial([1, QRMath.gexp(i)], 0));
        }
        return a
    },
    getLengthInBits: function (mode, type) {
        if (1 <= type && type < 10) {
            switch (mode) {
                case QRMode.MODE_NUMBER:
                    return 10
                case QRMode.MODE_ALPHA_NUM:
                    return 9
                case QRMode.MODE_8BIT_BYTE:
                    return 8
                case QRMode.MODE_KANJI:
                    return 8
                default:
                    throw new Error("mode:" + mode)
            }
        } else if (type < 27) {
            switch (mode) {
                case QRMode.MODE_NUMBER:
                    return 12
                case QRMode.MODE_ALPHA_NUM:
                    return 11
                case QRMode.MODE_8BIT_BYTE:
                    return 16
                case QRMode.MODE_KANJI:
                    return 10
                default:
                    throw new Error("mode:" + mode)
            }
        } else if (type < 41) {
            switch (mode) {
                case QRMode.MODE_NUMBER:
                    return 14
                case QRMode.MODE_ALPHA_NUM:
                    return 13
                case QRMode.MODE_8BIT_BYTE:
                    return 16
                case QRMode.MODE_KANJI:
                    return 12
                default:
                    throw new Error("mode:" + mode)
            }
        } else {
            throw new Error("type:" + type)
        }
    },
    getLostPoint: function (qrCode) {
        var moduleCount = qrCode.getModuleCount();
        var lostPoint = 0;
        for (var row = 0; row < moduleCount; row++) {
            for (var col = 0; col < moduleCount; col++) {
                var sameCount = 0;
                var dark = qrCode.isDark(row, col);
                for (var r = -1; r <= 1; r++) {
                    if (row + r < 0 || moduleCount <= row + r) {
                        continue
                    }
                    for (var c = -1; c <= 1; c++) {
                        if (col + c < 0 || moduleCount <= col + c) {
                            continue
                        }
                        if (r == 0 && c == 0) {
                            continue
                        }
                        if (dark == qrCode.isDark(row + r, col + c)) {
                            sameCount++;
                        }
                    }
                }
                if (sameCount > 5) {
                    lostPoint += (3 + sameCount - 5);
                }
            }
        }
        for (var row = 0; row < moduleCount - 1; row++) {
            for (var col = 0; col < moduleCount - 1; col++) {
                var count = 0;
                if (qrCode.isDark(row, col)) count++;
                if (qrCode.isDark(row + 1, col)) count++;
                if (qrCode.isDark(row, col + 1)) count++;
                if (qrCode.isDark(row + 1, col + 1)) count++;
                if (count == 0 || count == 4) {
                    lostPoint += 3;
                }
            }
        }
        for (var row = 0; row < moduleCount; row++) {
            for (var col = 0; col < moduleCount - 6; col++) {
                if (qrCode.isDark(row, col) && !qrCode.isDark(row, col + 1) && qrCode.isDark(row, col + 2) && qrCode.isDark(row, col + 3) && qrCode.isDark(row, col + 4) && !qrCode.isDark(row, col + 5) && qrCode.isDark(row, col + 6)) {
                    lostPoint += 40;
                }
            }
        }
        for (var col = 0; col < moduleCount; col++) {
            for (var row = 0; row < moduleCount - 6; row++) {
                if (qrCode.isDark(row, col) && !qrCode.isDark(row + 1, col) && qrCode.isDark(row + 2, col) && qrCode.isDark(row + 3, col) && qrCode.isDark(row + 4, col) && !qrCode.isDark(row + 5, col) && qrCode.isDark(row + 6, col)) {
                    lostPoint += 40;
                }
            }
        }
        var darkCount = 0;
        for (var col = 0; col < moduleCount; col++) {
            for (var row = 0; row < moduleCount; row++) {
                if (qrCode.isDark(row, col)) {
                    darkCount++;
                }
            }
        }
        var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5;
        lostPoint += ratio * 10;
        return lostPoint
    }
};
var QRMath = {
    glog: function (n) {
        if (n < 1) {
            throw new Error("glog(" + n + ")")
        }
        return QRMath.LOG_TABLE[n]
    }, gexp: function (n) {
        while (n < 0) {
            n += 255;
        }
        while (n >= 256) {
            n -= 255;
        }
        return QRMath.EXP_TABLE[n]
    }, EXP_TABLE: new Array(256), LOG_TABLE: new Array(256)
};
for (var i = 0; i < 8; i++) {
    QRMath.EXP_TABLE[i] = 1 << i;
}
for (var i = 8; i < 256; i++) {
    QRMath.EXP_TABLE[i] = QRMath.EXP_TABLE[i - 4] ^ QRMath.EXP_TABLE[i - 5] ^ QRMath.EXP_TABLE[i - 6] ^ QRMath.EXP_TABLE[i - 8];
}
for (var i = 0; i < 255; i++) {
    QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]] = i;
}

function QRPolynomial(num, shift) {
    if (num.length == undefined) {
        throw new Error(num.length + "/" + shift)
    }
    var offset = 0;
    while (offset < num.length && num[offset] == 0) {
        offset++;
    }
    this.num = new Array(num.length - offset + shift);
    for (var i = 0; i < num.length - offset; i++) {
        this.num[i] = num[i + offset];
    }
}

QRPolynomial.prototype = {
    get: function (index) {
        return this.num[index]
    }, getLength: function () {
        return this.num.length
    }, multiply: function (e) {
        var num = new Array(this.getLength() + e.getLength() - 1);
        for (var i = 0; i < this.getLength(); i++) {
            for (var j = 0; j < e.getLength(); j++) {
                num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i)) + QRMath.glog(e.get(j)));
            }
        }
        return new QRPolynomial(num, 0)
    }, mod: function (e) {
        if (this.getLength() - e.getLength() < 0) {
            return this
        }
        var ratio = QRMath.glog(this.get(0)) - QRMath.glog(e.get(0));
        var num = new Array(this.getLength());
        for (var i = 0; i < this.getLength(); i++) {
            num[i] = this.get(i);
        }
        for (var i = 0; i < e.getLength(); i++) {
            num[i] ^= QRMath.gexp(QRMath.glog(e.get(i)) + ratio);
        }
        return new QRPolynomial(num, 0).mod(e)
    }
};

function QRRSBlock(totalCount, dataCount) {
    this.totalCount = totalCount;
    this.dataCount = dataCount;
}

QRRSBlock.RS_BLOCK_TABLE = [[1, 26, 19], [1, 26, 16], [1, 26, 13], [1, 26, 9], [1, 44, 34], [1, 44, 28], [1, 44, 22], [1, 44, 16], [1, 70, 55], [1, 70, 44], [2, 35, 17], [2, 35, 13], [1, 100, 80], [2, 50, 32], [2, 50, 24], [4, 25, 9], [1, 134, 108], [2, 67, 43], [2, 33, 15, 2, 34, 16], [2, 33, 11, 2, 34, 12], [2, 86, 68], [4, 43, 27], [4, 43, 19], [4, 43, 15], [2, 98, 78], [4, 49, 31], [2, 32, 14, 4, 33, 15], [4, 39, 13, 1, 40, 14], [2, 121, 97], [2, 60, 38, 2, 61, 39], [4, 40, 18, 2, 41, 19], [4, 40, 14, 2, 41, 15], [2, 146, 116], [3, 58, 36, 2, 59, 37], [4, 36, 16, 4, 37, 17], [4, 36, 12, 4, 37, 13], [2, 86, 68, 2, 87, 69], [4, 69, 43, 1, 70, 44], [6, 43, 19, 2, 44, 20], [6, 43, 15, 2, 44, 16], [4, 101, 81], [1, 80, 50, 4, 81, 51], [4, 50, 22, 4, 51, 23], [3, 36, 12, 8, 37, 13], [2, 116, 92, 2, 117, 93], [6, 58, 36, 2, 59, 37], [4, 46, 20, 6, 47, 21], [7, 42, 14, 4, 43, 15], [4, 133, 107], [8, 59, 37, 1, 60, 38], [8, 44, 20, 4, 45, 21], [12, 33, 11, 4, 34, 12], [3, 145, 115, 1, 146, 116], [4, 64, 40, 5, 65, 41], [11, 36, 16, 5, 37, 17], [11, 36, 12, 5, 37, 13], [5, 109, 87, 1, 110, 88], [5, 65, 41, 5, 66, 42], [5, 54, 24, 7, 55, 25], [11, 36, 12], [5, 122, 98, 1, 123, 99], [7, 73, 45, 3, 74, 46], [15, 43, 19, 2, 44, 20], [3, 45, 15, 13, 46, 16], [1, 135, 107, 5, 136, 108], [10, 74, 46, 1, 75, 47], [1, 50, 22, 15, 51, 23], [2, 42, 14, 17, 43, 15], [5, 150, 120, 1, 151, 121], [9, 69, 43, 4, 70, 44], [17, 50, 22, 1, 51, 23], [2, 42, 14, 19, 43, 15], [3, 141, 113, 4, 142, 114], [3, 70, 44, 11, 71, 45], [17, 47, 21, 4, 48, 22], [9, 39, 13, 16, 40, 14], [3, 135, 107, 5, 136, 108], [3, 67, 41, 13, 68, 42], [15, 54, 24, 5, 55, 25], [15, 43, 15, 10, 44, 16], [4, 144, 116, 4, 145, 117], [17, 68, 42], [17, 50, 22, 6, 51, 23], [19, 46, 16, 6, 47, 17], [2, 139, 111, 7, 140, 112], [17, 74, 46], [7, 54, 24, 16, 55, 25], [34, 37, 13], [4, 151, 121, 5, 152, 122], [4, 75, 47, 14, 76, 48], [11, 54, 24, 14, 55, 25], [16, 45, 15, 14, 46, 16], [6, 147, 117, 4, 148, 118], [6, 73, 45, 14, 74, 46], [11, 54, 24, 16, 55, 25], [30, 46, 16, 2, 47, 17], [8, 132, 106, 4, 133, 107], [8, 75, 47, 13, 76, 48], [7, 54, 24, 22, 55, 25], [22, 45, 15, 13, 46, 16], [10, 142, 114, 2, 143, 115], [19, 74, 46, 4, 75, 47], [28, 50, 22, 6, 51, 23], [33, 46, 16, 4, 47, 17], [8, 152, 122, 4, 153, 123], [22, 73, 45, 3, 74, 46], [8, 53, 23, 26, 54, 24], [12, 45, 15, 28, 46, 16], [3, 147, 117, 10, 148, 118], [3, 73, 45, 23, 74, 46], [4, 54, 24, 31, 55, 25], [11, 45, 15, 31, 46, 16], [7, 146, 116, 7, 147, 117], [21, 73, 45, 7, 74, 46], [1, 53, 23, 37, 54, 24], [19, 45, 15, 26, 46, 16], [5, 145, 115, 10, 146, 116], [19, 75, 47, 10, 76, 48], [15, 54, 24, 25, 55, 25], [23, 45, 15, 25, 46, 16], [13, 145, 115, 3, 146, 116], [2, 74, 46, 29, 75, 47], [42, 54, 24, 1, 55, 25], [23, 45, 15, 28, 46, 16], [17, 145, 115], [10, 74, 46, 23, 75, 47], [10, 54, 24, 35, 55, 25], [19, 45, 15, 35, 46, 16], [17, 145, 115, 1, 146, 116], [14, 74, 46, 21, 75, 47], [29, 54, 24, 19, 55, 25], [11, 45, 15, 46, 46, 16], [13, 145, 115, 6, 146, 116], [14, 74, 46, 23, 75, 47], [44, 54, 24, 7, 55, 25], [59, 46, 16, 1, 47, 17], [12, 151, 121, 7, 152, 122], [12, 75, 47, 26, 76, 48], [39, 54, 24, 14, 55, 25], [22, 45, 15, 41, 46, 16], [6, 151, 121, 14, 152, 122], [6, 75, 47, 34, 76, 48], [46, 54, 24, 10, 55, 25], [2, 45, 15, 64, 46, 16], [17, 152, 122, 4, 153, 123], [29, 74, 46, 14, 75, 47], [49, 54, 24, 10, 55, 25], [24, 45, 15, 46, 46, 16], [4, 152, 122, 18, 153, 123], [13, 74, 46, 32, 75, 47], [48, 54, 24, 14, 55, 25], [42, 45, 15, 32, 46, 16], [20, 147, 117, 4, 148, 118], [40, 75, 47, 7, 76, 48], [43, 54, 24, 22, 55, 25], [10, 45, 15, 67, 46, 16], [19, 148, 118, 6, 149, 119], [18, 75, 47, 31, 76, 48], [34, 54, 24, 34, 55, 25], [20, 45, 15, 61, 46, 16]];
QRRSBlock.getRSBlocks = function (typeNumber, errorCorrectLevel) {
    var rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel);
    if (rsBlock == undefined) {
        throw new Error("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel)
    }
    var length = rsBlock.length / 3;
    var list = [];
    for (var i = 0; i < length; i++) {
        var count = rsBlock[i * 3 + 0];
        var totalCount = rsBlock[i * 3 + 1];
        var dataCount = rsBlock[i * 3 + 2];
        for (var j = 0; j < count; j++) {
            list.push(new QRRSBlock(totalCount, dataCount));
        }
    }
    return list
};
QRRSBlock.getRsBlockTable = function (typeNumber, errorCorrectLevel) {
    switch (errorCorrectLevel) {
        case QRErrorCorrectLevel.L:
            return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0]
        case QRErrorCorrectLevel.M:
            return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1]
        case QRErrorCorrectLevel.Q:
            return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2]
        case QRErrorCorrectLevel.H:
            return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3]
        default:
            return undefined
    }
};

function QRBitBuffer() {
    this.buffer = [];
    this.length = 0;
}

QRBitBuffer.prototype = {
    get: function (index) {
        var bufIndex = Math.floor(index / 8);
        return ((this.buffer[bufIndex] >>> (7 - index % 8)) & 1) == 1
    }, put: function (num, length) {
        for (var i = 0; i < length; i++) {
            this.putBit(((num >>> (length - i - 1)) & 1) == 1);
        }
    }, getLengthInBits: function () {
        return this.length
    }, putBit: function (bit) {
        var bufIndex = Math.floor(this.length / 8);
        if (this.buffer.length <= bufIndex) {
            this.buffer.push(0);
        }
        if (bit) {
            this.buffer[bufIndex] |= (0x80 >>> (this.length % 8));
        }
        this.length++;
    }
};
var QRCodeLimitLength = [[17, 14, 11, 7], [32, 26, 20, 14], [53, 42, 32, 24], [78, 62, 46, 34], [106, 84, 60, 44], [134, 106, 74, 58], [154, 122, 86, 64], [192, 152, 108, 84], [230, 180, 130, 98], [271, 213, 151, 119], [321, 251, 177, 137], [367, 287, 203, 155], [425, 331, 241, 177], [458, 362, 258, 194], [520, 412, 292, 220], [586, 450, 322, 250], [644, 504, 364, 280], [718, 560, 394, 310], [792, 624, 442, 338], [858, 666, 482, 382], [929, 711, 509, 403], [1003, 779, 565, 439], [1091, 857, 611, 461], [1171, 911, 661, 511], [1273, 997, 715, 535], [1367, 1059, 751, 593], [1465, 1125, 805, 625], [1528, 1190, 868, 658], [1628, 1264, 908, 698], [1732, 1370, 982, 742], [1840, 1452, 1030, 790], [1952, 1538, 1112, 842], [2068, 1628, 1168, 898], [2188, 1722, 1228, 958], [2303, 1809, 1283, 983], [2431, 1911, 1351, 1051], [2563, 1989, 1423, 1093], [2699, 2099, 1499, 1139], [2809, 2213, 1579, 1219], [2953, 2331, 1663, 1273]];

var useSVG = document.documentElement.tagName.toLowerCase() === "svg";

let Drawing;

if (useSVG) {

    Drawing = function (el, htOption) {
        this._el = el;
        this._htOption = htOption;
    };

    Drawing.prototype.draw = function (oQRCode) {
        var _htOption = this._htOption;
        var _el = this._el;
        var nCount = oQRCode.getModuleCount();
        Math.floor(_htOption.width / nCount);
        Math.floor(_htOption.height / nCount);

        this.clear();

        function makeSVG(tag, attrs) {
            var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
            for (var k in attrs)
                if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]);
            return el
        }

        var svg = makeSVG("svg", {
            'viewBox': '0 0 ' + String(nCount) + " " + String(nCount),
            'width': '100%',
            'height': '100%',
            'fill': _htOption.colorLight
        });
        svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink");
        _el.appendChild(svg);

        svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"}));
        svg.appendChild(makeSVG("rect", {
            "fill": _htOption.colorDark,
            "width": "1",
            "height": "1",
            "id": "template"
        }));

        for (var row = 0; row < nCount; row++) {
            for (var col = 0; col < nCount; col++) {
                if (oQRCode.isDark(row, col)) {
                    var child = makeSVG("use", {"x": String(col), "y": String(row)});
                    child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template");
                    svg.appendChild(child);
                }
            }
        }
    };
    Drawing.prototype.clear = function () {
        while (this._el.hasChildNodes())
            this._el.removeChild(this._el.lastChild);
    };
} else {

    /**
     * Drawing QRCode by using canvas
     *
     * @constructor
     * @param {HTMLElement} el
     * @param {Object} htOption QRCode Options
     */
    Drawing = function (el, htOption) {
        this._bIsPainted = false;
        this._htOption = htOption;
        this._elCanvas = document.createElement("canvas");
        this._elCanvas.width = htOption.width;
        this._elCanvas.height = htOption.height;
        el.appendChild(this._elCanvas);
        this._el = el;
        this._oContext = this._elCanvas.getContext("2d");
        this._bIsPainted = false;
        this._elImage = document.createElement("img");
        this._elImage.alt = "Scan me!";
        this._elImage.style.display = "none";
        this._el.appendChild(this._elImage);
        this._bSupportDataURI = null;
    };

    /**
     * Draw the QRCode
     *
     * @param {QRCode} oQRCode
     */
    Drawing.prototype.draw = function (oQRCode) {
        var _elImage = this._elImage;
        var _oContext = this._oContext;
        var _htOption = this._htOption;

        var nCount = oQRCode.getModuleCount();
        var nWidth = _htOption.width / nCount;
        var nHeight = _htOption.height / nCount;
        var nRoundedWidth = Math.round(nWidth);
        var nRoundedHeight = Math.round(nHeight);

        _elImage.style.display = "none";
        this.clear();

        for (var row = 0; row < nCount; row++) {
            for (var col = 0; col < nCount; col++) {
                var bIsDark = oQRCode.isDark(row, col);
                var nLeft = col * nWidth;
                var nTop = row * nHeight;
                _oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight;
                _oContext.lineWidth = 1;
                _oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight;
                _oContext.fillRect(nLeft, nTop, nWidth, nHeight);

                // 안티 앨리어싱 방지 처리
                _oContext.strokeRect(
                    Math.floor(nLeft) + 0.5,
                    Math.floor(nTop) + 0.5,
                    nRoundedWidth,
                    nRoundedHeight
                );

                _oContext.strokeRect(
                    Math.ceil(nLeft) - 0.5,
                    Math.ceil(nTop) - 0.5,
                    nRoundedWidth,
                    nRoundedHeight
                );
            }
        }

        this._bIsPainted = true;
    };

    /**
     * Make the image from Canvas if the browser supports Data URI.
     */
    Drawing.prototype.makeImage = function () {
        if (this._bIsPainted) {
            this._elImage.src = this._elCanvas.toDataURL("image/png");
            this._elImage.style.display = "block";
            this._elCanvas.style.display = "none";
        }
    };

    /**
     * Return whether the QRCode is painted or not
     *
     * @return {Boolean}
     */
    Drawing.prototype.isPainted = function () {
        return this._bIsPainted
    };

    /**
     * Clear the QRCode
     */
    Drawing.prototype.clear = function () {
        this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height);
        this._bIsPainted = false;
    };

    /**
     * @private
     * @param {Number} nNumber
     */
    Drawing.prototype.round = function (nNumber) {
        if (!nNumber) {
            return nNumber
        }

        return Math.floor(nNumber * 1000) / 1000
    };
}

/**
 * Get the type by string length
 *
 * @private
 * @param {String} sText
 * @param {Number} nCorrectLevel
 * @return {Number} type
 */
function _getTypeNumber(sText, nCorrectLevel) {
    var nType = 1;
    var length = _getUTF8Length(sText);

    for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) {
        var nLimit = 0;

        switch (nCorrectLevel) {
            case QRErrorCorrectLevel.L :
                nLimit = QRCodeLimitLength[i][0];
                break
            case QRErrorCorrectLevel.M :
                nLimit = QRCodeLimitLength[i][1];
                break
            case QRErrorCorrectLevel.Q :
                nLimit = QRCodeLimitLength[i][2];
                break
            case QRErrorCorrectLevel.H :
                nLimit = QRCodeLimitLength[i][3];
                break
        }

        if (length <= nLimit) {
            break
        } else {
            nType++;
        }
    }

    if (nType > QRCodeLimitLength.length) {
        throw new Error("Too long data")
    }

    return nType
}

function _getUTF8Length(sText) {
    var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a');
    return replacedText.length + (replacedText.length != sText ? 3 : 0)
}

/**
 * @class QRCode
 * @constructor
 * @example
 * new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie");
 *
 * @example
 * var oQRCode = new QRCode("test", {
 *    text : "http://naver.com",
 *    width : 128,
 *    height : 128
 * });
 *
 * oQRCode.clear(); // Clear the QRCode.
 * oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode.
 *
 * @param {HTMLElement|String} el target element or 'id' attribute of element.
 * @param {Object|String} vOption
 * @param {String} vOption.text QRCode link data
 * @param {Number} [vOption.width=256]
 * @param {Number} [vOption.height=256]
 * @param {String} [vOption.colorDark="#000000"]
 * @param {String} [vOption.colorLight="#ffffff"]
 * @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H]
 */
const QRCode = function (el, vOption) {
    this._htOption = {
        width: 256,
        height: 256,
        typeNumber: 4,
        colorDark: "#000000",
        colorLight: "#ffffff",
        correctLevel: QRErrorCorrectLevel.H
    };

    if (typeof vOption === 'string') {
        vOption = {
            text: vOption
        };
    }

    // Overwrites options
    if (vOption) {
        for (var i in vOption) {
            this._htOption[i] = vOption[i];
        }
    }

    if (typeof el == "string") {
        el = document.getElementById(el);
    }

    if (this._htOption.useSVG) {
        Drawing = svgDrawer;
    }

    this._el = el;
    this._oQRCode = null;
    this._oDrawing = new Drawing(this._el, this._htOption);

    if (this._htOption.text) {
        this.makeCode(this._htOption.text);
    }
};

/**
 * Make the QRCode
 *
 * @param {String} sText link data
 */
QRCode.prototype.makeCode = function (sText) {
    this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel);
    this._oQRCode.addData(sText);
    this._oQRCode.make();
    this._el.title = sText;
    this._oDrawing.draw(this._oQRCode);
    this.makeImage();
};

/**
 * Make the Image from Canvas element
 * - It occurs automatically
 * - Android below 3 doesn't support Data-URI spec.
 *
 * @private
 */
QRCode.prototype.makeImage = function () {
    if (typeof this._oDrawing.makeImage == "function") {
        this._oDrawing.makeImage();
    }
};

/**
 * Clear the QRCode
 */
QRCode.prototype.clear = function () {
    this._oDrawing.clear();
};

/**
 * @name QRCode.CorrectLevel
 */
QRCode.CorrectLevel = QRErrorCorrectLevel;

function bitlyShortener(accessToken) {

    if (!accessToken || accessToken === "BITLY_TOKEN") {
        return undefined
    } else {
        return async function (url) {
            const api = "https://api-ssl.bitly.com/v3/shorten";
            const devIP = "192.168.1.11";
            if (url.startsWith("http://localhost")) {
                url = url.replace("localhost", devIP);
            }  // Dev hack
            let endpoint = api + "?access_token=" + accessToken + "&longUrl=" + encodeURIComponent(url);
            return igvxhr.loadJson(endpoint, {})
                .then(function (json) {
                    return json.data.url
                })
        }
    }
}


function tinyURLShortener({endpoint, api_token}) {
    endpoint = endpoint || "https://api.tinyurl.com/create";
    let token = api_token;
    if(token.startsWith("ll4539")) {
        token = atob(token.substring(6));
    }
    return async function (url) {
        const response = await fetch(`${endpoint}?api_token=${token}`, {
            method: 'post',
            mode: "cors", // no-cors, *cors, same-origin
            cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
            headers: {
                "accept": "application/json",
                "Content-Type": "application/json",
                // 'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: JSON.stringify({url: url}), // body data type must match "Content-Type" header
        });
        if (response.ok) {
            const json = await response.json();
            if (json.errors.length > 0) {
                throw Error(json.errors[0])
            } else {
                return json.data["tiny_url"]
            }
        } else {
            throw new Error(response.statusText)
        }
    }
}

/*
 *  The MIT License (MIT)
 *
 * Copyright (c) 2016-2017 The Regents of the University of California
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
 * following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,  FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */

let urlShortener;

function setURLShortener(config) {

    let fn;
    if (typeof config === "function") {
        fn = config;

    } else if (config.provider) {
        if ("tinyURL" === config.provider && (config.apiKey || config.api_token)) {
            fn = tinyURLShortener(config);
        } else if ("bitly" === config.provider && config.apiKey) {
            fn = bitlyShortener(config.apiKey);
        }  else {
            alertSingleton.present(new Error(`Unknown URL shortener provider: ${config.provider}`));
        }
    } else {
        alertSingleton.present(new Error('URL shortener object must either be an object specifying a provider and apiKey, or a function'));
    }

    if (fn) {
        urlShortener =
            {
                shortenURL: fn
            };
    }

    return fn;

}

function sessionURL() {

    let surl,
        path,
        idx;

    path = window.location.href.slice();
    idx = path.indexOf("?");

    surl = (idx > 0 ? path.substring(0, idx) : path) + "?sessionURL=blob:" + Globals.browser.compressedSession();

    return surl;
}

async function shortSessionURL(base, session) {

    const url = `${base}?sessionURL=blob:${session}`;

    return urlShortener ? await urlShortener.shortenURL(url) : url

}

function doShortenURL() {
    return !(undefined === urlShortener)
}

/*
 *  The MIT License (MIT)
 *
 * Copyright (c) 2016-2017 The Regents of the University of California
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
 * following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,  FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */

let href;
let session;
let modal;

function createShareWidgets(container, browser, options) {

    const modalElement = document.getElementById('igv-app-share-modal');
    const share_input = document.getElementById('igv-app-share-input');
    const copy_link_button = document.getElementById('igv-app-copy-link-button');
    const qrcode_button = document.getElementById('igv-app-qrcode-button');
    const qrcode_image = document.getElementById('igv-app-qrcode-image');
    const embed_container = document.getElementById('igv-app-embed-container');
    const embed_button = document.getElementById('igv-app-embed-button');
    const iframeButton = document.querySelector('#igv-app-copy-iframe-button');
    const embedTarget = options.embedTarget || `https://igv.org/web/release/${index.version()}/embed.html`;

    modal = new bootstrap.Modal(modalElement);

    if (options.urlShortener) {
        setURLShortener(options.urlShortener) !== undefined;
    }

// Function to show the modal
    modalElement.addEventListener('show.bs.modal', (e) => {
        iframeButton.style.display = 'none';

        document.querySelector('#igv-app-qrcode-image').style.display = 'none';
        document.querySelector('#igv-app-qrcode-container').style.display = 'none';

        if (true === doShortenURL()) {
            document.querySelector('#igv-share-short-url-radio').checked = false;
            document.querySelector('#igv-share-long-url-radio').checked = true;
        } else {
            document.querySelector('#igv-share-url-radio-pair-container').style.display = 'none';
        }
    });

    modalElement.addEventListener('shown.bs.modal', async () => {
        session = undefined;
        try {
            session = browser.compressedSession();
        } catch (e) {
            modal.hide();
            alertSingleton.present(e.message);
        }

        if (session) {
            if (embedTarget) {
                const snippet = getEmbeddableSnippet(container, embedTarget, session);
                const textArea = embed_container.querySelector('textarea');
                textArea.value = snippet;
                textArea.select();
            }

            href = window.location.href.slice();
            const idx = href.indexOf("?");
            if (idx > 0) {
                href = href.substring(0, idx);
            }

            share_input.value = `${href}?sessionURL=blob:${session}`;
        } else {
            modal.hide();
        }
    });

    modalElement.addEventListener('hidden.bs.modal', () => {
        href = session = undefined;
        embed_container.style.display = 'none';
        qrcode_image.style.display = 'none';
    });

    document.getElementById('igv-share-short-url-radio').addEventListener('click', async () => {
        share_input.value = await shortSessionURL(href, session);
        embed_container.style.display = iframeButton.style.display = 'none';
        document.querySelector('#igv-app-qrcode-container').style.display = 'block';
    });

    document.getElementById('igv-share-long-url-radio').addEventListener('click', async () => {
        document.querySelector('#igv-app-qrcode-image').style.display = 'none';
        document.querySelector('#igv-app-qrcode-container').style.display = 'none';
        share_input.value = `${href}?sessionURL=blob:${session}`;
    });

    copy_link_button.addEventListener('click', () => {
        share_input.select();
        const success = document.execCommand('copy');
        if (success) {
            modal.hide();
        } else {
            console.error('fail!');
        }
    });

    if (embedTarget) {
        iframeButton.addEventListener('click', () => {

            const textArea = embed_container.querySelector('textarea');
            textArea.select();

            const success = document.execCommand('copy');

            if (success) {
                modal.hide();
            } else {
                console.error('fail!');
            }
        });

        embed_button.addEventListener('click', () => {

            qrcode_image.style.display = 'none';

            if ('block' === embed_container.style.display) {
                embed_container.style.display = iframeButton.style.display = 'none';
            } else {
                embed_container.style.display = iframeButton.style.display = 'block';
            }

        });

    } else {
        modalElement.querySelector('.btn-group').style.display = 'none';
    }

    qrcode_button.addEventListener('click', async () => {

        embed_container.style.display = iframeButton.style.display = 'none';

        if ('block' === qrcode_image.style.display) {
            qrcode_image.innerHTML = '';
            qrcode_image.style.display = 'none';
        } else {
            qrcode_image.innerHTML = '';
            const qrcode = new QRCode(qrcode_image, {width: 128, height: 128, correctLevel: QRCode.CorrectLevel.H});
            const shortURL = await shortSessionURL(href, session);
            qrcode.makeCode(shortURL);
            qrcode_image.style.display = 'block';
        }

    });

}

function getEmbeddableSnippet(container, embedTarget, session) {
    const embedUrl = `${embedTarget}?sessionURL=blob:${session}`;
    const height = container.clientHeight + 50;
    return '<iframe src="' + embedUrl + '" style="width:100%; height:' + height + 'px"  allowfullscreen></iframe>'
}

/*
 *  The MIT License (MIT)
 *
 * Copyright (c) 2016-2017 The Regents of the University of California
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
 * following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,  FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */


function createSaveImageWidgets(browser) {

    for(let imageType of ['png', 'svg']) {
        const saveModal = new bootstrap.Modal(document.getElementById(`igv-app-${imageType}-save-modal`));
        const modalElement = saveModal._element;

        const input_default_value = `igv-app.${imageType}`;

        const input = modalElement.querySelector('input');

        modalElement.addEventListener('show.bs.modal', () => input.value = input_default_value);

        modalElement.addEventListener('hidden.bs.modal', () => input.value = input_default_value);

        const okHandler = () => {

            let filename = input.value;

            if (undefined === filename || '' === filename) {
                filename = input.getAttribute('placeholder');
            } else if (!filename.endsWith(`.${imageType}`)) {
                filename = `${filename}.${imageType}`;
            }

            saveModal.hide();

            imageType === 'svg' ? browser.saveSVGtoFile(filename) : browser.savePNGtoFile(filename);
        };

        // ok - button
        const ok = modalElement.querySelector('.modal-footer button:nth-child(2)');

        ok.addEventListener('click', okHandler);

        input.addEventListener('keyup', (e) => {
            if (13 === e.keyCode) {
                okHandler();
            }
        });

        // upper dismiss - x - button
        let dismiss = modalElement.querySelector('.modal-header button');
        dismiss.addEventListener('click', () => saveModal.hide());

        // lower dismiss - close - button
        dismiss = modalElement.querySelector('.modal-footer button:nth-child(1)');
        dismiss.addEventListener('click', () => saveModal.hide());
    }
}

const _version = "2.3.0";
function version() {
    return _version;
}

function createCircularViewResizeModal(id, title){

    const str = 'igv-app-circular-view-resize-modal';

    const html =
        `<div id="${ id }" class="modal">

            <div class="modal-dialog modal-sm">
    
                <div class="modal-content">
    
                    <div class="modal-header">
                        <div class="modal-title">${ title }</div>
    
                        <button type="button" class="close" data-dismiss="modal">
                            <span>&times;</span>
                        </button>
    
                    </div>
    
                    <div class="modal-body">
                          <div class="form-group">
                            <input id="${ str }-input" type="text" class="form-control">
                          </div>
                    </div>
        
                </div>
    
            </div>

        </div>`;

    const fragment = document.createRange().createContextualFragment(html);

    return fragment.firstChild
}

class NotificationDialog {
    constructor(parent, key) {

        this.card = document.createElement('div');
        this.card.className = "card custom-dialog-card";
        parent.appendChild(this.card);

        this.cardBody = document.createElement('div');
        this.cardBody.className = "card-body p-4";
        this.card.appendChild(this.cardBody);

        this.messageArea = document.createElement('div');
        this.messageArea.className = "card-text mb-4";
        this.cardBody.appendChild(this.messageArea);

        this.buttonContainer = document.createElement('div');
        this.buttonContainer.className = "d-flex justify-content-center gap-2";
        this.cardBody.appendChild(this.buttonContainer);

        this.gotItButton = document.createElement('button');
        this.gotItButton.className = "btn btn-secondary btn-sm";
        this.gotItButton.textContent = "Got it";
        this.buttonContainer.appendChild(this.gotItButton);

        this.remindButton = document.createElement('button');
        this.remindButton.className = "btn btn-secondary btn-sm";
        this.remindButton.textContent = "Remind me later";
        this.buttonContainer.appendChild(this.remindButton);

        this.gotItButton.addEventListener('click', () => this.handleGotIt(key));
        this.remindButton.addEventListener('click', () => this.handleRemind());

        // Handle Escape key
        document.addEventListener('keydown', (event) => {
            if (event.key === 'Escape' && this.card.style.display !== 'none') {
                this.handleRemind();
            }
        });

        this.card.style.display = 'none';
    }

    handleGotIt(key) {
        localStorage.setItem(`note_${key}`, "true");
        this.card.style.display = 'none';
    }

    handleRemind() {
        this.card.style.display = 'none';
    }

    present(str) {
        this.messageArea.innerHTML = str;
        this.card.style.display = "";
    }
}

function createStringInputModal(rootContainer, modalID, handler,
                                               {title, prompt, type, rows, cols, minLength, maxLength, placeholder}) {

    const blatModalElement = createModalElement(modalID, title);
    rootContainer.appendChild(blatModalElement);
    const blatModal = new bootstrap.Modal(blatModalElement);

    const parent = blatModalElement.querySelector('.modal-body');

    const container = div('igv-file-load-widget-container');
    parent.appendChild(container);


    let input_data_row;
    if (prompt) {
        input_data_row = div('igv-flw-input-row');
        container.appendChild(input_data_row);
        // label
        const labelElem = div('igv-flw-input-label');
        input_data_row.appendChild(labelElem);
        labelElem.textContent = prompt;
    }

    type = type || 'text';
    let input;
    if ('text' === type) {
        input = document.createElement('input');
        input.setAttribute('type', 'text');
    } else {
        input = document.createElement(type);
    }
    if (placeholder) input.setAttribute('placeholder', placeholder);
    if (rows) input.setAttribute('rows', rows);
    if (cols) input.setAttribute('cols', cols);
    if (minLength) input.setAttribute('minlength', minLength);
    if (maxLength) input.setAttribute('maxlength', maxLength);

    const targetContainer = input_data_row || container;
    targetContainer.appendChild(input);

    const doDismiss = () => {
        blatModal.hide();
    };

    const doOK = async () => {
        const value = input.value;
        const success = await handler(value);
        if(success !== false) {
            // Clear the input field
            input.value = '';
            blatModal.hide();
        }
    };

    const modalElement = blatModal._element;

    // x - button
    modalElement.querySelector('.modal-header button').addEventListener('click', doDismiss);

    // ok - button
    modalElement.querySelector('.modal-footer button:nth-child(2)').addEventListener('click', doOK);

    modalElement.addEventListener('keypress', event => {
        if ('Enter' === event.key) {
            doOK();
        }
    });
}

function createModalElement(id, title) {

    const html =
        `<div id="${id}" class="modal fade" tabindex="-1">

        <div class="modal-dialog modal-lg">
    
            <div class="modal-content">
    
                <div class="modal-header">
                    <div class="modal-title">${title}</div>
    
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
    
                </div>
    
                <div class="modal-body">
                </div>
    
                <div class="modal-footer">
                    <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
                    <button type="button" class="btn btn-sm btn-secondary" >OK</button>
                </div>
    
            </div>
    
        </div>

    </div>`;

    const fragment = document.createRange().createContextualFragment(html);

    return fragment.firstChild
}

// Convenience function to create a div with a class
function div(className) {
    const div = document.createElement('div');
    div.classList.add(className);
    return div
}

function createToolsWidgets(rootContainer, browser, options) {


    // Blat
    const blatModalID = 'igv-blat-input-modal';
    const handler = async (blatSequence) => {

        if (blatSequence) {
            if (blatSequence.length < 20 || blatSequence.length > 8000) {
                alertSingleton.present("BLAT sequences must be between 20 and 8000 bases in length.");
                return false
            } else {
                await browser.blat(blatSequence, options);
                return true
            }
        }

    };
    //{title, prompt, type, rows, cols, minLength, maxLength, placeholder}
    createStringInputModal(rootContainer, blatModalID, handler,
        {
            title: 'Enter BLAT Sequence',
            type: 'textarea',
            rows: 10,
            cols: 80,
            minLength: 20,
            maxLength: 8000,
            placeholder: 'Enter BLAT query sequence here'
        });
}

let sampleInfoURLModal;

function createSampleInfoWidgets(igvMain, browser) {

    const localFileInput = document.getElementById('igv-app-sample-info-dropdown-local-track-file-input');
    const dropboxButton = document.getElementById('igv-app-dropdown-dropbox-sample-info-file-button');
    const googleDriveButton = document.getElementById('igv-app-dropdown-google-drive-sample-info-file-button');
    const urlModalId = 'igv-app-sample-info-from-url-modal';


    const sampleInfoFileLoadHandler = async configuration => {
        try {
            await browser.loadSampleInfo(configuration);
        } catch (e) {
            console.error(e);
            alertSingleton.present(e);
        }
    };

    // local file
    localFileInput.addEventListener('change', async () => {

        const {files} = localFileInput;

        const paths = Array.from(files);

        localFileInput.value = '';

        await sampleInfoFileLoadHandler({url: paths[0]});
    });

    //  Dropbox
    if (dropboxButton) dropboxButton.addEventListener('click', async () => {

        const result = await initializeDropbox();

        if (true === result) {

            const obj =
                {
                    success: dbFiles => {

                        const configList = dbFiles.map(({link}) => {
                            return {url: link}
                        });

                        sampleInfoFileLoadHandler(configList[0]);
                    },
                    cancel: () => {
                    },
                    linkType: "preview",
                    multiselect: false,
                    folderselect: false,
                };

            Dropbox.choose(obj);

        } else {
            alertSingleton.present('Cannot connect to Dropbox');
        }
    });

    // Google Drive
    if (googleDriveButton) {
        googleDriveButton.addEventListener('click', () => {

            const filePickerHandler = async responses => {
                const paths = responses.map(({url}) => url);
                await sampleInfoFileLoadHandler({url: paths[0]});
            };

            createDropdownButtonPicker(false, filePickerHandler);
        });
    }

    // URL
    createSampleInfoURLWidget(urlModalId, igvMain, sampleInfoFileLoadHandler);
}

function createSampleInfoURLWidget(urlModalId, igvMain, sampleInfoFileLoadHandler) {

    const html =
        `<div id="${urlModalId}" class="modal fade" tabindex="-1">
        <div class="modal-dialog modal-lg">
            <div class="modal-content">
                <div class="modal-header">
                    <div class="modal-title">Sample Info URL</div>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
    
                <div class="modal-body">
                </div>
    
                <div class="modal-footer">
                    <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
                    <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">OK</button>
                </div>
            </div>
        </div>
    </div>`;

    const fragment = document.createRange().createContextualFragment(html);
    const urlModalElement = fragment.firstChild;

    igvMain.appendChild(urlModalElement);

    const fileLoadWidgetConfig =
        {
            widgetParent: urlModalElement.querySelector('.modal-body'),
            dataTitle: 'Sample Info',
            indexTitle: 'Index',
            dataOnly: true
        };

    const fileLoadWidget = new URLLoadWidget(fileLoadWidgetConfig);

    sampleInfoURLModal = new bootstrap.Modal(urlModalElement);
    configureModal(fileLoadWidget, sampleInfoURLModal, async fileLoadWidget => {
        const paths = fileLoadWidget.retrievePaths();
        await sampleInfoFileLoadHandler({url: paths[0]});
        return true
    });
}

/*
 *  The MIT License (MIT)
 *
 * Copyright (c) 2016-2017 The Regents of the University of California
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
 * following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,  FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */


document.addEventListener("DOMContentLoaded", async (event) => await main(document.getElementById('igv-app-container'), igvwebConfig));

let isDropboxEnabled = false;

let isGoogleEnabled;
let isGoogleDriveEnabled;

let currentGenomeId;

let notificationDialog;

async function main(container, config) {

    alertSingleton.init(document.getElementById('igv-main'));

    document.getElementById('igv-app-version').textContent = `IGV-Web app version ${version()}`;
    document.getElementById('igv-igvjs-version').textContent = `igv.js version ${index.version()}`;

    if (config.notifications) {
        setupNotifications(config.notifications);
    }

    isDropboxEnabled = undefined !== config.dropboxAPIKey;
    isGoogleEnabled = undefined !== config.clientId;
    isGoogleDriveEnabled = config.googleDriveEnabled && isGoogleEnabled;

    configureCloudButtons();

    // Configure Google oAuth
    if (isGoogleEnabled) {
        checkGoogleConfig(config);
        try {
            await init$1({
                client_id: config.clientId,
                apiKey: config.apiKey,
                scope: 'https://www.googleapis.com/auth/userinfo.profile',
            });

        } catch (e) {
            const str = `Error Google oAuth: ${e.message || e.details}`;
            alertSingleton.present(str);
            console.error(str);
        }
    }

    // Load pre-defined genomes for use by igv.js and webapp
    if (config.genomes) {
        config.genomes = await getGenomesArray(config.genomes);
        config.igvConfig.genomes = config.genomes;
        config.igvConfig.loadDefaultGenomes = false;
    }

    // Recently accessed genomes, which might include custom genomes loaded by URL
    let recentGenomes;
    const recentGenomesString = localStorage.getItem("recentGenomes");
    if (recentGenomesString) {
        recentGenomes = JSON.parse(recentGenomesString);
    }


    // Initialize igv.js, creating the Browser instance
    let browser;

    // Optionally change the igv genome to the last genome used.
    const igvConfig = config.igvConfig;
    const igvConfigGenome = igvConfig.genome;   // the genome specified in the configuration file
    if (config.restoreLastGenome) {
        try {
            const lastGenomeId = localStorage.getItem("genomeID");
            if (lastGenomeId && lastGenomeId !== igvConfigGenome) {
                if (config.genomes && config.genomes.find(elem => elem.id === lastGenomeId) ||
                    (recentGenomes && recentGenomes.find(elem => elem.id === lastGenomeId)) ||
                    ((lastGenomeId.startsWith("GCA_") || lastGenomeId.startsWith("GCF_")) && lastGenomeId.length >= 13)) {
                    igvConfig.genome = lastGenomeId;
                    igvConfig.tracks = [];
                }
            }
        } catch (e) {
            console.error(e);
        }
    }
    try {
        browser = await index.createBrowser(container, igvConfig);

    } catch (e) {
        // Try configured genome
        if(igvConfigGenome !== igvConfig.genome) {
            igvConfig.genome = igvConfigGenome;
            if(browser) {
                index.removeBrowser(browser);
            }
            browser = await index.createBrowser(container, igvConfig);
        } else {
            console.error(e);
            alertSingleton.present(e);
            return
        }
    }
    Globals.browser = browser;

    const igvMain = document.getElementById('igv-main');

    // Genome widgets
    await createGenomeWidgets(igvMain, browser, config.genomes);


    // Sample info
    createSampleInfoWidgets(igvMain, browser);

    // Sessions
    await createSessionWidgets(igvMain, browser, config);

    // Tools
    createToolsWidgets(igvMain, browser, config);

    // Image save
    createSaveImageWidgets(browser);

    // Share
    createShareWidgets(container, browser, config);


    // Tracks
    await createTrackWidgets(igvMain, browser, config);

    browser.on("genomechange", async ({genome, trackConfigurations}) => {
            if (currentGenomeId !== genome.id) {
                currentGenomeId = genome.id;
                await trackMenuGenomeChange(browser, genome);
            }
        });
    // Manually fire the genome change event to initialize the track menu.
    await trackMenuGenomeChange(browser, browser.genome);



    createAppBookmarkHandler($('#igv-app-bookmark-button'));

    if (true === config.enableCircularView) {

        const {x: minX, y: minY} = document.querySelector('#igv-main').getBoundingClientRect();

        const circularViewContainer = document.getElementById('igv-circular-view-container');

        browser.createCircularView(circularViewContainer, false);

        makeDraggable(circularViewContainer, browser.circularView.toolbar, {minX, minY});

        browser.circularView.setSize(512);

        document.getElementById('igv-app-circular-view-nav-item').style.display = 'block';

        const dropdownButton = document.getElementById('igv-app-circular-view-dropdown-button');
        dropdownButton.addEventListener('click', e => {

            document.getElementById('igv-app-circular-view-presentation-button').innerText = browser.circularViewVisible ? 'Hide' : 'Show';

            if (browser.circularViewVisible) {
                document.getElementById('igv-app-circular-view-resize-button').removeAttribute('disabled');
                document.getElementById('igv-app-circular-view-clear-chords-button').removeAttribute('disabled');
            } else {
                document.getElementById('igv-app-circular-view-resize-button').setAttribute('disabled', '');
                document.getElementById('igv-app-circular-view-clear-chords-button').setAttribute('disabled', '');
            }


        });

        document.getElementById('igv-app-circular-view-presentation-button').addEventListener('click', e => {
            browser.circularViewVisible = !browser.circularViewVisible;
            const str = e.target.innerText;
            e.target.innerText = 'Show' === str ? 'Hide' : 'Show';
        });

        document.getElementById('igv-app-circular-view-clear-chords-button').addEventListener('click', () => browser.circularView.clearChords());

        document.getElementById('igv-main').appendChild(createCircularViewResizeModal('igv-app-circular-view-resize-modal', 'Resize Circular View'));

        document.getElementById('igv-app-circular-view-resize-modal-input').addEventListener('keyup', (event) => {
            event.preventDefault();
            event.stopPropagation();
            if (13 === event.keyCode) {
                browser.circularView.setSize(Number.parseInt(event.target.value));
            }
        });

        document.getElementById('igv-app-circular-view-resize-modal').addEventListener('shown.bs.modal', () => {
            document.getElementById('igv-app-circular-view-resize-modal-input').value = circularViewContainer.clientWidth.toString();
        });
    }

}


function configureCloudButtons() {

    ['track', 'genome', 'sample-info'].forEach(str => {
        let imgElement;

        if (isDropboxEnabled) {
            imgElement = document.querySelector(`img#igv-app-${str}-dropbox-button-image`);
            imgElement.src = `data:image/svg+xml;base64,${dropboxButtonImageBase64()}`;
        } else {
            imgElement = document.getElementById(`igv-app-dropdown-dropbox-${str}-file-button`);
            imgElement.parentElement.style.display = 'none';
        }

        if (isGoogleDriveEnabled) {
            imgElement = document.querySelector(`img#igv-app-${str}-google-drive-button-image`);
            imgElement.src = `data:image/svg+xml;base64,${googleDriveButtonImageBase64()}`;
        } else {
            imgElement = document.getElementById(`igv-app-dropdown-google-drive-${str}-file-button`);
            imgElement.parentElement.style.display = 'none';
        }
    });

    if (isDropboxEnabled) {
        const sessionDropdownMenu = document.querySelector('div#igv-session-dropdown-menu');
        const firstChild = sessionDropdownMenu.children[0];
        firstChild.insertAdjacentHTML('afterend', dropboxDropdownItem('igv-app-dropdown-dropbox-session-file-button'));
    }

    if (isGoogleDriveEnabled) {
        document.querySelector('div#igv-session-dropdown-menu > :nth-child(2)')
            .insertAdjacentHTML('afterend', googleDriveDropdownItem('igv-app-dropdown-google-drive-session-file-button'));
    }

    // Google sign in NOT google drive
    if (isGoogleEnabled) {
        configureGoogleSignInButton();
    }
}

function setupNotifications(notificatons) {

    if (notificatons && notificatons.length > 0) {

        notificatons.map(notification => {

            const [key, value] = Object.entries(notification).flat();
            notificationDialog = new NotificationDialog(document.body, key);

            const status = "true" === localStorage.getItem(`note_${key}`);
            if (!status) {
                notificationDialog.present(value);
            }

        });
    }

}

function configureGoogleSignInButton() {

    if (true === isGoogleEnabled) {

        const dropdownToggle = document.querySelector('#igv-google-drive-dropdown-toggle');
        dropdownToggle.style.display = 'block';

        const signInOutButton = document.querySelector('#igv-google-drive-sign-out-button');

        let currentUserProfile = undefined;

        $('#igv-google-drive-dropdown').on('show.bs.dropdown', async () => {

            currentUserProfile = await getCurrentUserProfile();

            if (currentUserProfile) {
                const name = currentUserProfile.email || currentUserProfile.name || '';
                signInOutButton.innerText = `Sign Out ${name}`;
            } else {
                signInOutButton.innerText = 'Sign In';
            }

        });

        signInOutButton.addEventListener('click', async () => {

            if (currentUserProfile) {
                await signOut();
            } else {
                await signIn();
            }
        });
    }
}

function checkGoogleConfig(config) {
    if (config.apiKey && config.apiKey.startsWith("leawkk")) {
        config.apiKey = atob(config.apiKey.substring(6));
    }
    if (config.clientId && config.clientId.startsWith("Z_%%12")) {
        config.clientId = atob(config.clientId.substring(6));
    }
}

function createAppBookmarkHandler($bookmark_button) {

    $bookmark_button.on('click', (e) => {

        let url = undefined;
        try {
            url = sessionURL();
        } catch (e) {
            alertSingleton.present(e.message);
        }

        if (url) {
            window.history.pushState({}, "IGV", url);

            const str = (/Mac/i.test(navigator.userAgent) ? 'Cmd' : 'Ctrl');
            const blurb = 'A bookmark URL has been created. Press ' + str + '+D to save.';
            alert(blurb);
        }
    });
}

async function getGenomesArray(genomes) {

    if (undefined === genomes) {
        return undefined
    }
    if (Array.isArray(genomes)) {
        return genomes
    } else {

        try {
            return igvxhr.loadJson(genomes)
        } catch (e) {
            alertSingleton.present(e.message);
        }
    }
}

export { main };
