Switch to side-by-side view

--- a
+++ b/exseek/templates/igv/main.html
@@ -0,0 +1,408 @@
+
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta name="description" content="">
+    <meta name="author" content="">
+    <link rel="shortcut icon" href="https://igv.org/web/img/favicon.ico">
+    <title>Integrative Genomics Viewer</title>
+
+    <!-- Bootstrap 4 Dependancies - jQuery | Popper -->
+    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
+
+    <!-- Latest compiled and minified CSS -->
+    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+    <!-- Optional theme -->
+    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
+
+    <!-- Latest compiled and minified JavaScript -->
+    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+
+    <!-- Global styles -->
+    <style type="text/css">
+    body {
+            font-size: 11px;
+        }
+    p.panel-title {
+        font-size: 14px;
+        font-weight: bold;
+        margin-bottom: 0.5em;
+    }
+    #sample-list {
+        list-style-type: none;
+        margin: 0;
+        padding: 0;
+    }
+    #sample-list li{
+        margin: 0;
+    }
+    </style>
+
+    <!-- IGV JS -->
+    <script src="https://igv.org/web/release/2.1.0/dist/igv.min.js"></script>
+
+</head>
+
+<body>
+
+<script type="text/javascript">
+    var dataTrackTypes = {"wig": 0, "alignment": 0};
+    var tracks = {{ tracks_json }};
+
+    var groups = [
+    {% for group_name, group in groups.items() %}
+        {
+            "name": "{{ group_name }}",
+            "color": "{{ group.color }}",
+            "show": "{{ group.show }}"
+        } {% if not loop.last %},{% endif %}
+    {% endfor %}
+    ];
+    var showStrand = {"+": true, "-": true};
+    var showGroup = {};
+    for(let i = 0; i < groups.length; i ++)
+        showGroup[groups[i].name] = groups[i].show;
+    var sampleIds = [];
+    var showSample = {};
+    for(name in tracks){
+        if(tracks[name].type in dataTrackTypes){
+            if(!(tracks[name].sample_id in showSample))
+                sampleIds.push(tracks[name].sample_id);
+            showSample[tracks[name].sample_id] = tracks[name].show;
+        }
+    }
+
+    applyForAllTracks = function(func){
+        for(name in tracks){
+            if(tracks[name].type in dataTrackTypes){
+                func(tracks[name]);
+            }
+        }
+    };
+
+    findGroup = function(name){
+        var found = [];
+        groups.forEach(function(group){
+            if(group.name == name)
+                found.push(group);
+        });
+        if(found.length > 0)
+            return found[0];
+    }
+
+    createTrackNavigator = function(){
+        var trackNav = document.getElementById("track-nav");
+        onToggleTrack = function(event){
+            tracks[event.target.name].show = event.target.checked;
+            toggleTracks();
+        };
+        var checkbox, label, ul, li;
+        var trackNavCheckBoxes = {};
+        ul = document.createElement("ul");
+        ul.setAttribute("id", "sample-list");
+        for(name in tracks){
+            li = document.createElement("li");
+            checkbox = document.createElement("input");
+            checkbox.setAttribute("type", "checkbox");
+            checkbox.setAttribute("name", name);
+            if(tracks[name].show)
+                checkbox.setAttribute("checked", "");
+            checkbox.addEventListener("change", onToggleTrack);
+            li.appendChild(checkbox);
+            trackNavCheckBoxes[name] = checkbox;
+
+            label = document.createElement("label");
+            label.setAttribute("for", name);
+            label.appendChild(document.createTextNode(name));
+            li.appendChild(label);
+            ul.appendChild(li);
+        }
+        trackNav.appendChild(ul);
+        return trackNavCheckBoxes;
+    };
+
+    createTrackGroupSelector = function(){
+        var tr, td, checkbox, label, color;
+        var groupTable = document.getElementById("track-groups-table");
+        onShowTrackGroupChecked = function(event){
+            groups[event.target.name].show = event.target.checked;
+            showTrackGroup(event.target);
+        }
+        onTrackColorGroupChanged = function(event){
+            groups[event.target.name].show = event.target.value;
+            setTrackColorGroup(event.target);
+        }
+        for(let i = 0; i < groups.length; i ++){
+            var group = groups[i];
+            tr = document.createElement("tr");
+
+            checkbox = document.createElement("input");
+            checkbox.setAttribute("type", "checkbox");
+            checkbox.setAttribute("name", "show-group-" + i);
+            if(group.show)
+                checkbox.setAttribute("checked", "");
+            checkbox.addEventListener("change", showTrackGroup);
+            
+            label = document.createElement("label");
+            label.setAttribute("for", "show-group-" + i);
+            label.innerHTML = group.name;
+
+            td = document.createElement("td");
+            td.appendChild(checkbox);
+            td.appendChild(label);
+            tr.appendChild(td);
+
+            color = document.createElement("input");
+            color.setAttribute("type", "color");
+            color.setAttribute("name", "track-color-group-" + i);
+            color.setAttribute("value", group.color);
+            color.addEventListener("change", setTrackColorGroup);
+            td = document.createElement("td");
+            td.appendChild(color);
+            tr.appendChild(td);
+
+            groupTable.appendChild(tr);
+        }
+    };
+
+    updateTrackViews = function(items){
+        for(name in tracks){
+            if(!(tracks[name].type in dataTrackTypes))
+                continue;
+            igv.browser.findTracks("name", name).forEach(function(track){
+                for(let i = 0; i < items.length; i ++){
+                    if(items[i] == "height")
+                        track.trackView.setTrackHeight(tracks[name].height);
+                    else if(items[i] == "data_range"){
+                        track.trackView.setDataRange(tracks[name].min, tracks[name].max, tracks[name].autoscale);
+                        if(tracks[name].autoscale)
+                            track.trackView.updateViews();
+                    }else if(items[i] == "color"){
+                        track.trackView.setColor(tracks[name].color);
+                    }else{
+                        track.trackView.track.logScale = tracks[name].logScale;
+                        track.trackView.updateViews();
+                    }
+                }
+            });
+        }
+    };
+
+    // show or remove tracks according to track config
+    toggleTracks = function(){
+        var currentTrack;
+        var showTrack;
+        var track;
+        for(name in tracks){
+            track = tracks[name];
+            currentTrack = igv.browser.findTracks("name", name);
+            if(track.type in dataTrackTypes){
+                showTrack = track.show && showGroup[track.group];
+                if(track.type == "wig"){
+                    showTrack = showTrack && showStrand[track.strand];
+                }
+            }else{
+                showTrack = track.show;
+            }
+            if(showTrack && (currentTrack.length == 0)){
+                igv.browser.loadTrack(track);
+            }else if(!showTrack && (currentTrack.length > 0)){
+                igv.browser.removeTrackByName(name);
+            }
+        }
+    };
+
+    showTracksWithStrand = function(element){
+        var name, group;
+        var strand = (element.name == "show-plus-track")? "+" : "-";
+        showStrand[strand] = element.checked;
+        toggleTracks();
+    };
+
+    showTrackGroup = function(element){
+        var c = element.name.split("-");
+        var group = groups[parseInt(c[c.length - 1])];
+        showGroup[group.name] = element.checked;
+        group.show = element.checked;
+        toggleTracks();
+    };
+
+    setTrackColorGroup = function(element){
+        var c = element.name.split("-");
+        var group = groups[parseInt(c[c.length - 1])].name;
+        var i;
+        var trackView;
+        for(name in tracks){
+            if(tracks[name].group == group){
+                tracks[name].color = element.value;
+            }
+        }
+        updateTrackViews(["color"]);
+    };
+</script>
+
+<div id="left-panel" style="position: fixed; margin-left: 5px; width: 300px; height: 100%; overflow: scroll">
+    <div id="config-panel" style="border:1px solid lightgray; padding-left: 10px; padding-top: 10px;padding-bottom: 10px">
+        <p class="panel-title">Track configuation</p>
+        <table id="table-config">
+            <tr><td>Minimum value: </td><td><input type="text" id="config-data-min" value="0" style="width: 50px"></td></tr>
+            <tr><td>Maximum value: </td><td><input type="text" id="config-data-max" value="100" style="width: 50px"></td></tr>
+            <tr><td>Autoscale: </td><td><input type="checkbox" id="config-autoscale" checked></td></tr>
+            <tr><td>Track height: </td><td><input type="text" id="config-height" value="25" style="width: 50px"></td></tr>
+        </table>
+    </div>
+    <div id="sequence-panel" style="border:1px solid lightgray; padding-left: 10px; padding-top: 10px;padding-bottom: 10px; margin-top: 5px">
+        <p class="panel-title">Sequence</p> 
+        <p id="sequence-locus"></p>
+        <textarea id="sequence-box" rows="4" cols="40" readonly></textarea><br>
+        <input type="checkbox" id="reverse-sequence" name="reverse-sequence" onchange="showSequence()">
+        <label for="reverse-sequence">Reverse complement</label><br>
+        <input type="button" id="show-sequence" value="Show sequence">
+        <input type="button" id="copy-sequence" value="Copy sequence">
+    </div>
+    <div id="track-selection" style="border:1px solid lightgray; padding-left: 10px; padding-top: 10px;padding-bottom: 10px; margin-top: 5px">
+        <p class="panel-title">Track selection</p>
+        <input type="checkbox" onchange="showTracksWithStrand(this)" name="show-plus-track" checked>
+        <label for="show-plus-track">Show tracks in + strand</label><br>
+        <input type="checkbox" onchange="showTracksWithStrand(this)" name="show-minus-strand" checked>
+        <label for="show-minus-track">Show tracks in - strand</label><br>
+    </div>
+    <div id="track-groups" style="border:1px solid lightgray; padding-left: 10px; padding-top: 10px;padding-bottom: 10px; margin-top: 5px">
+        <p class="panel-title">Track groups</p>
+        <table id="track-groups-table" border="0">
+        {% for name, group in groups.items() %}
+            <tr>
+            <td><input type="checkbox" onchange="showTrackGroup(this)" name="show-group-{{ loop.index0 }}" checked>
+            <label for="show-group-{{ loop.index0 }}">{{ name }}</label></td>
+            <td><input type="color" onchange="setTrackColorGroup(this)" name="track-color-group-{{ loop.index0 }}" value="{{ group.color }}"></td>
+            </tr>
+        {% endfor %}
+        </table>
+    </div>
+    <div id="track-nav" style="border:1px solid lightgray; padding-left: 10px; padding-top: 10px;padding-bottom: 10px; margin-top: 5px; overflow: scroll; height:400px;">
+        <p class="panel-title">Track navigator</p>
+    </div>
+</div>
+
+<div id="igv-div" style="margin-left: 310px; padding-top: 10px;padding-bottom: 10px; border:1px solid lightgray"></div>
+
+<script type="text/javascript">
+    document.addEventListener("DOMContentLoaded", function () {
+
+        var options = {{ options_json }};
+
+        if(window.location.hash.length > 1){
+            options["locus"] = window.location.hash.substr(1);
+        }
+
+        var igvDiv = document.getElementById("igv-div");
+
+        igv.createBrowser(igvDiv, options)
+            .then(function (browser) {
+                console.log("Created IGV browser");
+                for(name in tracks){
+                    if(tracks[name].show){
+                        browser.loadTrack(tracks[name]);
+                    }
+                }
+        });
+        
+
+        createTrackNavigator();
+
+        showSequence = function(){
+            var complement = {'A': 'T', 'C': 'G', 'G': 'C', 'T': 'A'};
+            var referenceFrame;
+
+            referenceFrame = igv.browser.genomicStateList[0].referenceFrame;
+            var start = referenceFrame.start;
+            var chrName = referenceFrame.chrName;
+            var end = referenceFrame.start + 
+                referenceFrame.bpPerPixel * (igv.browser.viewportContainerWidth() / igv.browser.genomicStateList.length);
+            end = Math.floor(end);
+            var locus;
+            if((end - start) > 100000){
+                alert("Sequence too long");
+                return;
+            }
+
+            igv.browser.genome.sequence.getSequence(chrName, start, end).then(function(seq){
+                seq = seq.toUpperCase();
+                locus = "Locus: " + chrName + ":" + start + "-" + end;
+                document.getElementById("sequence-locus").innerHTML = locus;
+                if(document.getElementById("reverse-sequence").checked){
+                    seq = seq.split("").map(function(cv){
+                        return complement[cv];
+                    }).join("");
+                    seq = seq.split("").reverse().join("");
+                }
+                document.getElementById("sequence-box").value = seq;
+            });
+        }
+
+        
+        document.getElementById("show-sequence").addEventListener("click", function(event){
+            showSequence();
+        });
+
+        document.getElementById("copy-sequence").addEventListener("click", function(event){
+            showSequence();
+            document.getElementById("sequence-box").select();
+            document.execCommand('copy');
+        });
+
+        document.getElementById("config-data-min").addEventListener("change", function(event){
+            var value = parseFloat(event.target.value);
+            if(isNaN(value)){
+                alert("Error: minimum value should be a number");
+            }else{
+                applyForAllTracks(function(track){ track.min = value; });
+                updateTrackViews(["data_range"]);
+            }
+        });
+
+        document.getElementById("config-data-max").addEventListener("change", function(event){
+            var value = parseFloat(event.target.value);
+            if(isNaN(value)){
+                alert("Error: maximum value should be a number");
+            }else{
+                applyForAllTracks(function(track){ track.max = value; });
+                updateTrackViews(["data_range"]);
+            }
+        });
+
+        document.getElementById("config-autoscale").addEventListener("change", function(event){
+            applyForAllTracks(function(track){
+                if(!track.autoscale){
+                    track.min = parseFloat(document.getElementById("config-data-min").value);
+                    track.max = parseFloat(document.getElementById("config-data-max").value);
+                }
+                track.autoscale = event.target.checked;
+            });
+            updateTrackViews(["data_range"]);
+        });
+
+        document.getElementById("config-height").addEventListener("change", function(event){
+            var value = parseInt(event.target.value);
+            if(isNaN(value)){
+                alert("Error: track height should be a number");
+            }else {
+                applyForAllTracks(function(track){ track.height = event.target.value; });
+                updateTrackViews(["height"]);
+            }
+        });
+
+    });
+
+</script>
+
+</body>
+
+</html>