1136 lines
38 KiB
JavaScript
1136 lines
38 KiB
JavaScript
class iGeneIndexer {
|
|
|
|
#parsedTSV = null;
|
|
#genome = null;
|
|
#pam = null;
|
|
#helper = null;
|
|
#currentDataLength = 0;
|
|
#pamOutDistances = null;
|
|
#pamInDistances = null;
|
|
#pamNoneDistances = null;
|
|
#chromosome = null;
|
|
#minIndex = null;
|
|
#maxIndex = null;
|
|
#genomeInterestingExtract = null;
|
|
|
|
static restrictionEnzymes = null;
|
|
static {
|
|
iGeneIndexer.fetchAndParseAllRestrictionEnzymesFileFromREBASE().then(reArray => {
|
|
iGeneIndexer.restrictionEnzymes = reArray;
|
|
}).catch(err => alert(err));
|
|
}
|
|
|
|
constructor(parsedTSV, genome, pam, helper) {
|
|
this.#parsedTSV = parsedTSV;
|
|
this.#genome = genome;
|
|
this.#pam = pam;
|
|
this.#helper = helper;
|
|
|
|
this.#currentDataLength = parsedTSV.length;
|
|
|
|
// Preparing distance matrices
|
|
this.#pamOutDistances = new Array(this.#currentDataLength);
|
|
this.#pamInDistances = new Array(this.#currentDataLength);
|
|
this.#pamNoneDistances = new Array(this.#currentDataLength);
|
|
for(var i = 0; i < this.#currentDataLength; i++) {
|
|
this.#pamOutDistances[i] = new Array(this.#currentDataLength);
|
|
this.#pamOutDistances[i].fill(NaN);
|
|
this.#pamInDistances[i] = new Array(this.#currentDataLength);
|
|
this.#pamInDistances[i].fill(NaN);
|
|
this.#pamNoneDistances[i] = new Array(this.#currentDataLength);
|
|
this.#pamNoneDistances[i].fill(NaN);
|
|
}
|
|
|
|
this.#parsedTSV.forEach((arrayItem, aiIndex) => {
|
|
const rank = this.getHelper().hasRank() ? this.getHelper().getRank(arrayItem) : aiIndex + 1;
|
|
|
|
// Make every entry to have a distance of zero from itself
|
|
this.#pamOutDistances[rank - 1][rank - 1] = 0;
|
|
this.#pamInDistances[rank - 1][rank - 1] = 0;
|
|
this.#pamNoneDistances[rank - 1][rank - 1] = 0;
|
|
|
|
const sequence = this.getHelper().getSequence(arrayItem);
|
|
const chromosome = this.getHelper().hasChromosome() ? this.getHelper().getChromosome(arrayItem) : this.getHelper().getDefaultChromosome();
|
|
const index = this.getHelper().getIndex(arrayItem);
|
|
const strand = this.getHelper().getStrand(arrayItem);
|
|
|
|
parsedTSV.forEach((comparedEntry, ceIndex) => {
|
|
const comparedEntryRank = this.getHelper().hasRank() ? this.getHelper().getRank(comparedEntry) : ceIndex + 1;
|
|
if (comparedEntryRank == rank) {
|
|
// Skipping comparison with itself
|
|
return;
|
|
}
|
|
const comparedEntrySequence = this.getHelper().getSequence(comparedEntry);
|
|
const comparedEntryStrand = this.getHelper().getStrand(comparedEntry);
|
|
const comparedEntryChromosome = this.getHelper().hasChromosome() ? this.getHelper().getChromosome(comparedEntry) : this.getHelper().getDefaultChromosome();
|
|
const comparedEntryIndex = this.getHelper().getIndex(comparedEntry);
|
|
// Checking chromosome, should be an impossible case but...
|
|
if (chromosome != null && comparedEntryChromosome != null && chromosome != comparedEntryChromosome) {
|
|
console.error("Different chromosome in genomic location, expected: " + chromosome + ", found: " + comparedEntryChromosome)
|
|
return;
|
|
}
|
|
|
|
this.#pamOutDistances[rank - 1][comparedEntryRank - 1] = this.computePamOutDistance(index, strand, sequence, comparedEntryIndex, comparedEntryStrand, comparedEntrySequence);
|
|
this.#pamInDistances[rank - 1][comparedEntryRank - 1] = this.computePamInDistance(index, strand, sequence, comparedEntryIndex, comparedEntryStrand, comparedEntrySequence);
|
|
this.#pamNoneDistances[rank - 1][comparedEntryRank - 1] = this.computePamNoneDistance(index, strand, sequence, comparedEntryIndex, comparedEntryStrand, comparedEntrySequence);
|
|
});
|
|
|
|
if (this.#chromosome == null) {
|
|
this.#chromosome = chromosome;
|
|
}
|
|
if (this.#minIndex == null || index < this.#minIndex) {
|
|
this.#minIndex = index;
|
|
}
|
|
if (this.#maxIndex == null || index > this.#maxIndex) {
|
|
this.#maxIndex = index;
|
|
}
|
|
});
|
|
this.fetchActualGenomePartFromUSCS().then(dna => {
|
|
this.#genomeInterestingExtract = dna;
|
|
}).catch(err => alert(err));
|
|
|
|
}
|
|
|
|
getGenome() {
|
|
return this.#genome;
|
|
}
|
|
|
|
getChromosome() {
|
|
return this.#chromosome;
|
|
}
|
|
|
|
getPAM() {
|
|
return this.#pam;
|
|
}
|
|
|
|
getHelper() {
|
|
return this.#helper;
|
|
}
|
|
|
|
getMinIndex() {
|
|
return this.#minIndex;
|
|
}
|
|
|
|
getMaxIndex() {
|
|
return this.#maxIndex;
|
|
}
|
|
|
|
getGenomeInterestingExtract() {
|
|
return this.#genomeInterestingExtract;
|
|
}
|
|
|
|
computeStartIndex(index, strand, sequence) {
|
|
if (strand == '+') {
|
|
return index;
|
|
} else {
|
|
return (index + sequence.length + (this.getHelper().hasPAMInSequence() ? 0 : this.#pam.length)) - 1;
|
|
}
|
|
}
|
|
|
|
computeEndIndex(index, strand, sequence) {
|
|
if (strand == '+') {
|
|
return (index + sequence.length - (this.getHelper().hasPAMInSequence() ? this.#pam.length : 0)) - 1;
|
|
} else {
|
|
return (index + (this.getHelper().hasPAMInSequence() ? this.#pam.length : 0)) - 1; // The PAM are the first n basis and have to be subtracted in this case
|
|
}
|
|
}
|
|
|
|
computePamOutDistance(index, strand, sequence, comparedEntryIndex, comparedEntryStrand, comparedEntrySequence) {
|
|
if (strand != comparedEntryStrand) {
|
|
const start = this.computeStartIndex(index, strand, sequence);
|
|
const comparedStart = this.computeStartIndex(comparedEntryIndex, comparedEntryStrand, comparedEntrySequence);
|
|
|
|
var distance = 0;
|
|
if (strand == '+') {
|
|
distance = start - comparedStart;
|
|
} else {
|
|
distance = comparedStart - start;
|
|
}
|
|
if (distance > 0) {
|
|
return distance;
|
|
}
|
|
}
|
|
return NaN;
|
|
}
|
|
|
|
computePamInDistance(index, strand, sequence, comparedEntryIndex, comparedEntryStrand, comparedEntrySequence) {
|
|
if (strand != comparedEntryStrand) {
|
|
const end = this.computeEndIndex(comparedEntryIndex, comparedEntryStrand, comparedEntrySequence);
|
|
const comparedEnd = this.computeEndIndex(index, strand, sequence);
|
|
|
|
var distance = 0;
|
|
if (strand == '+') {
|
|
distance = comparedEnd - end;
|
|
} else {
|
|
distance = end - comparedEnd;
|
|
}
|
|
if (distance > 0) {
|
|
return distance;
|
|
}
|
|
}
|
|
return NaN;
|
|
}
|
|
|
|
computePamNoneDistance(index, strand, sequence, comparedEntryIndex, comparedEntryStrand, comparedEntrySequence) {
|
|
const firstStart = this.computeStartIndex(index, strand, sequence);
|
|
const firstEnd = this.computeEndIndex(index, strand, sequence);
|
|
const secondStart = this.computeStartIndex(comparedEntryIndex, comparedEntryStrand, comparedEntrySequence);
|
|
const secondEnd = this.computeEndIndex(comparedEntryIndex, comparedEntryStrand, comparedEntrySequence);
|
|
|
|
return Math.max(firstStart, firstEnd, secondStart, secondEnd) - Math.min(firstStart, firstEnd, secondStart, secondEnd);
|
|
}
|
|
|
|
getMatchesOf(distancesMatrix, rank, distance) {
|
|
const matches = new Map();
|
|
for (var i = 0; i < this.#currentDataLength; i++) {
|
|
const currentDistance = distancesMatrix[rank - 1][i];
|
|
if ((i + 1) != rank && currentDistance != NaN && currentDistance > 0 && currentDistance <= distance) {
|
|
matches.set(i + 1, currentDistance);
|
|
}
|
|
}
|
|
return matches;
|
|
}
|
|
|
|
getPamOutMatches(rank, distance) {
|
|
return this.getMatchesOf(this.#pamOutDistances, rank, distance);
|
|
}
|
|
|
|
getPamInMatches(rank, distance) {
|
|
return this.getMatchesOf(this.#pamInDistances, rank, distance);
|
|
}
|
|
|
|
getNoneMatches(rank, distance) {
|
|
return this.getMatchesOf(this.#pamNoneDistances, rank, distance);
|
|
}
|
|
|
|
evaluateMatches(rank, restrictionEnzymeSequence, distance) {
|
|
const minIndex = this.getMinIndex();
|
|
// Getting the RE matches relative index and mapping to their absolute index by adding the chromosome extract start index (the minIndex)
|
|
const reMatchesIndexes = this.getAllMatches(
|
|
iGeneIndexer.iub2regexp(restrictionEnzymeSequence),
|
|
this.getGenomeInterestingExtract()
|
|
).map(m => {return {index: minIndex + m.index, value: m.value}; });
|
|
|
|
const reReverseStrandMatchesIndexes = this.getAllMatches(
|
|
iGeneIndexer.iub2regexp(restrictionEnzymeSequence),
|
|
iGeneIndexer.reverseComplementOf(this.getGenomeInterestingExtract())
|
|
).map(m => {return {index: minIndex + m.index + m.value.length, value: m.value}; });
|
|
|
|
const matches = [];
|
|
let gRNA = null;
|
|
this.#parsedTSV.forEach((arrayItem, aiIndex) => {
|
|
const aiRank = this.getHelper().hasRank() ? this.getHelper().getRank(arrayItem) : aiIndex + 1;
|
|
if (rank == aiRank) {
|
|
gRNA = arrayItem;
|
|
}
|
|
});
|
|
const sequence = this.getHelper().getSequence(gRNA);
|
|
const index = this.getHelper().getIndex(gRNA);
|
|
const strand = this.getHelper().getStrand(gRNA);
|
|
|
|
const start = this.computeStartIndex(index, strand, sequence);
|
|
const end = this.computeEndIndex(index, strand, sequence);
|
|
if (strand == '+') {
|
|
reMatchesIndexes.forEach((reMatch) => {
|
|
const reStartDistanceFromEnd = reMatch.index - end;
|
|
if (reStartDistanceFromEnd > 0 && reStartDistanceFromEnd <= distance) {
|
|
matches.push({chromosome: this.#chromosome, index: reMatch.index, sequence: reMatch.value, strand: '+', distance: reStartDistanceFromEnd});
|
|
}
|
|
const reEndDistanceFromStart = start - (reMatch.index + reMatch.value.length);
|
|
if (reEndDistanceFromStart > 0 && reEndDistanceFromStart <= distance) {
|
|
matches.push({chromosome: this.#chromosome, index: reMatch.index, sequence: reMatch.value, strand: '+', distance: -1 * reEndDistanceFromStart});
|
|
}
|
|
});
|
|
} else {
|
|
reReverseStrandMatchesIndexes.forEach((reRSMatch) => {
|
|
const reStartDistanceFromEnd = end - (reRSMatch.index + reRSMatch.value.length);
|
|
if (reStartDistanceFromEnd > 0 && reStartDistanceFromEnd <= distance) {
|
|
matches.push({chromosome: this.#chromosome, index: reRSMatch.index, sequence: reRSMatch.value, strand: '+', distance: reStartDistanceFromEnd});
|
|
}
|
|
const reEndDistanceFromStart = reRSMatch.index - start;
|
|
if (reEndDistanceFromStart > 0 && reEndDistanceFromStart <= distance) {
|
|
matches.push({chromosome: this.#chromosome, index: reRSMatch.index, sequence: reRSMatch.value, strand: '+', distance: -1 * reEndDistanceFromStart});
|
|
}
|
|
});
|
|
}
|
|
return matches;
|
|
}
|
|
|
|
getAllMatches(searchStr, str) {
|
|
var searchStrLen = searchStr.length;
|
|
if (searchStrLen == 0) {
|
|
return [];
|
|
}
|
|
const regEx = new RegExp(searchStr, "g");
|
|
const matches = [];
|
|
let match;
|
|
while ((match = regEx.exec(str)) !== null) {
|
|
matches.push({index: match.index, value: match[0]});
|
|
}
|
|
return matches;
|
|
}
|
|
|
|
async fetchActualGenomePartFromUSCS() {
|
|
return this.fetchGenomePartFromUSCS(this.getGenome(), this.getChromosome(), this.getMinIndex(), this.getMaxIndex());
|
|
}
|
|
|
|
/*
|
|
Respone JSON example:
|
|
{
|
|
"downloadTime": "2023:05:23T16:54:33Z",
|
|
"downloadTimeStamp": 1684860873,
|
|
"genome": "danRer10",
|
|
"chrom": "chr15",
|
|
"start": 43776601,
|
|
"end": 43794868,
|
|
"dna": "GTGCGCCGGA..."
|
|
}
|
|
*/
|
|
async fetchGenomePartFromUSCS(genome, chromosome, start, end) {
|
|
const resp = await iGeneIndexer.fetchViaCORSProxy('https://genome-euro.ucsc.edu/cgi-bin/hubApi/getData/sequence?genome=' + genome + '&chrom=' + chromosome + '&start=' + start + '&end=' + end);
|
|
if (resp.ok) {
|
|
let json = await resp.json();
|
|
return json.dna;
|
|
} else {
|
|
let json = await resp.json();
|
|
throw "Error downloading genome part from USCS ([" + json.statusCode + ' - ' + json.statusMessage + '] - ' + json.error.split(' for endpoint')[0] + ')';
|
|
}
|
|
}
|
|
|
|
/*
|
|
Respone JSON example:
|
|
{
|
|
"downloadTime": "2023:05:23T17:11:47Z",
|
|
"downloadTimeStamp": 1684861907,
|
|
"genome": "danRer10",
|
|
"dataTime": "2015-01-22T19:35:19",
|
|
"dataTimeStamp": 1421951719,
|
|
"chromCount": 1061,
|
|
"chromosomes": {
|
|
"chr4": 76625712,
|
|
"chrUn_KN150525v1": 650,
|
|
"chrUn_KN150247v1": 728,
|
|
[...]
|
|
"chr3": 62385949,
|
|
"chr5": 71715914,
|
|
"chr7": 74082188
|
|
}
|
|
}
|
|
*/
|
|
static async fetchGenomeChromosomesFromUSCS(genome) {
|
|
const resp = await iGeneIndexer.fetchViaCORSProxy('https://genome-euro.ucsc.edu/cgi-bin/hubApi/list/chromosomes?genome=' + genome);
|
|
if (resp.ok) {
|
|
let json = await resp.json();
|
|
return json.chromosomes;
|
|
} else {
|
|
throw "Error downloading '" + genome + "' genome's chromosomes from USCS";
|
|
}
|
|
}
|
|
|
|
static async loadAllRestrictionEnzymesFileFromREBASE() {
|
|
const allEnzymesResponse = await iGeneIndexer.fetchViaCORSProxy("http://rebase.neb.com/rebase/link_allenz");
|
|
if (allEnzymesResponse.ok) {
|
|
return await allEnzymesResponse.text();
|
|
} else {
|
|
const message = await allEnzymesResponse.text();
|
|
throw "Error downloading Restriction Enzymes list file from REBASE\n\n[" + allEnzymesResponse.status + (allEnzymesResponse.status == 404 ? ' - Not found' : '') + '] Message from service: ' + message;
|
|
}
|
|
}
|
|
|
|
static async fetchAndParseAllRestrictionEnzymesFileFromREBASE() {
|
|
return await this.loadAllRestrictionEnzymesFileFromREBASE().then(allEnzymesText => {
|
|
const enzymesSequence = allEnzymesText.substring(allEnzymesText.indexOf("<1>")).split("References:\n")[0].trim();
|
|
const enzymesToParseLines = enzymesSequence.split("\n");
|
|
const enzymes = [];
|
|
for (let i = 0; i < enzymesToParseLines.length; i += 9) {
|
|
enzymes.push({name: enzymesToParseLines[i].substring(3), sequence: enzymesToParseLines[i + 4].substring(3) });
|
|
}
|
|
return enzymes;
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* IUB2regexp
|
|
*
|
|
* A function that, given a sequence with IUB ambiguity codes,
|
|
* outputs a translation with IUB codes changed to a regular expression
|
|
*
|
|
* It is based on the IUB ambiguity codes:
|
|
* (Eur. J. Biochem. 150: 1-5, 1985):
|
|
* R = G or A
|
|
* Y = C or T
|
|
* M = A or C
|
|
* K = G or T
|
|
* S = G or C
|
|
* W = A or T
|
|
* B = not A (C or G or T)
|
|
* D = not C (A or G or T)
|
|
* H = not G (A or C or T)
|
|
* V = not T (A or C or G)
|
|
* N = A or C or G or T
|
|
*
|
|
*/
|
|
static iub2regexp(iub) {
|
|
|
|
const iub2characterOrRegexp = {
|
|
'A' : 'A',
|
|
'C' : 'C',
|
|
'G' : 'G',
|
|
'T' : 'T',
|
|
'R' : '[GA]',
|
|
'Y' : '[CT]',
|
|
'M' : '[AC]',
|
|
'K' : '[GT]',
|
|
'S' : '[GC]',
|
|
'W' : '[AT]',
|
|
'B' : '[CGT]',
|
|
'D' : '[AGT]',
|
|
'H' : '[ACT]',
|
|
'V' : '[ACG]',
|
|
'N' : '[ACGT]'
|
|
};
|
|
|
|
// Remove the ^ signs from the recognition sites
|
|
iub = iub.replace('\^', '');
|
|
|
|
let regularExpression = '';
|
|
// Translate each character in the iub sequence
|
|
[...iub].forEach(c => regularExpression += iub2characterOrRegexp[c]);
|
|
|
|
return regularExpression;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the complement of a base, cosnidering the complete IUB notations
|
|
*
|
|
* Info source: https://arep.med.harvard.edu/labgc/adnan/projects/Utilities/revcomp.html
|
|
*
|
|
* @param {*} base the base to complement in IUB notation
|
|
* @returns the complement base (e.g.: For 'A' returns 'T')
|
|
*/
|
|
static complement(base) {
|
|
return { A: 'T', T: 'A', G: 'C', C: 'G', Y: 'R', R: 'Y', S: 'S', W: 'W', K: 'M', M: 'K', B: 'V', D: 'H', H: 'D', V: 'B', N: 'N'}[base];
|
|
}
|
|
|
|
/**
|
|
* Converts a sequence to its reverse complement, considering the complete IUB notations by using the `complement(base)` function.
|
|
* @param {*} sequence the sequence to be converted
|
|
* @returns the reverse complement of the sequence
|
|
*/
|
|
static reverseComplementOf(sequence) {
|
|
return sequence.split('').reverse().map(iGeneIndexer.complement).join('');
|
|
}
|
|
|
|
static fetchViaCORSProxy(url) {
|
|
return fetch("https://corsproxy.io/?" + encodeURIComponent(url));
|
|
}
|
|
}
|
|
|
|
class AbstractImporter extends HTMLElement {
|
|
|
|
#rootdoc = null;
|
|
#headers = null
|
|
#results = null;
|
|
#indexer = null;
|
|
#helper = null;
|
|
|
|
constructor(helper){
|
|
super()
|
|
this.#helper = helper;
|
|
this.#rootdoc = this.attachShadow({ "mode" : "open"})
|
|
this.render()
|
|
}
|
|
|
|
getRootDoc() {
|
|
return this.#rootdoc;
|
|
}
|
|
|
|
render(){
|
|
this.getRootDoc().innerHTML = ``;
|
|
}
|
|
|
|
parseTSV(tsvString) {
|
|
const results = tsvString.split("\n");
|
|
this.#headers = results[0].split("\t");
|
|
this.#results = [];
|
|
results.forEach((line, i)=>{ if(i > 0 && line.trim().length > 0) this.#results.push(line.split("\t")) });
|
|
}
|
|
|
|
importTSVExport(file, genome, pam) {
|
|
const reader = new FileReader();
|
|
reader.addEventListener("load", () => {
|
|
this.parseTSV(reader.result);
|
|
this.setIndexer(new iGeneIndexer(this.#results, genome, pam, this.getHelper()));
|
|
this.enableData();
|
|
})
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
importTSVURL(tsvURL, genome, pam) {
|
|
fetch(tsvURL).then(resp => {
|
|
if (resp.ok) {
|
|
resp.text().then(textData => {
|
|
this.parseTSV(textData);
|
|
this.setIndexer(new iGeneIndexer(this.#results, genome, pam, this.getHelper()));
|
|
this.enableData();
|
|
})
|
|
} else {
|
|
resp.text().then(page => {
|
|
throw "Error loading TSV from URL: " + tsvURL + " (" + page + ")";
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
enableData() {
|
|
}
|
|
|
|
#API
|
|
setIndexer(indexer) {
|
|
this.#indexer = indexer;
|
|
}
|
|
|
|
getIndexer() {
|
|
return this.#indexer;
|
|
}
|
|
|
|
getHelper() {
|
|
return this.#helper;
|
|
}
|
|
|
|
getHeader() {
|
|
// Abstract, to be ovverrided in subclass
|
|
}
|
|
|
|
getRenderer(filter, output){
|
|
if (filter.mode == 're') {
|
|
if(output === "html") {
|
|
return new DefaultHTMLRETableRenderer(this, filter);
|
|
} else if(output === "tsv") {
|
|
return alert("This is not supported in R.E. mode");
|
|
} else if(output === "tsv2") {
|
|
return new DefaultTSVRETableRenderer(this, filter, "tsv2");
|
|
}
|
|
} else {
|
|
if(output === "html") {
|
|
return new DefaultHTMLDimerTableRenderer(this, filter);
|
|
} else if(output === "tsv") {
|
|
return new DefaultTSVDimerTableRenderer(this, filter);
|
|
} else if(output === "tsv2") {
|
|
return new DefaultTSVDimerTableRenderer(this, filter, "tsv2");
|
|
}
|
|
}
|
|
}
|
|
|
|
setTableHeaders(headers){
|
|
this.#headers = headers;
|
|
}
|
|
|
|
getTableHeaders(){
|
|
return this.#headers;
|
|
}
|
|
|
|
hasRank() {
|
|
return this.getHelper().hasRank();
|
|
}
|
|
|
|
getRank(line) {
|
|
return this.getHelper().getRank(line);
|
|
}
|
|
|
|
setTableLines(results){
|
|
this.#results = results;
|
|
}
|
|
|
|
getTableLines(){
|
|
return this.#results;
|
|
}
|
|
|
|
getTableLine(i){
|
|
return this.#results[i];
|
|
}
|
|
|
|
getTableLineByRank(r){
|
|
return this.#results[r-1];
|
|
}
|
|
|
|
getMatches(pam, rank, distance){
|
|
if(pam === "in"){
|
|
return this.getIndexer().getPamInMatches(rank, distance)
|
|
} else if(pam === "out"){
|
|
return this.getIndexer().getPamOutMatches(rank, distance)
|
|
} else {
|
|
var m = new Map();
|
|
this.getIndexer().getPamInMatches(rank, distance).forEach((v, k) => { m.set(k, v) })
|
|
this.getIndexer().getPamOutMatches(rank, distance).forEach((v, k) => { m.set(k, v) })
|
|
this.getIndexer().getNoneMatches(rank, distance).forEach((v, k) => { m.set(k, v) })
|
|
return m;
|
|
}
|
|
}
|
|
|
|
getREMatchesForRankAt(rank, re, distance) {
|
|
return this.getIndexer().evaluateMatches(rank, re, distance);
|
|
}
|
|
|
|
getDatadisplay() {
|
|
return document.querySelector("igene-data-display");
|
|
}
|
|
}
|
|
|
|
class AbstractHelper {
|
|
#defaultChromosome
|
|
constructor() {
|
|
if (this.constructor == AbstractHelper) {
|
|
throw new Error("AbstractHelper class can't be instantiated");
|
|
}
|
|
}
|
|
hasPAMInSequence() {
|
|
throw new Error("Method 'hasPAMInSequence' must be implementd");
|
|
}
|
|
hasRank() {
|
|
throw new Error("Method 'hasRank' must be implementd");
|
|
}
|
|
getRank(line) {
|
|
throw new Error("Method 'getRank' must be implementd");
|
|
}
|
|
getSequence(line) {
|
|
throw new Error("Method 'getTargetSequence' must be implementd");
|
|
}
|
|
hasChromosome() {
|
|
throw new Error("Method 'hasChromosome' must be implementd");
|
|
}
|
|
getChromosome(line) {
|
|
throw new Error("Method 'getChromosome' must be implementd");
|
|
}
|
|
getIndex(line) {
|
|
throw new Error("Method 'getIndex' must be implementd");
|
|
}
|
|
getStrand(line) {
|
|
throw new Error("Method 'getStrand' must be implementd");
|
|
}
|
|
setDefaultChromosome(chromosome) {
|
|
this.#defaultChromosome = chromosome;
|
|
}
|
|
getDefaultChromosome() {
|
|
return this.#defaultChromosome;
|
|
}
|
|
}
|
|
|
|
class ChopChopHelper extends AbstractHelper {
|
|
hasPAMInSequence() {
|
|
return true;
|
|
}
|
|
hasRank() {
|
|
return true;
|
|
}
|
|
getRank(line) {
|
|
return +line[0];
|
|
}
|
|
getSequence(line) {
|
|
return line[1];
|
|
}
|
|
hasChromosome() {
|
|
return true;
|
|
}
|
|
getChromosome(line) {
|
|
return line[2].split(':')[0];
|
|
}
|
|
getIndex(line) {
|
|
return +line[2].split(':')[1];
|
|
}
|
|
getStrand(line) {
|
|
return line[3];
|
|
}
|
|
}
|
|
|
|
class CrisprscanHelper extends AbstractHelper {
|
|
constructor() {
|
|
super();
|
|
}
|
|
hasPAMInSequence() {
|
|
return true;
|
|
}
|
|
hasRank() {
|
|
return false;
|
|
}
|
|
getSequence(line) {
|
|
return line[5];
|
|
}
|
|
hasChromosome() {
|
|
return false;
|
|
}
|
|
getChromosome(line) {
|
|
return null;
|
|
}
|
|
getIndex(line) {
|
|
return +line[1];
|
|
}
|
|
getStrand(line) {
|
|
return line[3];
|
|
}
|
|
}
|
|
|
|
class AbstractTableRenderer{
|
|
|
|
constructor(){}
|
|
|
|
renderTable(){
|
|
alert("Not yet implemented")
|
|
}
|
|
|
|
renderHeaders(){
|
|
alert("Not yet implemented")
|
|
}
|
|
|
|
renderRows(){
|
|
alert("Not yet implemented")
|
|
}
|
|
|
|
renderMatchRows(){
|
|
alert("Not yet implemented")
|
|
}
|
|
}
|
|
|
|
class DefaultHTMLDimerTableRenderer extends AbstractTableRenderer {
|
|
#source = null
|
|
#output = ""
|
|
#filter = null
|
|
|
|
constructor(source, filter){
|
|
super()
|
|
this.#source = source
|
|
this.#filter = filter
|
|
}
|
|
|
|
getOutput(){
|
|
return this.#output
|
|
}
|
|
|
|
renderTable(){
|
|
const s = this.#source
|
|
this.#output = `
|
|
<table class="table table-bordered">
|
|
<thead>
|
|
${this.renderHeaders()}
|
|
</thead>
|
|
<tbody>
|
|
${this.renderRows()}
|
|
</tbody>
|
|
</table>
|
|
`
|
|
return this.#output
|
|
}
|
|
|
|
renderHeaders(){
|
|
const s = this.#source
|
|
return `
|
|
<tr class="table-dark">
|
|
${s.hasRank() ? `` : `<th>Rank</th>`}${s.getTableHeaders().reduce((acc, h)=>`${acc}<th>${h}</th>`, "")}
|
|
</tr>
|
|
`
|
|
}
|
|
|
|
renderRows(){
|
|
const s = this.#source
|
|
const rows = s.getTableLines()
|
|
var outputs = []
|
|
const suffix = "</tr>"
|
|
for(let i=0; i < rows.length; i++){
|
|
const row = rows[i]
|
|
const rankedrow = s.hasRank() ? row : [i + 1].concat(row);
|
|
var prefix = ""
|
|
var mrows = []
|
|
const matches = s.getMatches(this.#filter.mode, rankedrow[0], this.#filter.distance)
|
|
const tds = rankedrow.reduce((acc, d, i)=>{
|
|
if(i === 0) return acc + `<td class="rank-td"><div class="d-flex justify-content-between"><span>${d}</span><span title="match count" class="badge bg-primary">${matches.size}</span></div></td>`;
|
|
return acc + `<td><span>${d}</span></td>`
|
|
}, "")
|
|
if(matches.size === 0 && !this.#filter.zeromatches){
|
|
prefix = `<tr class="d-none table-primary table-row-nomatch" data-rank="${rankedrow[0]}">`
|
|
} else if(matches.size > 0){
|
|
prefix = `<tr class="table-primary table-row-match" data-rank="${rankedrow[0]}">`
|
|
mrows = this.renderMatchRows(rankedrow, matches)
|
|
}
|
|
outputs.push(prefix + tds + suffix)
|
|
outputs = outputs.concat(mrows)
|
|
}
|
|
return outputs.join("");
|
|
}
|
|
|
|
renderMatchRows(pivot, matches){
|
|
const s = this.#source
|
|
var prefix = `<tr class="table-light d-none" data-rel-rank="${pivot[0]}">`
|
|
const outputs = []
|
|
for (const [key, value] of matches) {
|
|
const row = s.getTableLineByRank(key)
|
|
const rankedrow = s.hasRank() ? row : [key].concat(row);
|
|
if(row){
|
|
const tds = row.reduce((acc, d, ind)=>{
|
|
if(ind === 0) return acc + `<td class="rank-td"><div class="d-flex" style="padding-left:5px;justify-content:space-between"><span>${key}</span><span title="distance" class="badge bg-secondary">${value}</span></div></td>`;
|
|
return acc + `<td><span>${d}</span></td>`
|
|
}, prefix)
|
|
outputs.push(tds + `</tr>`)
|
|
}
|
|
|
|
}
|
|
return outputs
|
|
}
|
|
}
|
|
|
|
class CrisprScanHTMLDimerTableRenderer extends DefaultHTMLDimerTableRenderer {
|
|
|
|
#source = null
|
|
|
|
constructor(source, filter){
|
|
super(source, filter)
|
|
this.#source = source
|
|
}
|
|
|
|
renderMatchRows(pivot, matches){
|
|
const s = this.#source
|
|
var prefix = `<tr class="table-light d-none" data-rel-rank="${pivot[0]}">`
|
|
const outputs = []
|
|
for (const [key, value] of matches) {
|
|
const row = s.getTableLineByRank(key)
|
|
const rankedrow = s.hasRank() ? row : [key].concat(row);
|
|
if(row){
|
|
const tds = row.reduce((acc, d, ind)=>{
|
|
if(ind === 0) return acc + `
|
|
<td class="rank-td"><div class="d-flex" style="padding-left:5px;justify-content:space-between"><span>${key}</span><span title="distance" class="badge bg-secondary">${value}</span></div></td>
|
|
<td><!--empty cell for aligning with name attribute--></td>
|
|
`;
|
|
return acc + `<td><span>${d}</span></td>`
|
|
}, prefix)
|
|
outputs.push(tds + `</tr>`)
|
|
}
|
|
|
|
}
|
|
return outputs
|
|
}
|
|
}
|
|
|
|
class DefaultHTMLRETableRenderer extends AbstractTableRenderer {
|
|
#source = null
|
|
#output = ""
|
|
#filter = null
|
|
|
|
constructor(source, filter){
|
|
super(source, filter)
|
|
this.#source = source
|
|
this.#filter = filter
|
|
}
|
|
|
|
getOutput(){
|
|
return this.#output
|
|
}
|
|
|
|
renderTable(){
|
|
if (this.#filter.enzyme != '') {
|
|
const s = this.#source
|
|
this.#output = `
|
|
<table class="table table-bordered">
|
|
<thead>
|
|
${this.renderHeaders()}
|
|
</thead>
|
|
<tbody>
|
|
${this.renderRows()}
|
|
</tbody>
|
|
</table>
|
|
`
|
|
return this.#output
|
|
} else {
|
|
return `<h5>Please, select the restriction enzyme from the list</h5>`;
|
|
}
|
|
}
|
|
|
|
renderHeaders(){
|
|
const s = this.#source
|
|
return `
|
|
<tr class="table-dark">
|
|
${s.hasRank() ? `` : `<th>Rank</th>`}${s.getTableHeaders().reduce((acc, h)=>`${acc}<th>${h}</th>`, "")}
|
|
</tr>
|
|
`
|
|
}
|
|
|
|
renderRows(){
|
|
const s = this.#source
|
|
const rows = s.getTableLines()
|
|
var outputs = []
|
|
const suffix = "</tr>"
|
|
for(let i=0; i < rows.length; i++){
|
|
const row = rows[i]
|
|
const rankedrow = s.hasRank() ? row : [i + 1].concat(row);
|
|
var prefix = ""
|
|
var mrows = []
|
|
const matches = s.getREMatchesForRankAt(rankedrow[0], this.#filter.enzyme, this.#filter.distance);
|
|
const tds = rankedrow.reduce((acc, d, i)=>{
|
|
if(i === 0) return acc + `<td class="rank-td"><div class="d-flex justify-content-between"><span>${d}</span><span title="match count" class="badge bg-primary">${matches.length}</span></div></td>`;
|
|
return acc + `<td><span>${d}</span></td>`
|
|
}, "")
|
|
if(matches.length === 0 && !this.#filter.zeromatches){
|
|
prefix = `<tr class="d-none table-primary table-row-nomatch" data-rank="${rankedrow[0]}">`
|
|
} else if(matches.length > 0){
|
|
prefix = `<tr class="table-primary table-row-match" data-rank="${rankedrow[0]}">`
|
|
mrows = this.renderMatchRows(rankedrow, matches)
|
|
}
|
|
outputs.push(prefix + tds + suffix)
|
|
outputs = outputs.concat(mrows)
|
|
}
|
|
return outputs.join("")
|
|
}
|
|
|
|
renderMatchRows(pivot, matches){
|
|
const s = this.#source
|
|
var outputs = []
|
|
matches.forEach(match => {
|
|
outputs.push(
|
|
`<tr class="table-light d-none" data-rel-rank="${pivot[0]}">
|
|
<td class="rank-td"><div class="d-flex" style="padding-left:5px;justify-content:space-between"><span> </span><span title="distance" class="badge bg-secondary">${match.distance}</span></div></td>
|
|
<td colspan="5">
|
|
<span>${match.sequence}</span>
|
|
<span>at ${match.index}</span>
|
|
<span>on strand ${match.strand}</span>
|
|
</td>
|
|
</tr>`);
|
|
});
|
|
/*matches.forEach(match => {
|
|
outputs.push(
|
|
`<tr class="table-light d-none" data-rel-rank="${pivot[0]}">
|
|
<td class="rank-td"><div class="d-flex" style="padding-left:5px;justify-content:space-between"><span> </span><span title="distance" class="badge bg-secondary">${match.distance}</span></div></td>
|
|
<td><span>${match.sequence}</span></td>
|
|
<td><span>${match.index}</span></td>
|
|
<td><span>${match.strand}</span></td>
|
|
</tr>`);
|
|
});*/
|
|
return outputs
|
|
}
|
|
}
|
|
|
|
class DefaultTSVDimerTableRenderer extends AbstractTableRenderer {
|
|
#source = null
|
|
#output = ""
|
|
#filter = null
|
|
#sep = "\t"
|
|
#strategy
|
|
|
|
constructor(source, filter, strategy){
|
|
super()
|
|
this.#source = source
|
|
this.#filter = filter
|
|
this.#strategy = strategy
|
|
}
|
|
|
|
getOutput(){
|
|
return this.#output
|
|
}
|
|
|
|
renderTable(){
|
|
if(this.#strategy === "tsv2"){
|
|
this.#output = this.renderHeaders2() + "\n" + this.renderRows2()
|
|
}else{
|
|
this.#output = this.renderHeaders() + "\n" + this.renderRows()
|
|
}
|
|
return this.#output
|
|
}
|
|
|
|
renderHeaders(){
|
|
const hdrs = [...this.#source.getTableHeaders()]
|
|
const rankedhdrs = this.#source.hasRank() ? hdrs : ["Rank"].concat(hdrs)
|
|
rankedhdrs.splice(1,0,"Distance")
|
|
return rankedhdrs.join(this.#sep)
|
|
}
|
|
|
|
renderRows(){
|
|
const s = this.#source
|
|
const rows = s.getTableLines()
|
|
var outputs = []
|
|
for(let i=0; i < rows.length; i++){
|
|
const row = rows[i]
|
|
const rankedrow = s.hasRank() ? row : [i + 1].concat(row);
|
|
const matches = s.getMatches(this.#filter.mode, rankedrow[0], this.#filter.distance)
|
|
const tds = rankedrow.map((d, ind)=>{
|
|
if(ind === 0) return [`${d}${this.#sep}--------`];
|
|
return `${d}`
|
|
}).join(this.#sep)
|
|
if(matches.size === 0 && this.#filter.zeromatches){
|
|
outputs.push(tds)
|
|
} else if(matches.size > 0) {
|
|
outputs.push(tds)
|
|
outputs = outputs.concat(this.renderMatchRows(rankedrow, matches))
|
|
}
|
|
}
|
|
return outputs.join("\n")
|
|
}
|
|
|
|
renderMatchRows(pivot, matches){
|
|
const s = this.#source
|
|
var outputs = []
|
|
for (const [key, value] of matches) {
|
|
const row = s.getTableLineByRank(key)
|
|
if(row){
|
|
const rankedrow = s.hasRank() ? row : [key].concat(row);
|
|
const tds = rankedrow.map((d, ind)=>{
|
|
if(ind === 0) return `${d}${this.#sep}${value}`;
|
|
return `${d}`
|
|
}).join(this.#sep)
|
|
outputs.push(tds)
|
|
}
|
|
}
|
|
return outputs
|
|
}
|
|
|
|
renderHeaders2(){
|
|
const hdrs = [...this.#source.getTableHeaders()]
|
|
const rankedhdrs = this.#source.hasRank() ? hdrs : ["Rank"].concat(hdrs)
|
|
const outhdrs = rankedhdrs.map(h=>`${h}${this.#sep}Match ${h}`)
|
|
outhdrs.splice(1,0,"Distance")
|
|
return outhdrs.join(this.#sep)
|
|
}
|
|
|
|
renderRows2(){
|
|
const s = this.#source
|
|
const rows = s.getTableLines()
|
|
var outputs = []
|
|
for(let i=0; i < rows.length; i++){
|
|
const row = rows[i]
|
|
const rankedrow = s.hasRank() ? row : [i + 1].concat(row);
|
|
const matches = s.getMatches(this.#filter.mode, rankedrow[0], this.#filter.distance)
|
|
if(matches.size > 0){
|
|
const mrows = this.renderMatchRows2(rankedrow, matches)
|
|
outputs = outputs.concat(mrows)
|
|
}
|
|
}
|
|
return outputs.join("\n")
|
|
}
|
|
|
|
renderMatchRows2(pivot, matches){
|
|
const s = this.#source
|
|
var outputs = []
|
|
for (const [key, value] of matches) {
|
|
const row = s.getTableLineByRank(key)
|
|
if(row){
|
|
const rankedrow = s.hasRank() ? row : [key].concat(row);
|
|
const tds = rankedrow.map((d, i)=>{
|
|
if(i === 0) return `${pivot[i]}${this.#sep}${d}${this.#sep}${value}`;
|
|
return `${pivot[i]}${this.#sep}${d}`
|
|
}).join(this.#sep)
|
|
outputs.push(tds)
|
|
}
|
|
}
|
|
return outputs
|
|
}
|
|
|
|
}
|
|
|
|
class DefaultTSVRETableRenderer extends AbstractTableRenderer {
|
|
#source = null
|
|
#output = ""
|
|
#filter = null
|
|
#sep = "\t"
|
|
#strategy
|
|
|
|
constructor(source, filter, strategy){
|
|
super()
|
|
this.#source = source
|
|
this.#filter = filter
|
|
this.#strategy = strategy
|
|
}
|
|
|
|
getOutput(){
|
|
return this.#output
|
|
}
|
|
|
|
renderTable(){
|
|
if(this.#strategy === "tsv2"){
|
|
this.#output = this.renderHeaders2() + "\n" + this.renderRows2()
|
|
}else{
|
|
this.#output = this.renderHeaders() + "\n" + this.renderRows()
|
|
}
|
|
return this.#output
|
|
}
|
|
|
|
renderHeaders(){
|
|
throw "Not yet implemented"
|
|
}
|
|
|
|
renderRows(){
|
|
throw "Not yet implemented"
|
|
}
|
|
|
|
renderMatchRows(pivot, matches){
|
|
throw "Not yet implemented"
|
|
}
|
|
|
|
renderHeaders2(){
|
|
const hdrs = [...this.#source.getTableHeaders()]
|
|
const rankedhdrs = this.#source.hasRank() ? hdrs : ["Rank"].concat(hdrs)
|
|
rankedhdrs.splice(0,0,"Distance")
|
|
rankedhdrs.splice(0,0,"RE strand")
|
|
rankedhdrs.splice(0,0,"RE index")
|
|
rankedhdrs.splice(0,0,"RE sequence")
|
|
return rankedhdrs.join(this.#sep)
|
|
}
|
|
|
|
renderRows2(){
|
|
const s = this.#source
|
|
const rows = s.getTableLines()
|
|
var outputs = []
|
|
for(let i=0; i < rows.length; i++){
|
|
const row = rows[i]
|
|
const rankedrow = s.hasRank() ? row : [i + 1].concat(row);
|
|
const matches = s.getREMatchesForRankAt(rankedrow[0], this.#filter.enzyme, this.#filter.distance);
|
|
if(matches.length > 0){
|
|
const mrows = this.renderMatchRows2(rankedrow, matches)
|
|
outputs = outputs.concat(mrows)
|
|
}
|
|
}
|
|
return outputs.join("\n")
|
|
}
|
|
|
|
renderMatchRows2(pivot, matches){
|
|
const s = this.#source
|
|
var outputs = []
|
|
for (let i=0; i < matches.length; i++) {
|
|
const m = matches[i]
|
|
const tds = pivot.map((d, i)=>{
|
|
// TODO: check whether misalignment for RE matches occurs also in TSV2 exported data
|
|
if(i === 0) return `${m.sequence}${this.#sep}${m.index}${this.#sep}${m.strand}${this.#sep}${m.distance}${this.#sep}${d}`;
|
|
else return `${d}`
|
|
}).join(this.#sep)
|
|
outputs.push(tds)
|
|
}
|
|
return outputs
|
|
}
|
|
|
|
}
|
|
|
|
class CrisprScanTSVDimerTableRenderer extends DefaultTSVDimerTableRenderer {
|
|
|
|
#source
|
|
#sep = "\t"
|
|
|
|
constructor(source, filter, strategy){
|
|
super(source, filter, strategy)
|
|
this.#source = source
|
|
}
|
|
|
|
renderMatchRows2(pivot, matches){
|
|
const s = this.#source
|
|
var outputs = []
|
|
for (const [key, value] of matches) {
|
|
const row = s.getTableLineByRank(key)
|
|
if(row){
|
|
const rankedrow = s.hasRank() ? row : [key].concat(row);
|
|
const tds = rankedrow.map((d, i)=>{
|
|
if(i === 0) return `${pivot[i]}${this.#sep}${d}${this.#sep}${value}`;
|
|
return `${pivot[i]}${this.#sep}${d}`
|
|
}).join(this.#sep)
|
|
outputs.push(tds)
|
|
}
|
|
}
|
|
return outputs
|
|
}
|
|
}
|