From 3b6899ef1fdfe41c8c34592f568a04d20edf46e5 Mon Sep 17 00:00:00 2001 From: dcore94 Date: Fri, 18 Oct 2024 11:42:51 +0200 Subject: [PATCH 01/22] d4sboot uses dynamic scope or UMA if explicitly requested --- boot/d4s-boot.js | 47 ++++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/boot/d4s-boot.js b/boot/d4s-boot.js index 21c3a8d..b3296bd 100644 --- a/boot/d4s-boot.js +++ b/boot/d4s-boot.js @@ -21,6 +21,7 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement { #queue = [] #interval = null #config = null + #uma = false #rpt = null constructor() { @@ -63,8 +64,8 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement { console.log("Keycloak initialized and user authenticated") //console.log("Token exp: " + this.expirationDate(this.#keycloak.tokenParsed.exp)) - //if an audience is provided then perform also authorization - if (this.#audience) { + //if an audience is provided and UMA flow requested then perform also authorization + if (this.#audience && this.#uma) { return this.loadConfig() } else { Promise.resolve() @@ -101,7 +102,11 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement { clientId: this.#clientId }) - return this.#keycloak.init({onLoad: 'login-required', checkLoginIframe: false }) + const properties = {onLoad: 'login-required', checkLoginIframe: false} + if(this.#audience && !this.#uma){ + properties["scope"] = `d4s-context:${this.#audience}` + } + return this.#keycloak.init(properties) } startStateChecker() { @@ -113,7 +118,7 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement { } else { if (this.#queue.length > 0) { this.#keycloak.updateToken(30).then(() => { - if (this.#audience) { + if (this.#uma && this.#audience) { //console.log("Checking entitlement for audience", this.#audience) const audience = encodeURIComponent(this.#audience) return this.entitlement(audience) @@ -156,18 +161,19 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement { return d } - checkContext() { - const parseJwt = this.parseJwt - const expDt = this.expirationDate - const audience = encodeURIComponent(this.#audience) - this.entitlement(audience).then(function (rpt) { - // onGrant callback function. - // If authorization was successful you'll receive an RPT - // with the necessary permissions to access the resource server - //console.log(rpt) - //console.log("rpt expires: " + expDt(parseJwt(rpt).exp)) - }) - } + // TODO: Candidate for removal + // checkContext() { + // const parseJwt = this.parseJwt + // const expDt = this.expirationDate + // const audience = encodeURIComponent(this.#audience) + // this.entitlement(audience).then(function (rpt) { + // // onGrant callback function. + // // If authorization was successful you'll receive an RPT + // // with the necessary permissions to access the resource server + // //console.log(rpt) + // //console.log("rpt expires: " + expDt(parseJwt(rpt).exp)) + // }) + // } secureFetch(url, request) { const p = new Promise((resolve, reject) => { @@ -291,7 +297,7 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement { } static get observedAttributes() { - return ["url", "realm", "gateway", "redirect-url", "context"]; + return ["url", "realm", "gateway", "redirect-url", "context", "uma"]; } attributeChangedCallback(name, oldValue, newValue) { @@ -312,10 +318,17 @@ window.customElements.define('d4s-boot-2', class extends HTMLElement { case "context": this.#audience = newValue break + case "uma": + this.#uma = newValue === "true" ? true : false + break } } } + get uma(){ + return this.#uma + } + get authenticated(){ return this.#authenticated } From b5758dadc40ea9305c7bb066b029ac99bd39946b Mon Sep 17 00:00:00 2001 From: dcore94 Date: Fri, 18 Oct 2024 12:44:53 +0200 Subject: [PATCH 02/22] fixed to handle scope init option --- common/js/keycloak.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/js/keycloak.js b/common/js/keycloak.js index aba4ff7..b26629d 100644 --- a/common/js/keycloak.js +++ b/common/js/keycloak.js @@ -217,6 +217,10 @@ } else { kc.enableLogging = false; } + + if (typeof initOptions.scope === 'string') { + kc.scope = initOptions.scope; + } } if (!kc.responseMode) { @@ -439,7 +443,7 @@ } var scope; - if (options && options.scope) { + if (options && options.scope || kc.scope) { if (options.scope.indexOf("openid") != -1) { scope = options.scope; } else { From a522a7a1ccc6591c985d4bb2c0e4a72c83a97980 Mon Sep 17 00:00:00 2001 From: dcore94 Date: Fri, 18 Oct 2024 12:50:06 +0200 Subject: [PATCH 03/22] fixed to handle scope init option --- common/js/keycloak.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/common/js/keycloak.js b/common/js/keycloak.js index b26629d..754e00a 100644 --- a/common/js/keycloak.js +++ b/common/js/keycloak.js @@ -442,16 +442,14 @@ baseUrl = kc.endpoints.authorize(); } - var scope; - if (options && options.scope || kc.scope) { - if (options.scope.indexOf("openid") != -1) { - scope = options.scope; - } else { - scope = "openid " + options.scope; - } - } else { - scope = "openid"; - } + var scope = options && options.scope || kc.scope; + if (!scope) { + // if scope is not set, default to "openid" + scope = "openid"; + } else if (scope.indexOf("openid") === -1) { + // if openid scope is missing, prefix the given scopes with it + scope = "openid " + scope; + } var url = baseUrl + '?client_id=' + encodeURIComponent(kc.clientId) From 7f36ebe941f53604937b386f9b5683c58d52ac7c Mon Sep 17 00:00:00 2001 From: dcore94 Date: Fri, 25 Oct 2024 15:09:45 +0200 Subject: [PATCH 04/22] Specify that number is of type integer --- ccp/js/inputwidgeteditorcontroller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ccp/js/inputwidgeteditorcontroller.js b/ccp/js/inputwidgeteditorcontroller.js index 5512079..4dc3aca 100644 --- a/ccp/js/inputwidgeteditorcontroller.js +++ b/ccp/js/inputwidgeteditorcontroller.js @@ -144,7 +144,7 @@ class CCPInputWidgetEditorController extends HTMLElement { - + From a58fdbfd3d745ae624b37a190570214886eb6e32 Mon Sep 17 00:00:00 2001 From: dcore94 Date: Fri, 15 Nov 2024 15:24:35 +0100 Subject: [PATCH 05/22] added fix for unicde characters --- ccp/js/executionformcontroller.js | 4 ++-- ccp/js/inputwidgetcontroller.js | 30 +++++++++++++-------------- ccp/js/inputwidgeteditorcontroller.js | 2 +- ccp/js/outputwidgetcontroller.js | 2 +- common/js/utils.js | 25 ++++++++++++++++++++++ 5 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 common/js/utils.js diff --git a/ccp/js/executionformcontroller.js b/ccp/js/executionformcontroller.js index 83cea27..91b7e26 100644 --- a/ccp/js/executionformcontroller.js +++ b/ccp/js/executionformcontroller.js @@ -491,7 +491,7 @@ class CCPExecutionForm extends HTMLElement { "in": (e, d) => { return Object.values(d.inputs) }, target: "div", apply: (e, d) => { - e.innerHTML = `` + e.innerHTML = `` } } ] @@ -504,7 +504,7 @@ class CCPExecutionForm extends HTMLElement { "in": (e, d) => { return Object.values(d.outputs) }, target: "div", apply: (e, d) => { - e.innerHTML = `` + e.innerHTML = `` } } ] diff --git a/ccp/js/inputwidgetcontroller.js b/ccp/js/inputwidgetcontroller.js index abbf705..ff2ef8c 100644 --- a/ccp/js/inputwidgetcontroller.js +++ b/ccp/js/inputwidgetcontroller.js @@ -7,65 +7,65 @@ class CCPInputWidgetController extends HTMLElement { } connectedCallback() { - this.#data = JSON.parse(atob(this.getAttribute("input"))) + this.#data = JSON.parse(base64DecodeUnicode(this.getAttribute("input"))) if (this.isChecklist()) { const opts = this.#data.schema.enum.join(",") - this.innerHTML += `` + this.innerHTML += `` this.querySelector("d4s-ccp-input-checklist").default = this.#data.schema.default } else if (this.isEnum()) { const opts = this.#data.schema.enum.join(",") - this.innerHTML += `` + this.innerHTML += `` this.querySelector("d4s-ccp-input-enum").default = this.#data.schema.default } else if (this.isCode()) { - this.innerHTML += `` + this.innerHTML += `` this.querySelector("d4s-ccp-input-textarea").default = this.#data.schema.default } else if (this.isDateTime()) { const t = this.#data.schema.format.toLowerCase() === "datetime" ? "datetime-local" : this.#data.schema.format.toLowerCase() - this.innerHTML += `` + this.innerHTML += `` this.querySelector("d4s-ccp-input-simple").default = this.#data.schema.default } else if (this.isFile()) { - this.innerHTML += `` + this.innerHTML += `` this.querySelector("d4s-ccp-input-file").default = this.#data.schema.default } else if (this.isRemoteFile()) { - this.innerHTML += `` + this.innerHTML += `` this.querySelector("d4s-ccp-input-remotefile").default = this.#data.schema.default } else if (this.isGeo()) { - this.innerHTML += `` + this.innerHTML += `` this.querySelector("d4s-ccp-input-geo").default = this.#data.schema.default } else if (this.isSecret()) { - this.innerHTML += `` + this.innerHTML += `` this.querySelector("d4s-ccp-input-secret").default = this.#data.schema.default } else if (this.isBoolean()) { - this.innerHTML += `` + this.innerHTML += `` this.querySelector("d4s-ccp-input-boolean").default = this.#data.schema.default } else if (this.isNumber()) { - this.innerHTML += `` + this.innerHTML += `` this.querySelector("d4s-ccp-input-simple").default = this.#data.schema.default } else { - this.innerHTML += `` + this.innerHTML += `` this.querySelector("d4s-ccp-input-simple").default = this.#data.schema.default } @@ -173,7 +173,7 @@ class CCPBaseInputWidgetController extends HTMLElement { this.#rootdoc = this//this.attachShadow({ mode: "open" }); this.#name = this.getAttribute("name") this.#title = this.getAttribute("title") - this.#description = this.getAttribute("description") + this.#description = base64DecodeUnicode(this.getAttribute("description")) //this.#default = this.getAttribute("default") this.#minOccurs = Number(this.getAttribute("minoccurs") ? this.getAttribute("minoccurs") : 1) this.#maxOccurs = Number(this.getAttribute("maxoccurs") ? this.getAttribute("maxoccurs") : 1) @@ -296,7 +296,7 @@ class CCPBaseInputWidgetController extends HTMLElement {
- ${atob(this.#description)} + ${this.#description}
@@ -616,7 +616,7 @@ class CCPRemoteFileInputWidgetController extends CCPBaseInputWidgetController { Select an item or drag and drop it to a proper input -
+
` + return `` } renderDeleteButton() { diff --git a/ccp/js/outputwidgetcontroller.js b/ccp/js/outputwidgetcontroller.js index f8661ec..ff62b06 100644 --- a/ccp/js/outputwidgetcontroller.js +++ b/ccp/js/outputwidgetcontroller.js @@ -4,7 +4,7 @@ class CCPOutputWidgetController extends HTMLElement { constructor(){ super() - this.#output = JSON.parse(atob(this.getAttribute("output"))) + this.#output = JSON.parse(base64DecodeUnicode(this.getAttribute("output"))) } connectedCallback(){ diff --git a/common/js/utils.js b/common/js/utils.js new file mode 100644 index 0000000..d77a92c --- /dev/null +++ b/common/js/utils.js @@ -0,0 +1,25 @@ +function base64EncodeUnicode(str) { + // Encode the string as UTF-8 bytes + const utf8Bytes = new TextEncoder().encode(str); + + // Convert the bytes to a binary string + let binary = ''; + utf8Bytes.forEach(byte => { + binary += String.fromCharCode(byte); + }); + + // Encode the binary string to Base64 + return btoa(binary); +} + +function base64DecodeUnicode(base64) { + // Decode the Base64 string to a binary string + const binary = atob(base64); + + // Convert the binary string to a Uint8Array + const bytes = Uint8Array.from(binary, char => char.charCodeAt(0)); + + // Decode the UTF-8 bytes back to a Unicode string + const decoder = new TextDecoder(); + return decoder.decode(bytes); +} \ No newline at end of file From 9b2ce4f71030ce2199e91e56d03c0d3288962f47 Mon Sep 17 00:00:00 2001 From: dcore94 Date: Wed, 20 Nov 2024 10:32:07 +0100 Subject: [PATCH 06/22] more resilient fetching of archived executions --- ccp/js/executionhistorycontroller.js | 66 +++++++++++++++------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/ccp/js/executionhistorycontroller.js b/ccp/js/executionhistorycontroller.js index 7ce2b03..59c5aff 100644 --- a/ccp/js/executionhistorycontroller.js +++ b/ccp/js/executionhistorycontroller.js @@ -695,38 +695,42 @@ class CCPExecutionHistory extends HTMLElement { //parse all metada content that is useful to build up an entry const metadata = await this.#ws.download(metadatafolder[0].id, null, true) const zip = new JSZip() - const req = JSON.parse(await (zip.loadAsync(metadata).then(z=>z.file("metadata/request.json").async("string")))) - const status = JSON.parse(await (zip.loadAsync(metadata).then(z=>z.file("metadata/jobStatus.json").async("string")))) - const meth = JSON.parse(await (zip.loadAsync(metadata).then(z=>z.file("metadata/method.json").async("string")))) - const infra = JSON.parse(await (zip.loadAsync(metadata).then(z=>z.file("metadata/infrastructure.json").async("string")))) - const parser = new DOMParser() - // Fasted way to get context is from prov-o. If it doesn't match the context in boot skip this execution - const provo = parser.parseFromString(await zip.loadAsync(metadata).then(z=>z.file("metadata/prov-o.xml").async("string")), "text/xml") - const context = provo.querySelector("entity[*|id='d4s:VRE']>value").textContent - if(context !== this.#boot.context && context.replaceAll("/", "%2F") !== this.#boot.context) continue; - //build entry from downloaded data - const entry = { - id : req.id, - status : status.status, - created : status.created, - started : status.started, - updated : status.updated, - message : status.message, - method : meth.title, - methodversion : meth.version, - infrastructure: infra.name, - infrastructuretype : infra.type, - ccpnote : req.inputs.ccpnote ? req.inputs.ccpnote : "", - runtime : req.inputs.ccpimage, - replicas : req.inputs.ccpreplicas ? req.inputs.ccpreplicas : 1, - href : `items/${executionfolders[j].id}/download`, - wsid : executionfolders[j].id, - fullrequest : req, - fullmethod : meth, - fullinfrastructure : infra + try{ + const req = JSON.parse(await (zip.loadAsync(metadata).then(z=>z.file("metadata/request.json").async("string")))) + const status = JSON.parse(await (zip.loadAsync(metadata).then(z=>z.file("metadata/jobStatus.json").async("string")))) + const meth = JSON.parse(await (zip.loadAsync(metadata).then(z=>z.file("metadata/method.json").async("string")))) + const infra = JSON.parse(await (zip.loadAsync(metadata).then(z=>z.file("metadata/infrastructure.json").async("string")))) + const parser = new DOMParser() + // Fasted way to get context is from prov-o. If it doesn't match the context in boot skip this execution + const provo = parser.parseFromString(await zip.loadAsync(metadata).then(z=>z.file("metadata/prov-o.xml").async("string")), "text/xml") + const context = provo.querySelector("entity[*|id='d4s:VRE']>value").textContent + if(context !== this.#boot.context && context.replaceAll("/", "%2F") !== this.#boot.context) continue; + //build entry from downloaded data + const entry = { + id : req.id, + status : status.status, + created : status.created, + started : status.started, + updated : status.updated, + message : status.message, + method : meth.title, + methodversion : meth.version, + infrastructure: infra.name, + infrastructuretype : infra.type, + ccpnote : req.inputs.ccpnote ? req.inputs.ccpnote : "", + runtime : req.inputs.ccpimage, + replicas : req.inputs.ccpreplicas ? req.inputs.ccpreplicas : 1, + href : `items/${executionfolders[j].id}/download`, + wsid : executionfolders[j].id, + fullrequest : req, + fullmethod : meth, + fullinfrastructure : infra + } + if(this.#archived[entry.method]) this.#archived[entry.method].push(entry); + else this.#archived[entry.method] = [entry] + } catch (err){ + console.warn(`Skipping entry ${metadatafolder[0].id} because of ${err}`) } - if(this.#archived[entry.method]) this.#archived[entry.method].push(entry); - else this.#archived[entry.method] = [entry] } } } From bec4a722d57b28dece4dd93c255cbaea2a8ade7d Mon Sep 17 00:00:00 2001 From: dcore94 Date: Wed, 20 Nov 2024 10:58:00 +0100 Subject: [PATCH 07/22] added creation date log --- ccp/js/executionhistorycontroller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ccp/js/executionhistorycontroller.js b/ccp/js/executionhistorycontroller.js index 59c5aff..76d5a56 100644 --- a/ccp/js/executionhistorycontroller.js +++ b/ccp/js/executionhistorycontroller.js @@ -729,7 +729,7 @@ class CCPExecutionHistory extends HTMLElement { if(this.#archived[entry.method]) this.#archived[entry.method].push(entry); else this.#archived[entry.method] = [entry] } catch (err){ - console.warn(`Skipping entry ${metadatafolder[0].id} because of ${err}`) + console.warn(`Skipping entry ${metadatafolder[0].id} because of ${err} created at ${new Date(metadatafolder[0].creationTime)}`) } } } From 745b25562383de1161f9c103c5ba2a4a4619fad0 Mon Sep 17 00:00:00 2001 From: dcore94 Date: Wed, 20 Nov 2024 11:00:06 +0100 Subject: [PATCH 08/22] more tuning on error log --- ccp/js/executionhistorycontroller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ccp/js/executionhistorycontroller.js b/ccp/js/executionhistorycontroller.js index 76d5a56..c714f06 100644 --- a/ccp/js/executionhistorycontroller.js +++ b/ccp/js/executionhistorycontroller.js @@ -729,7 +729,7 @@ class CCPExecutionHistory extends HTMLElement { if(this.#archived[entry.method]) this.#archived[entry.method].push(entry); else this.#archived[entry.method] = [entry] } catch (err){ - console.warn(`Skipping entry ${metadatafolder[0].id} because of ${err} created at ${new Date(metadatafolder[0].creationTime)}`) + console.warn(`Skipping entry ${metadatafolder[0].id} because of ${err} created at ${new Date(metadatafolder[0].creationTime)}`, metadatafolder[0]) } } } From 5501b6e8f639eda4975099408e61f7e8831d7797 Mon Sep 17 00:00:00 2001 From: dcore94 Date: Wed, 20 Nov 2024 18:43:24 +0100 Subject: [PATCH 09/22] addeed components for i-gene --- i-gene/components/chopchop_importer.js | 206 ++++ i-gene/components/crisprscan_importer.js | 133 +++ i-gene/components/datadisplay.js | 325 +++++++ i-gene/components/smartinput.js | 83 ++ i-gene/i-gene.js | 1135 ++++++++++++++++++++++ i-gene/index.html | 41 + i-gene/readme.html | 324 ++++++ i-gene/resources/pamin.png | Bin 0 -> 4985 bytes i-gene/resources/pamout.png | Bin 0 -> 5487 bytes i-gene/resources/re.png | Bin 0 -> 5405 bytes 10 files changed, 2247 insertions(+) create mode 100644 i-gene/components/chopchop_importer.js create mode 100644 i-gene/components/crisprscan_importer.js create mode 100644 i-gene/components/datadisplay.js create mode 100644 i-gene/components/smartinput.js create mode 100644 i-gene/i-gene.js create mode 100644 i-gene/index.html create mode 100644 i-gene/readme.html create mode 100644 i-gene/resources/pamin.png create mode 100644 i-gene/resources/pamout.png create mode 100644 i-gene/resources/re.png diff --git a/i-gene/components/chopchop_importer.js b/i-gene/components/chopchop_importer.js new file mode 100644 index 0000000..5af22f9 --- /dev/null +++ b/i-gene/components/chopchop_importer.js @@ -0,0 +1,206 @@ +class ChopChopImporter extends AbstractImporter { + + #id = null; + #runinfo = null; + #query = null; + #cut = null; + #vis = null; + + constructor(){ + super(new ChopChopHelper()) + } + + render(){ + this.getRootDoc().innerHTML = ` + + +
+
+
Import from CHOPCHOP
+
+
+ +
+ https://chopchop.cbu.uib.no/results/ + + +
+ +
+
+ + + +
+
+ +
+ ` + this.getRootDoc().querySelector("#chopchop_job_id").addEventListener("paste", ev=> { + const contents = ev.clipboardData.getData('text'); + if (contents.indexOf('/results/') > 0) { + ev.preventDefault(); + this.getRootDoc().querySelector("#chopchop_job_id").value = contents.substring(contents.indexOf("/results/") + 9, contents.indexOf("/details/") != -1 ? contents.indexOf("/details/") + 1 : contents.length); + } + }); + this.getRootDoc().querySelector("#chopchop_job_id").addEventListener("drop", ev=> { + const uri = ev.dataTransfer.getData("text/uri-list"); + if (uri.indexOf('/results/') > 0) { + ev.preventDefault(); + ev.stopPropagation(); + this.getRootDoc().querySelector("#chopchop_job_id").value = uri.substring(uri.indexOf("/results/") + 9, uri.indexOf("/details/") != -1 ? uri.indexOf("/details/") + 1 : uri.length); + } + }); + this.getRootDoc().querySelector("#chopchop_import_button").addEventListener("click", ev=> { + this.#id = this.getRootDoc().querySelector("#chopchop_job_id").value + if (this.#id == null || this.#id == '') { + alert("Please, enter the CHOPCHOP job id to import"); + return; + } + if (this.#id.endsWith('/')) { + this.#id = this.#id.substring(0, this.#id.length - 1) + } + this.import() + }) + this.getRootDoc().querySelector("#chopchop_file_import_button").addEventListener("click", ev=> { + const tsvFile = this.getRootDoc().querySelector("#chopchop_file").files[0]; + const tsvURL = this.getRootDoc().querySelector("#chopchop_url").value; + if ((tsvFile == null || tsvFile == '') && (tsvURL == null || tsvURL == '')) { + alert("Please, select/enter the CHOPCHOP's TSV file/URL to import first"); + return; + } + const genome = this.getGenomeFromInput() + if (genome == null || genome == '') { + alert("Please, select the Genome first"); + return; + } + const pam = this.getPAMFromInput(); + if (pam == null || pam == '') { + alert("Please, select the PAM first"); + return; + } + if (tsvFile != null && tsvFile != '') { + this.importTSVExport(tsvFile, genome, pam); + } else { + this.importTSVURL(tsvURL, genome, pam); + } + }) + + this.getRootDoc().querySelector("a[name=trigger]").addEventListener("click", ev=> { + ev.preventDefault() + ev.stopPropagation() + + if(this.getDatadisplay()){ + this.getDatadisplay().show(this) + } + }) + } + + getGenomeFromInput() { + return this.getRootDoc().querySelector("#chopchop_file_genome").value; + } + + getPAMFromInput() { + return this.getRootDoc().querySelector("#chopchop_file_pam").value; + } + + import() { + this.downloadData() + .then(v=>{ + const runinfo = v[0].split("\t") + this.#runinfo = { + targets : runinfo[0], + genome : runinfo[1], + mode : runinfo[2], + uniqueMethod_cong : runinfo[3], + quideSize : runinfo[4] + } + this.#query = JSON.parse(v[1]) + this.parseTSV(v[2]) + this.#cut = JSON.parse(v[3]) + this.#vis = JSON.parse(v[4]) + + this.setIndexer(new iGeneIndexer(this.getTableLines(), this.getGenomeFromRunInfo(), this.getPAMFromRunInfo(), this.getHelper())); + this.enableData(); + }).catch(err=>console.error(err)) + } + + downloadData() { + const base = "http://chopchop.cbu.uib.no/results/" + this.#id + "/"; + const urls = [base + "run.info", base + "query.json", base + "results.tsv", base + "cutcoords.json", base + "viscoords.json"]; + const fetches = urls.map(u => { + return iGeneIndexer.fetchViaCORSProxy(u).then(r => { if(r.status !== 200) throw "Error fetching CHOPCHOP job via CORS proxy..."; else return r.text() }); + }) + return Promise.all(fetches); + } + + enableData(){ + const a = this.getRootDoc().querySelector("a[name=trigger]") + if (this.#query != null) { + a.textContent = ` + Show ${this.getTableLines().length} matches on genome ${this.getGenomeFromRunInfo()} ${this.getShortInput()} for ${this.#query.forSelect} + ` + } else { + a.textContent = ` + Show ${this.getTableLines().length} matches from imported TSV data + ` + } + a.classList.remove("d-none") + } + + #API + getGenomeFromRunInfo() { + return this.#runinfo.genome; + } + + getPAMFromRunInfo() { + let pam = 'NGG' // PAM is initialized with default value of the Cas9 sequence, then is copied from result's '-M' query parameter, if defined + this.#query.opts.forEach((element, index) => { + if (element == '-M') { + pam = this.#query.opts[index +1]; + } + }); + return pam; + } + + getShortInput(){ + if (this.#runinfo != null) { + var input = "" + if(this.#query.geneInput === ""){ + input = "fasta " + this.#query.fastaInput.length > 10 ? this.#query.fastaInput.substring(0,10) + "..." : this.#query.fastaInput + }else{ + input = "gene " + this.#query.geneInput + } + return input + } else { + return "[No info]" + } + } + + getHeader(){ + if (this.#runinfo != null) { + return `Imported from CHOPCHOP job ${this.#id}: ${this.getGenomeFromRunInfo()} ${this.getShortInput()}`; + } else { + return `Imported from CHOPCHOP TSV export: ${this.getGenomeFromInput()} - ${this.getPAMFromInput()}`; + } + } + +} + +window.customElements.define('igene-chopchop-importer', ChopChopImporter); diff --git a/i-gene/components/crisprscan_importer.js b/i-gene/components/crisprscan_importer.js new file mode 100644 index 0000000..ad344d2 --- /dev/null +++ b/i-gene/components/crisprscan_importer.js @@ -0,0 +1,133 @@ +class CrisprscanImporter extends AbstractImporter { + + constructor() { + super(new CrisprscanHelper()) + } + + render(){ + this.getRootDoc().innerHTML = ` + +
+ +
+ +
+
+ + + + +
+
+ +
+ ` + + this.getRootDoc().querySelector("#crispscan_file_genome").addEventListener("change", ev=> { + const genome = ev.target.value; + const chromosomeSelect = this.getRootDoc().querySelector("#crispscan_file_chromosome"); + chromosomeSelect.innerHTML = ""; + if (genome != '') { + iGeneIndexer.fetchGenomeChromosomesFromUSCS(genome).then(chromosomes => { + chromosomeSelect.innerHTML = ''; + for (const [name, index] of Object.entries(chromosomes).sort()) { + if (name.length < 6) { + const newOption = document.createElement("option"); + newOption.text = name; + newOption.value = name; + chromosomeSelect.add(newOption); + } + } + chromosomeSelect.disabled = false; + }).catch(err => alert(err)); + } else { + chromosomeSelect.disabled = true; + } + }); + this.getRootDoc().querySelector("#crispscan_file_import_button").addEventListener("click", ev=> { + const tsvFile = this.getRootDoc().querySelector("#crispscan_file").files[0]; + const tsvURL = this.getRootDoc().querySelector("#crisprscan_url").value; + if ((tsvFile == null || tsvFile == '') && (tsvURL == null || tsvURL == '')) { + alert("Please, select/enter the CRISPRscan's TSV file/URL to import first"); + return; + } + const genome = this.getRootDoc().querySelector("#crispscan_file_genome").value + if (genome == null || genome == '') { + alert("Please, enter the Genome first"); + return; + } + const chromosome = this.getRootDoc().querySelector("#crispscan_file_chromosome").value + if (chromosome == null || chromosome == '') { + alert("Please, enter the chromosome first"); + return; + } + const pam = this.getRootDoc().querySelector("#crispscan_file_pam").value + if (pam == null || pam == '') { + alert("Please, enter the PAM first"); + return; + } + this.getHelper().setDefaultChromosome(chromosome); + if (tsvFile != null && tsvFile != '') { + this.importTSVExport(tsvFile, genome, pam); + } else { + this.importTSVURL(tsvURL, genome, pam); + } + }) + + this.getRootDoc().querySelector("a[name=trigger]").addEventListener("click", ev=>{ + ev.preventDefault() + ev.stopPropagation() + if(this.getDatadisplay()){ + this.getDatadisplay().show(this) + } + }) + } + + enableData(){ + const a = this.getRootDoc().querySelector("a[name=trigger]") + a.textContent = ` + Show ${this.getTableLines().length} matches from imported TSV data + ` + a.classList.remove("d-none") + } + + getHeader(){ + return `Imported from CRISPRscan TSV file` + } + + getRenderer(filter, output){ + if (filter.mode !== 're') { + if(output === "html") { + return new CrisprScanHTMLDimerTableRenderer(this, filter); + } else if(output === "tsv2") { + return new CrisprScanTSVDimerTableRenderer(this, filter, "tsv2") + } else { + return super.getRenderer(this, filter) + } + } else { + return super.getRenderer(filter, output) + } + } + +} + +window.customElements.define('igene-crisprscan-importer', CrisprscanImporter); \ No newline at end of file diff --git a/i-gene/components/datadisplay.js b/i-gene/components/datadisplay.js new file mode 100644 index 0000000..f2333b0 --- /dev/null +++ b/i-gene/components/datadisplay.js @@ -0,0 +1,325 @@ +class DataDisplay extends HTMLElement{ + + #rootdoc = null; + #source = null; + #filter = { + mode : "out", + distance: 15, + enzyme: "", + zeromatches : false + } + #timeout = null; + #export = "" + + constructor(){ + super() + this.#rootdoc = this.attachShadow({ "mode" : "open"}) + this.render() + } + + show(source){ + this.#source = source + this.render() + } + + render(){ + if(this.#source == null){ + this.#rootdoc.innerHTML = `
Nothing to show
` + }else{ + const s = this.#source + this.#rootdoc.innerHTML = ` + + +
+
${s.getHeader()}
+
+
+
+ Pair of two guides + ? +
+
+

Pair of two guides

+

Analysis of the imported data to find a pair of two gRNAs in the maximum desired distance. By clicking on the gRNA, the gRNAs that can be used as a pair are shown. The number of available gRNAs as a pair is shown as a blue square.

+
+
+
PAM OUT
+ pam out +

The PAM sites of the pair of two gRNAs face outwards. The distance between them is calculated from the 5’ end of the one gRNA till the 5’ end of the other gRNA.

+
+
+
PAM IN
+ pam out +

The PAM sites of the pair of two gRNAs face inwards. The distance between them is calculated from the 3’ end of the one gRNA till the 3’ end of the other gRNA.

+
+
+
Any PAM
+

All the different directions of the two gRNAs and the configurations of the PAM sites are considered, including the configurations PAM OUT and PAM IN.

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ Restriction Enzyme + ? +
+
+

Restriction Enzyme

+ pam out +

Analysis of the imported data to find gRNAs in a maximum distance of a selected restriction enzyme. The distance between the gRNA and the restriction enzyme is calculated as the closest distance between the 5’ or 3’ end of the restriction enzyme and the 5’ or 3’ end the gRNA. By clicking on the gRNA, the accurate distance between the gRNA and the restriction enzyme is shown in a grey square. The – means that the restriction site is located upstream of the gRNA recognition site.

+
+
+
+
+
+ +
+ +
+ +
+
+
+
+ Maximum distance + +
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
+
+ ${this.renderTable()} +
+
+ ` + const helps = Array.prototype.slice.call(this.#rootdoc.querySelectorAll("span[name='help']")) + helps.forEach(h=>{ + h.addEventListener("click", ev=>{ + ev.target.parentElement.querySelector(".help-text").classList.toggle("d-none") + }) + }) + document.addEventListener("keydown", ev=>{ + if(ev.key === "Escape"){ + const hts = Array.prototype.slice.call(this.#rootdoc.querySelectorAll(".help-text")) + hts.forEach(ht=>{ + if(!ht.classList.contains("d-none")) ht.classList.add("d-none"); + }) + } + }) + this.#rootdoc.querySelector("div.filters").addEventListener("change", ev=>{ + const tgt = ev.target + if(tgt.id === "pam-any" || tgt.id === "pam-in" || tgt.id === "pam-out" || tgt.id === 're') { + if(tgt.id === "pam-any" || tgt.id === "pam-in" || tgt.id === "pam-out"){ + this.#rootdoc.querySelector("#restriction-enzyme").disabled = true; + } else if(tgt.id === "re" ) { + this.#rootdoc.querySelector("#restriction-enzyme").disabled = false; + } + this.#filter.mode = tgt.value + this.#rootdoc.querySelector("#table-container").innerHTML = this.renderTable(); + } + }) + + this.#rootdoc.querySelector("div.filters").addEventListener("input", ev=>{ + const tgt = ev.target + if(tgt.id === "distance"){ + if(this.#timeout == null){ + this.#timeout = window.setTimeout(()=>{ + this.#filter.distance = Number(this.#rootdoc.querySelector("#distance").value) + this.#rootdoc.querySelector("#table-container").innerHTML = this.renderTable() + this.#timeout = null; + }, 1000); + } + } + }) + + this.#rootdoc.querySelector("button[name='tsvexport2']").addEventListener("click", ev=>{ + this.exportTable2("\t") + }) + this.#rootdoc.querySelector("button[name='export2ws']").addEventListener("click", ev => { + this.export2Workspace("\t"); + }) + + this.#rootdoc.querySelector("#table-container").addEventListener("click", ev=>{ + var root = ev.target.parentElement + while(root != null && !root.getAttribute("data-rank")){ + root = root.parentElement + } + if(root == null) return; + const rank = root.getAttribute("data-rank") + if(rank){ + Array.prototype.slice.call(this.#rootdoc.querySelectorAll(`#table-container tr[data-rel-rank='${rank}']`)).forEach(tr=>tr.classList.toggle("d-none")) + } + }) + + this.#rootdoc.querySelector("#toggle-zero-matches").addEventListener("change", ev=>{ + const tgt = ev.target + this.#filter.zeromatches = tgt.checked + this.toggleVisibility() + }) + + const reSelect = this.#rootdoc.querySelector("#restriction-enzyme"); + reSelect.data = iGeneIndexer.restrictionEnzymes.filter(re=>!re.sequence.includes('?') && !re.sequence.includes('(')) + /*const reSelect = this.#rootdoc.querySelector("#restriction-enzyme"); + const reSelectFeedback = this.#rootdoc.querySelector("#restriction-enzyme-feedback"); + const reSelectDatalist = this.#rootdoc.querySelector("#restriction-enzyme-datalist"); + reSelectDatalist.innerHTML = iGeneIndexer.restrictionEnzymes.reduce((acc, e)=>{ + return acc + ` + + ` + }, '')*/ + + /*iGeneIndexer.restrictionEnzymes.forEach(enzyme => { + const newOption = document.createElement("option"); + newOption.text = enzyme.name + ' (' + enzyme.sequence + ')'; + newOption.value = enzyme.sequence; + if (enzyme.sequence.includes('?') || enzyme.sequence.includes('(')) { + newOption.disabled = true; + } + reSelectDatalist.add(newOption); + });*/ + + // reSelect.addEventListener("change", ev => { + // this.#filter.enzyme = reSelect.value; + // const o = reSelectDatalist.querySelector("option[value='" + reSelect.value + "']") + // reSelectFeedback.textContent = o ? o.textContent : reSelect.value + // this.#rootdoc.querySelector("#table-container").innerHTML = this.renderTable(); + // }) + } + } + + toggleVisibility(){ + const trs = Array.prototype.slice.call(this.#rootdoc.querySelectorAll("tr.table-row-nomatch")) + trs.forEach(tr=>tr.classList.toggle('d-none')) + } + + updateEnzyme(enzyme){ + this.#filter.enzyme = enzyme + setTimeout(() => { + this.#rootdoc.querySelector("#table-container").innerHTML = this.renderTable(); + }, 100); + } + + renderTable() { + const renderer = this.#source.getRenderer(this.#filter, "html") + return renderer.renderTable() + } + + constructFilename() { + const indexer = this.#source.getIndexer(); + const filter = this.#filter; + let filename = 'export_'; + filename += 'genome=' + indexer.getGenome() + '_chr=' + indexer.getChromosome() + '_pam=' + indexer.getPAM(); + filename += '_dist=' + filter.distance + '_mode=pam-' + filter.mode + (filter.mode === 're' ? '_(enz=' + filter.enzyme + ')' : ''); + return filename; + } + + exportTable(sep){ + const renderer = this.#source.getRenderer(this.#filter, "tsv") + if(renderer != null){ + this.#export = renderer.renderTable() + + var element = document.createElement('a'); + element.setAttribute('href', 'data:text/tab-separated-values;charset=utf-8,' + encodeURIComponent(this.#export)); + element.setAttribute('download', this.constructFilename() + '.tsv'); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + } + } + + exportTable2(sep){ + const renderer = this.#source.getRenderer(this.#filter, "tsv2") + if(renderer != null){ + this.#export = renderer.renderTable() + + var element = document.createElement('a'); + element.setAttribute('href', 'data:text/tab-separated-values;charset=utf-8,' + encodeURIComponent(this.#export)); + element.setAttribute('download', this.constructFilename() + '.tsv'); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + } + } + + async export2Workspace(sep) { + const renderer = this.#source.getRenderer(this.#filter, "tsv2") + if(renderer != null){ + const folderName = "i-Gene-Matcher"; + const date = new Date(); + const fileName = this.constructFilename() + '_' + date.toISOString().substring(0, 11) + date.toLocaleTimeString().replaceAll(":", "-") + ".tsv"; + const file2export = renderer.renderTable(); + const workspace = new D4SWorkspace("https://api.d4science.org/workspace"); + const wsId = await workspace.getWorkspace(null); + const iGeneFolder = await workspace.checkOrCreateFolder(wsId, folderName, "I-Gene tool export folder", null); + if(await workspace.uploadFile(iGeneFolder, fileName, "I-Gene Matcher Export work of " + date, file2export, "text/tab-separated-values", null)) { + alert("File successfully saved on D4Science Workspace as:\n\n/" + folderName + "/" + fileName); + } else { + alert("An error occurred during the file upload to workspace, check browser console for more details"); + } + } + } +} + +window.customElements.define('igene-data-display', DataDisplay); diff --git a/i-gene/components/smartinput.js b/i-gene/components/smartinput.js new file mode 100644 index 0000000..95ec435 --- /dev/null +++ b/i-gene/components/smartinput.js @@ -0,0 +1,83 @@ +class SmartInput extends HTMLElement { + + #data = null; + #rootdoc = null; + #delay = 300; + #value = null; + #timeoutid = null; + #input = null; + #list = null; + #label = null; + + constructor(){ + super() + this.#rootdoc = this.attachShadow({ "mode" : "open"}) + this.render() + } + + connectedCallback(){} + + set data(data){ this.#data = data; this.render() } + + render(){ + this.#rootdoc.innerHTML = ` + +
+
+ + +
+
+
    +
    +
    + ` + this.#list = this.#rootdoc.querySelector("ul") + this.#input = this.#rootdoc.querySelector("input") + this.#label = this.#rootdoc.querySelector("small#restriction-enzyme-feedback") + this.#rootdoc.addEventListener("keydown", ev=>{ + if(ev.code === "Escape"){ + this.#list.classList.add("d-none") + } + }) + this.#input.addEventListener("input", ev=>{ + this.#value = ev.target.value + this.#label.textContent = "" + if(this.#timeoutid != null) window.clearTimeout(this.#timeoutid) + this.#timeoutid = window.setTimeout(()=>{ + this.renderOptions() + }, this.#delay) + }) + this.#input.addEventListener("keypress", ev=>{ + if(ev.code === "Enter"){ + this.#list.classList.add("d-none") + document.querySelector("igene-data-display").updateEnzyme(this.#input.value) + } + }) + this.#list.addEventListener("click", ev=>{ + if(ev.target.classList.contains("list-group-item")){ + this.#input.value = ev.target.getAttribute("data-sequence") + this.#label.textContent = ev.target.getAttribute("data-enzyme") + this.#list.classList.add("d-none") + document.querySelector("igene-data-display").updateEnzyme(this.#input.value) + } + }) + } + + renderOptions(){ + if(this.#value == null || this.#value === ""){ + this.#list.classList.add("d-none") + return + } + const options = this.#data.filter(d=>{ + return d.name.startsWith(this.#value) || d.sequence.startsWith(this.#value) + }).map(o=>{ + return ` +
  • ${o.name}${o.sequence}
  • + ` + }).join("") + this.#list.innerHTML = options + this.#list.classList.remove("d-none") + } +} +window.customElements.define('nw-smart-input', SmartInput); \ No newline at end of file diff --git a/i-gene/i-gene.js b/i-gene/i-gene.js new file mode 100644 index 0000000..60e67e0 --- /dev/null +++ b/i-gene/i-gene.js @@ -0,0 +1,1135 @@ +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 + } +} diff --git a/i-gene/index.html b/i-gene/index.html new file mode 100644 index 0000000..a6bc318 --- /dev/null +++ b/i-gene/index.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + +
    + +
    +
    +

    Welcome to the I-Gene Crispr tool

    +

    I-GENE tool is an online tool to analyse your data for selecting gRNAs in pair or gRNAs in proximity to a desired restriction enzyme.

    +

    The input information comes from the web tool CHOPCHOP either as a link or as an imported TSV file and from CrisprScan as imported TSV.

    +
    +
    +
    + + +
    +
    +
    + +
    +
    + + + diff --git a/i-gene/readme.html b/i-gene/readme.html new file mode 100644 index 0000000..621fbed --- /dev/null +++ b/i-gene/readme.html @@ -0,0 +1,324 @@ + + + + + + + + + +
    +

    I-GeneMatcher Readme

    +

    Please follow the tutorial on how to use the I-GeneMatcher. If your video does not play you can download the video from here.

    + +
    +

    Table description

    +

    The following tables describe the meaning of the columns in the various data tables produced as output by the I-GeneMatcher.

    +
    Pair of two guides importing file from CHOPCHOP
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ColumnDescription
    RankRank of guide 1 as reported in CHOPCHOP
    Match rankRank of guide 2 as reported in CHOPCHOP
    DistancePrecise distance between the guides with the selected configuration
    Target sequenceSequence of guide 1
    Genomic locationPosition of guide 1
    Match Genomic locationPosition of guide 2
    StrandDNA strand that the guide 1 targets
    Match StrandDNA strand that the guide 2 targets
    GC content (%)GC content percentage of guide 1
    Match GC content (%)GC content percentage of guide 2
    Self-complementaritySelf-complementarity value of guide 1 as reported in CHOPCHOP
    Match Self-complementaritySelf-complementarity value of guide 2 as reported in CHOPCHOP
    MM0-3Targets containing 0 to 3 mismatches for guide 1
    Match MM0-3Targets containing 0 to 3 mismatches for guide 2
    EfficiencyEfficiency value of guide 1 as reported in CHOPCHOP
    Match EfficiencyEfficiency value of guide 2 as reported in CHOPCHOP
    +
    Pair of two guides importing file from Crisprscan
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ColumnDescription
    RankRank of guide 1 as reported in crisprscan
    Match rankRank of guide 2 as reported in crisprscan
    DistancePrecise distance between the guides with the selected configuration
    NameTargeted chromosome of guide 1
    Match NameTargeted chromosome of guide 2
    StartStarting genomic position of guide 1
    Match StartStarting genomic position of guide 2
    EndEnding genomic position of guide 1
    Match EndEnding genomic position of guide 2
    StrandDNA strand that the guide 1 targets
    Match StrandDNA strand that the guide 2 targets
    Type20 nucleotides following the type of PAM for guide 1
    Match Type20 nucleotides following the type of PAM for guide 2
    SeqTargeting sequence containing the PAM sequence of guide 1
    Match SeqTargeting sequence containing the PAM sequence of guide 2
    Sgrna_seqSequence of guide 1
    Match Sgrna_seqSequence of guide 2
    PromoterIf selected during the crisprscan research
    Match PromoterIf selected during the crisprscan research
    Remaining values as reported by crisprscan. Prefix "Match" always indicates the corresponding values of guide 2.
    +
    Restriction enzyme importing file from CHOPCHOP
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ColumnDescription
    RE sequenceRestriction site of the selected restriction enzyme
    RE indexPosition of the restriction site
    RE strand5'- 3'
    DistancePrecise distance from the guide
    RankRank of the guide as reported in CHOPCHOP
    Target sequenceSequence of the guide
    Genomic locationPosition of the guide
    StrandDNA strand that the guide targets
    GC content (%)GC content percentage of the guide
    Self-complementaritySelf-complementarity value of the guide as reported in CHOPCHOP
    MM0-3Targets containing 0 to 3 mismatches
    EfficiencyEfficiency value as reported in CHOPCHOP
    +
    Restriction enzyme importing file from Crisprscan
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ColumnDescription
    RE sequenceRestriction site of the selected restriction enzyme
    RE indexPosition of the restriction site
    RE strand5' - 3'
    DistancePrecise distance from the guide
    RankRank of the guide as reported in crisprscan
    NameTargeted chromosome
    StartStarting genomic position of the guide
    EndEnding genomic position of the guide
    StrandDNA strand that the guide targets
    Type20 nucleotides following the type of PAM
    SeqTargeting sequence containing the PAM sequence
    Sgrna_seqSequence of the guide
    PromoterIf selected during the crisprscan research
    Remaining values as reported by crisprscan.
    +
    +
    + + \ No newline at end of file diff --git a/i-gene/resources/pamin.png b/i-gene/resources/pamin.png new file mode 100644 index 0000000000000000000000000000000000000000..094ae73bbc594b84faf292b5afb427fcee6d5355 GIT binary patch literal 4985 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 XulU594M&0BWgvBB9SmO4Cj5T@`qUh3 literal 0 HcmV?d00001 diff --git a/i-gene/resources/pamout.png b/i-gene/resources/pamout.png new file mode 100644 index 0000000000000000000000000000000000000000..a95fce7f3f0ba7d2ce130a952b0f9d23ab1135e7 GIT binary patch literal 5487 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 JsF1e|`44Cjen|iT literal 0 HcmV?d00001 diff --git a/i-gene/resources/re.png b/i-gene/resources/re.png new file mode 100644 index 0000000000000000000000000000000000000000..a5730cf5499d446c13ea7aedff1d35f7c3c8ab83 GIT binary patch literal 5405 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 T_}@TDlSD&RSEWM9D&ju?Lx}tU literal 0 HcmV?d00001 From a7198db4a36715396e4665b3f8a8e009896fbe16 Mon Sep 17 00:00:00 2001 From: dcore94 Date: Wed, 20 Nov 2024 18:52:01 +0100 Subject: [PATCH 10/22] added custom workspace client --- i-gene/d4s-workspace.js | 117 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 i-gene/d4s-workspace.js diff --git a/i-gene/d4s-workspace.js b/i-gene/d4s-workspace.js new file mode 100644 index 0000000..4c92acd --- /dev/null +++ b/i-gene/d4s-workspace.js @@ -0,0 +1,117 @@ +class D4SWorkspace { + + #d4sboot = null; + #workspaceURL = null; + + constructor(workspaceURL, d4sboot) { + this.#workspaceURL = workspaceURL; + if (d4sboot) { + this.#d4sboot = d4sboot; + } else { + this.#d4sboot = document.querySelector("d4s-boot-2"); + } + } + + /** + * Calls with a secure fetch the workspace with provided data. + * To upload data by using a FormData object you have to leave the mime attribute as null. + * @param {*} method the method to use for the call, default to GET + * @param {*} uri the uri to invoke, relative to workspaceURL provided in the object's constructor + * @param {*} body the payload to send + * @param {*} mime the mime type of the payload + * @param {*} extraHeaders extra HTTP headers to send + * @returns the reponse payload as a promise, JSON data promise if the returned data is "application/json", a String data promise if "text/*" or a blob data promise in other cases + */ + callWorkspace(method, uri, body, mime, extraHeaders) { + let req = { }; + if (method) { + req.method = method; + } + if (body) { + req.body = body; + } + if (extraHeaders) { + req.headers = extraHeaders; + } + if (mime) { + if (req.headers) { + req.headers["Content-Type"] = mime; + } else { + req.headers = { "Content-Type" : mime }; + } + } + const url = this.#workspaceURL + (uri.startsWith('/') ? uri : '/' + uri); + return this.#d4sboot.secureFetch(url, req) + .then(resp => { + if (resp.ok) { + const contentType = resp.headers.get("content-type"); + if (contentType && contentType.indexOf("application/json") !== -1) { + return resp.json(); + } else if (contentType && contentType.indexOf("text/") !== -1) { + return resp.text(); + } else { + return resp.blob(); + } + } else { + throw "Cannot invoke workspace via secure fetch for URL: " + url; + } + }).catch(err => { + console.error(err); + throw err; + }); + } + + getWorkspace($extraHeaders) { + return this.callWorkspace("GET", "", null, null, $extraHeaders) + .then(json => { + return json.item.id; + }).catch(err => { + const msg = "Cannot get workspace root ID"; + console.error(msg); + throw msg; + }); + } + + checkOrCreateFolder(parentFolderId, name, description, extraHeaders) { + const baseURI = "/items/" + parentFolderId; + console.log("Checking existance of folder: " + name); + let uri = baseURI + "/items/" + name; + return this.callWorkspace("GET", uri, null, null, extraHeaders) + .then(json => { + if (json.itemlist[0]) { + const id = json.itemlist[0].id; + console.log("'" + name + "' folder exists with and has id: " + id); + return id; + } else { + console.info("'" + name + "' folder doesn't exist, creating it: " + name); + uri = baseURI + "/create/FOLDER"; + const params = new URLSearchParams({ name : name, description : description, hidden : false }); + return this.callWorkspace("POST", uri, params.toString(), "application/x-www-form-urlencoded", extraHeaders).then(id => { + console.log("New '" + name + "' folder successfully created with id: " + id); + return id + }); + } + }); + }; + + uploadFile(parentFolderId, name, description, data, contentType, extraHeaders) { + const uri = "/items/" + parentFolderId + "/create/FILE"; + const request = new FormData(); + request.append("name", name); + request.append("description", description); + request.append( + "file", + new Blob([data], { + type: contentType + }) + ); + return this.callWorkspace("POST", uri, request, null, extraHeaders) + .then(id => { + console.info("File '" + name + "' successfully uploaded and its id is: " + id); + return true; + }).catch(err => { + console.error("Cannot upload file '" + name + "'. Error: " + err); + return false; + }); + }; +} \ No newline at end of file From 550408e07d14afecdedd5ba0c49c66870acf197f Mon Sep 17 00:00:00 2001 From: dcore94 Date: Wed, 20 Nov 2024 19:02:06 +0100 Subject: [PATCH 11/22] fixed link to vide tutorial --- i-gene/readme.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i-gene/readme.html b/i-gene/readme.html index 621fbed..d1ae90b 100644 --- a/i-gene/readme.html +++ b/i-gene/readme.html @@ -19,7 +19,7 @@

    I-GeneMatcher Readme

    Please follow the tutorial on how to use the I-GeneMatcher. If your video does not play you can download the video from here.

    Table description

    From e2d543c8fd5a52362e395f17c53877254daf79f2 Mon Sep 17 00:00:00 2001 From: dcore94 Date: Wed, 20 Nov 2024 19:03:41 +0100 Subject: [PATCH 12/22] fixed url --- i-gene/readme.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i-gene/readme.html b/i-gene/readme.html index d1ae90b..752efde 100644 --- a/i-gene/readme.html +++ b/i-gene/readme.html @@ -19,7 +19,7 @@

    I-GeneMatcher Readme

    Please follow the tutorial on how to use the I-GeneMatcher. If your video does not play you can download the video from here.

    Table description

    From 982778621c67e84f2f380ca997648f13c4a80090 Mon Sep 17 00:00:00 2001 From: dcore94 Date: Tue, 26 Nov 2024 18:21:11 +0100 Subject: [PATCH 13/22] fixed possibly empty resources array --- ccp/js/executionhistorycontroller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ccp/js/executionhistorycontroller.js b/ccp/js/executionhistorycontroller.js index c714f06..397c071 100644 --- a/ccp/js/executionhistorycontroller.js +++ b/ccp/js/executionhistorycontroller.js @@ -965,7 +965,8 @@ class CCPExecutionHistory extends HTMLElement { { target : "li", "in" : (e,d)=>{ - return d.resources.map(l=>{ + const resources = d.resources ? d.resurces : [] + return resources.map(l=>{ return { href : this.#serviceurl + "/executions/" + d.id + "/" + l.path, path : l.path} }) }, From cbda89bdedc0256f79ab5a7283439847308a6e71 Mon Sep 17 00:00:00 2001 From: dcore94 Date: Tue, 26 Nov 2024 18:23:09 +0100 Subject: [PATCH 14/22] fixed typo --- ccp/js/executionhistorycontroller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ccp/js/executionhistorycontroller.js b/ccp/js/executionhistorycontroller.js index 397c071..34d5142 100644 --- a/ccp/js/executionhistorycontroller.js +++ b/ccp/js/executionhistorycontroller.js @@ -965,7 +965,7 @@ class CCPExecutionHistory extends HTMLElement { { target : "li", "in" : (e,d)=>{ - const resources = d.resources ? d.resurces : [] + const resources = d.resources ? d.resources : [] return resources.map(l=>{ return { href : this.#serviceurl + "/executions/" + d.id + "/" + l.path, path : l.path} }) From 14c211be6a28163df37f91d07cce21f4a0d8fc33 Mon Sep 17 00:00:00 2001 From: dcore94 Date: Tue, 26 Nov 2024 18:41:03 +0100 Subject: [PATCH 15/22] hide away temporary ugly entries --- ccp/js/executionhistorycontroller.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ccp/js/executionhistorycontroller.js b/ccp/js/executionhistorycontroller.js index 34d5142..d3f7b57 100644 --- a/ccp/js/executionhistorycontroller.js +++ b/ccp/js/executionhistorycontroller.js @@ -769,9 +769,14 @@ class CCPExecutionHistory extends HTMLElement { { target : "details[name=level1]", apply : (e,d)=>{ - e.alt = e.title = d - if(sessionStorage.getItem(d) === "open") e.open = "open"; - else e.removeAttribute("open"); + if(!!d){ + e.alt = e.title = d + if(sessionStorage.getItem(d) === "open") e.open = "open"; + else e.removeAttribute("open"); + }else{ + //hide away entries that may occur as empty because of timing issues on the server + e.style.display = "none" + } }, on_toggle : ev=>{ if(ev.target.open){ From c1c23347941308d0ab0c2f5502490f21caca2469 Mon Sep 17 00:00:00 2001 From: dcore94 Date: Tue, 26 Nov 2024 18:45:36 +0100 Subject: [PATCH 16/22] fixed undefined as string --- ccp/js/executionhistorycontroller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ccp/js/executionhistorycontroller.js b/ccp/js/executionhistorycontroller.js index d3f7b57..ac7ac1c 100644 --- a/ccp/js/executionhistorycontroller.js +++ b/ccp/js/executionhistorycontroller.js @@ -769,7 +769,7 @@ class CCPExecutionHistory extends HTMLElement { { target : "details[name=level1]", apply : (e,d)=>{ - if(!!d){ + if(!!d && d !== "undefined"){ e.alt = e.title = d if(sessionStorage.getItem(d) === "open") e.open = "open"; else e.removeAttribute("open"); From 034370f5777fbbb563a6a6141389709f5c706e10 Mon Sep 17 00:00:00 2001 From: dcore94 Date: Tue, 26 Nov 2024 18:48:32 +0100 Subject: [PATCH 17/22] added loading symbol --- ccp/js/executionhistorycontroller.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ccp/js/executionhistorycontroller.js b/ccp/js/executionhistorycontroller.js index ac7ac1c..e5d36ef 100644 --- a/ccp/js/executionhistorycontroller.js +++ b/ccp/js/executionhistorycontroller.js @@ -776,6 +776,7 @@ class CCPExecutionHistory extends HTMLElement { }else{ //hide away entries that may occur as empty because of timing issues on the server e.style.display = "none" + this.showLoading(e.parentElement) } }, on_toggle : ev=>{ From 7e9558f1bfdf65483e09d349753ac11d52e185da Mon Sep 17 00:00:00 2001 From: dcore94 Date: Fri, 29 Nov 2024 10:37:50 +0100 Subject: [PATCH 18/22] Improved output UI --- ccp/js/executionhistorycontroller.js | 29 ++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/ccp/js/executionhistorycontroller.js b/ccp/js/executionhistorycontroller.js index e5d36ef..426996f 100644 --- a/ccp/js/executionhistorycontroller.js +++ b/ccp/js/executionhistorycontroller.js @@ -180,8 +180,12 @@ class CCPExecutionHistory extends HTMLElement {
    -
      -
    • +
        + +
      @@ -967,13 +971,20 @@ class CCPExecutionHistory extends HTMLElement { }, { target : "ul", + on_click: ev=>{ + const tgt = ev.target + const ul = ev.currentTarget + if(tgt.getAttribute("name") === "toggle_meta"){ + Array.prototype.slice.call(ul.querySelectorAll("li.metadata")).forEach(e=>e.classList.toggle("d-none")) + } + }, recurse : [ { target : "li", "in" : (e,d)=>{ const resources = d.resources ? d.resources : [] return resources.map(l=>{ - return { href : this.#serviceurl + "/executions/" + d.id + "/" + l.path, path : l.path} + return { id: d.id, size: l.size, href : this.#serviceurl + "/executions/" + d.id + "/" + l.path, path : l.path} }) }, on_click : ev=>{ @@ -982,7 +993,17 @@ class CCPExecutionHistory extends HTMLElement { this.download(href, name) }, apply : (e,d)=>{ - e.innerHTML = `${d.path}` + const regexp = /^metadata.+|^auth.+|.+\/ccp-entrypoint.sh$|.+\/ccpenv$|.+\/docker-compose.yaml/ + var size = "" + if(d.path.match(regexp)){ + e.classList.add("metadata") + e.classList.add("d-none") + }else if(d.size){ + const s = Number(d.size) + var i = s === 0 ? 0 : Math.floor(Math.log(s) / Math.log(1024)); + size = `[${+((s / Math.pow(1024, i)).toFixed(2)) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]}]` + } + e.innerHTML = `${size} ${d.path}` } } ] From 91e63861783de448ab94451405d8e57af68095cc Mon Sep 17 00:00:00 2001 From: dcore94 Date: Mon, 2 Dec 2024 14:58:28 +0100 Subject: [PATCH 19/22] inverted link and size --- ccp/js/executionhistorycontroller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ccp/js/executionhistorycontroller.js b/ccp/js/executionhistorycontroller.js index 426996f..c883698 100644 --- a/ccp/js/executionhistorycontroller.js +++ b/ccp/js/executionhistorycontroller.js @@ -1003,7 +1003,7 @@ class CCPExecutionHistory extends HTMLElement { var i = s === 0 ? 0 : Math.floor(Math.log(s) / Math.log(1024)); size = `[${+((s / Math.pow(1024, i)).toFixed(2)) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]}]` } - e.innerHTML = `${size} ${d.path}` + e.innerHTML = `${d.path}${size} ` } } ] From 07e025700ef7a6d820dc873787c65e99259ab087 Mon Sep 17 00:00:00 2001 From: dcore94 Date: Mon, 2 Dec 2024 14:59:37 +0100 Subject: [PATCH 20/22] small margin --- ccp/js/executionhistorycontroller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ccp/js/executionhistorycontroller.js b/ccp/js/executionhistorycontroller.js index c883698..4a50223 100644 --- a/ccp/js/executionhistorycontroller.js +++ b/ccp/js/executionhistorycontroller.js @@ -1003,7 +1003,7 @@ class CCPExecutionHistory extends HTMLElement { var i = s === 0 ? 0 : Math.floor(Math.log(s) / Math.log(1024)); size = `[${+((s / Math.pow(1024, i)).toFixed(2)) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]}]` } - e.innerHTML = `${d.path}${size} ` + e.innerHTML = `${d.path}${size} ` } } ] From 99d37cd4dfda803307b23f2754af047cb77ed56e Mon Sep 17 00:00:00 2001 From: dcore94 Date: Wed, 4 Dec 2024 18:50:40 +0100 Subject: [PATCH 21/22] remove restore button which is no longer available --- ccp/js/executionhistorycontroller.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ccp/js/executionhistorycontroller.js b/ccp/js/executionhistorycontroller.js index 4a50223..5ee2c71 100644 --- a/ccp/js/executionhistorycontroller.js +++ b/ccp/js/executionhistorycontroller.js @@ -232,11 +232,11 @@ class CCPExecutionHistory extends HTMLElement { -