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 = ` ${this.renderHeaders()} ${this.renderRows()}
` return this.#output } renderHeaders(){ const s = this.#source return ` ${s.hasRank() ? `` : `Rank`}${s.getTableHeaders().reduce((acc, h)=>`${acc}${h}`, "")} ` } renderRows(){ const s = this.#source const rows = s.getTableLines() var outputs = [] const suffix = "" 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 + `
${d}${matches.size}
`; return acc + `${d}` }, "") if(matches.size === 0 && !this.#filter.zeromatches){ prefix = `` } else if(matches.size > 0){ prefix = `` 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 = `` 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 + `
${key}${value}
`; return acc + `${d}` }, prefix) outputs.push(tds + ``) } } 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 = `` 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 + `
${key}${value}
`; return acc + `${d}` }, prefix) outputs.push(tds + ``) } } 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 = ` ${this.renderHeaders()} ${this.renderRows()}
` return this.#output } else { return `
Please, select the restriction enzyme from the list
`; } } renderHeaders(){ const s = this.#source return ` ${s.hasRank() ? `` : `Rank`}${s.getTableHeaders().reduce((acc, h)=>`${acc}${h}`, "")} ` } renderRows(){ const s = this.#source const rows = s.getTableLines() var outputs = [] const suffix = "" 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 + `
${d}${matches.length}
`; return acc + `${d}` }, "") if(matches.length === 0 && !this.#filter.zeromatches){ prefix = `` } else if(matches.length > 0){ prefix = `` 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( `
 ${match.distance}
${match.sequence} at ${match.index} on strand ${match.strand} `); }); /*matches.forEach(match => { outputs.push( `
 ${match.distance}
${match.sequence} ${match.index} ${match.strand} `); });*/ 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 } }