diff --git a/idr_gallery/gallery_settings.py b/idr_gallery/gallery_settings.py index 9a8d78f8..efcbe543 100644 --- a/idr_gallery/gallery_settings.py +++ b/idr_gallery/gallery_settings.py @@ -31,11 +31,21 @@ ["BASE_URL", None, str_slash, - ("Base URL to use for JSON AJAX requests." + ("Base URL to use for non-gallery JSON AJAX requests." " e.g. 'https://demo.openmicroscopy.org'." " This allows data to be loaded from another OMERO server." " The default behaviour is to use the current server.")], + "omero.web.gallery.gallery_index": + ["GALLERY_INDEX", + None, + str_slash, + ("Base gallery URL to use for gallery JSON AJAX requests." + " e.g. 'https://idr.openmicroscopy.org/' is gallery index on IDR." + " This allows data to be loaded from another OMERO server, e.g. run" + " locally or on test server, but load data from IDR." + " Default behaviour is to use current server webgallery_index")], + "omero.web.gallery.category_queries": ["CATEGORY_QUERIES", ('{}'), @@ -143,6 +153,19 @@ " If a 'regex' and 'template' are specified, we try" " name.replace(regex, template).")], + "omero.web.gallery.index_json_url": + ["INDEX_JSON_URL", + 'https://raw.githubusercontent.com/will-moore/idr.openmicroscopy.org/idr_index_data/_data/idr_index.json', + str, + "URL to load JSON, with a 'tabs' list of {'title':'', 'text':'', 'videos':[]}" + ], + "omero.web.gallery.idr_studies_url": + ["IDR_STUDIES_URL", + 'https://raw.githubusercontent.com/IDR/idr.openmicroscopy.org/master/_data/studies.tsv', + str, + "URL to IDR studies as a tsv table" + ], + } process_custom_settings(sys.modules[__name__], 'GALLERY_SETTINGS_MAPPING') diff --git a/idr_gallery/static/idr_gallery/autocomplete.js b/idr_gallery/static/idr_gallery/autocomplete.js new file mode 100644 index 00000000..f0447225 --- /dev/null +++ b/idr_gallery/static/idr_gallery/autocomplete.js @@ -0,0 +1,152 @@ + +// ------ AUTO-COMPLETE ------------------- + +document.getElementById('maprConfig').onchange = (event) => { + document.getElementById('maprQuery').value = ''; + let value = event.target.value.replace('mapr_', ''); + let placeholder = `Type to filter values...`; + if (mapr_settings[value]) { + placeholder = `Type ${mapr_settings[value]['default'][0]}...`; + } + document.getElementById('maprQuery').placeholder = placeholder; + // Show all autocomplete options... + $("#maprQuery").focus(); + + // render(); +} + +function showAutocomplete(event) { + var configId = document.getElementById("maprConfig").value; + var autoCompleteValue = event.target.value; + + if (configId.indexOf('mapr_') != 0) { + // If not MAPR search, show all auto-complete results + autoCompleteValue = ''; + } + + $("#maprQuery").autocomplete("search", autoCompleteValue); +} + +document.getElementById('maprQuery').onfocus = function (event) { + showAutocomplete(event); +}; + +document.getElementById('maprQuery').onclick = function (event) { + showAutocomplete(event); +}; + +function showSpinner() { + document.getElementById('spinner').style.visibility = 'visible'; +} +function hideSpinner() { + document.getElementById('spinner').style.visibility = 'hidden'; +} + +// Initial setup... +$("#maprQuery") + .keyup(event => { + if (event.which == 13) { + let configId = document.getElementById("maprConfig").value; + document.location.href = `search/?query=${configId}:${event.target.value}`; + } + }) + .autocomplete({ + autoFocus: false, + delay: 1000, + source: function (request, response) { + + // if configId is not from mapr, we filter on mapValues... + let configId = document.getElementById("maprConfig").value; + if (configId.indexOf('mapr_') != 0) { + + let matches; + if (configId === 'Name') { + matches = model.getStudiesNames(request.term); + } else if (configId === 'Group') { + matches = model.getStudiesGroups(request.term); + } else { + matches = model.getKeyValueAutoComplete(configId, request.term); + } + response(matches); + return; + } + + // Don't handle empty query for mapr + if (request.term.length == 0) { + return; + } + + // Auto-complete to filter by mapr... + configId = configId.replace('mapr_', ''); + let case_sensitive = false; + + let requestData = { + case_sensitive: case_sensitive, + } + let url; + if (request.term.length === 0) { + // Try to list all top-level values. + // This works for 'wild-card' configs where number of values is small e.g. Organism + // But will return empty list for e.g. Gene + url = `${BASE_URL}mapr/api/${configId}/`; + requestData.orphaned = true + } else { + // Find auto-complete matches + url = `${BASE_URL}mapr/api/autocomplete/${configId}/`; + requestData.value = case_sensitive ? request.term : request.term.toLowerCase(); + requestData.query = true; // use a 'like' HQL query + } + showSpinner(); + $.ajax({ + dataType: "json", + type: 'GET', + url: url, + data: requestData, + success: function (data) { + hideSpinner(); + if (request.term.length === 0) { + // Top-level terms in 'maps' + if (data.maps && data.maps.length > 0) { + let terms = data.maps.map(m => m.id); + terms.sort(); + response(terms); + } + } + else if (data.length > 0) { + response($.map(data, function (item) { + return item; + })); + } else { + response([{ label: 'No results found.', value: -1 }]); + } + }, + error: function (data) { + hideSpinner(); + response([{ label: 'Loading auto-complete terms failed. Server may be busy.', value: -1 }]); + } + }); + }, + minLength: 0, + open: function () { }, + close: function () { + // $(this).val(''); + return false; + }, + focus: function (event, ui) { }, + select: function (event, ui) { + if (ui.item.value == -1) { + // Ignore 'No results found' + return false; + } + // show temp message in case loading search page is slow + $(this).val("loading search results..."); + // Load search page... + let configId = document.getElementById("maprConfig").value; + document.location.href = `search/?query=${configId}:${ui.item.value}`; + return false; + } + }).data("ui-autocomplete")._renderItem = function (ul, item) { + return $("
  • ") + .append("" + item.label + "") + .appendTo(ul); + } \ No newline at end of file diff --git a/idr_gallery/static/idr_gallery/categories.js b/idr_gallery/static/idr_gallery/categories.js index a7005c3b..7a494c2c 100644 --- a/idr_gallery/static/idr_gallery/categories.js +++ b/idr_gallery/static/idr_gallery/categories.js @@ -1,369 +1,319 @@ -"use strict"; - -// Copyright (C) 2019-2020 University of Dundee & Open Microscopy Environment. +// Copyright (C) 2019-2022 University of Dundee & Open Microscopy Environment. // All rights reserved. + // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. + // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. + // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . + // NB: SOURCE FILES are under /src. Compiled files are under /static/ -// loaded below -var mapr_settings = {}; // Model for loading Projects, Screens and their Map Annotations -var model = new StudiesModel(); // ----- event handling -------- +// loaded below +let mapr_settings = {}; -document.getElementById('maprConfig').onchange = function (event) { - document.getElementById('maprQuery').value = ''; - var value = event.target.value.replace('mapr_', ''); - var placeholder = "Type to filter values..."; +// Model for loading Projects, Screens and their Map Annotations +let model = new StudiesModel(); - if (mapr_settings[value]) { - placeholder = "Type ".concat(mapr_settings[value]['default'][0], "..."); - } +model.subscribe('thumbnails', (event, data) => { + // Will get called when each batch of thumbnails is loaded + renderThumbnails(data); +}); - document.getElementById('maprQuery').placeholder = placeholder; // Show all autocomplete options... - $("#maprQuery").focus(); - render(); -}; +let getTooltipContent = (reference) => { + return reference.querySelector(".idr_tooltip").innerHTML; +} -document.getElementById('maprQuery').onfocus = function (event) { - $("#maprQuery").autocomplete("search", event.target.value); -}; // ------ AUTO-COMPLETE ------------------- +function renderStudyContainers(containers) { + return ['screen', 'project'].map(objType => { + let studies = containers[objType]; + let count = studies.length; + if (count == 0) return; + // Link to first Project or Screen + return `${count} ${objType == 'project' ? 'Experiment' : 'Screen'}${count === 1 ? '' : 's'}`; + }).filter(Boolean).join(", "); +} +function studyHtml(study, studyObj) { + let idrId = study.Name.split("-")[0]; + let authors = model.getStudyValue(study, "Publication Authors") || " "; + authors = authors.split(",")[0]; + let title = escapeHTML(getStudyTitle(model, study)); + let pubmed = studyObj["pubmed_id"]; + return ` +
    +
    +
    + ${pubmed ? ` ${authors} et. al ` : ` ${authors} et. al `} +
    +
    ${idrId}
    +
    + +
    +
    ${renderStudyContainers(studyObj)}
    +
    ${imageCount(idrId)}
    +
    + + ${title} + +
    +
    +
    +
    + `} + + +// ------------ Render ------------------------- -function showSpinner() { - document.getElementById('spinner').style.visibility = 'visible'; -} +function render() { -function hideSpinner() { - document.getElementById('spinner').style.visibility = 'hidden'; -} + const groupByType = document.getElementById("groupByType").checked; + document.getElementById('studies').innerHTML = ""; -$("#maprQuery").keyup(function (event) { - if (event.which == 13) { - var configId = document.getElementById("maprConfig").value; - document.location.href = "search/?query=".concat(configId, ":").concat(event.target.value); - } -}).autocomplete({ - autoFocus: false, - delay: 1000, - source: function source(request, response) { - // if configId is not from mapr, we filter on mapValues... - var configId = document.getElementById("maprConfig").value; - - if (configId.indexOf('mapr_') != 0) { - var matches; - - if (configId === 'Name') { - matches = model.getStudiesNames(request.term); - } else if (configId === 'Group') { - matches = model.getStudiesGroups(request.term); - } else { - matches = model.getKeyValueAutoComplete(configId, request.term); + // we group by 'idr00ID' and show Screens and Experiments + let studyContainers = {}; + // go through all Screens and Experiments... + model.studies.forEach(study => { + let idrId = study.Name.split("-")[0]; + if (!studyContainers[idrId]) { + // data for each study: + studyContainers[idrId] = { + 'screen': [], 'project': [], + 'description': "", + 'pubmed_id': "" } - - response(matches); - return; - } // Don't handle empty query for mapr - - - if (request.term.length == 0) { - return; - } // Auto-complete to filter by mapr... - - - configId = configId.replace('mapr_', ''); - var case_sensitive = false; - var requestData = { - case_sensitive: case_sensitive - }; - var url; - - if (request.term.length === 0) { - // Try to list all top-level values. - // This works for 'wild-card' configs where number of values is small e.g. Organism - // But will return empty list for e.g. Gene - url = "".concat(BASE_URL, "mapr/api/").concat(configId, "/"); - requestData.orphaned = true; - } else { - // Find auto-complete matches - url = "".concat(BASE_URL, "mapr/api/autocomplete/").concat(configId, "/"); - requestData.value = case_sensitive ? request.term : request.term.toLowerCase(); - requestData.query = true; // use a 'like' HQL query } + let objType = study.objId.split("-")[0]; // 'screen' or 'project' + studyContainers[idrId][objType].push(study); + studyContainers[idrId]["description"] = model.getStudyDescription(study); + let pubmed = model.getStudyValue(study, 'PubMed ID'); + if (pubmed) { + studyContainers[idrId]["pubmed_id"] = pubmed.split(" ")[1]; + } + }); - showSpinner(); - $.ajax({ - dataType: "json", - type: 'GET', - url: url, - data: requestData, - success: function success(data) { - hideSpinner(); - - if (request.term.length === 0) { - // Top-level terms in 'maps' - if (data.maps && data.maps.length > 0) { - var terms = data.maps.map(function (m) { - return m.id; - }); - terms.sort(); - response(terms); - } - } else if (data.length > 0) { - response($.map(data, function (item) { - return item; - })); - } else { - response([{ - label: 'No results found.', - value: -1 - }]); - } - }, - error: function error(data) { - hideSpinner(); - response([{ - label: 'Error occured.', - value: -1 - }]); + + let idrIds = []; + + let html = ""; + if (!groupByType) { + // Show all studies... + html = model.studies.map(study => { + let idrId = study.Name.split("-")[0]; + // Ignore multiple projects/screens from same study/publication + if (idrIds.includes(idrId)) { + return ''; } + idrIds.push(idrId); + return studyHtml(study, studyContainers[idrId]); + }).join(""); + } else { + // group by Categories + let categories = Object.keys(CATEGORY_QUERIES); + // Sort by index + categories.sort(function (a, b) { + let idxA = CATEGORY_QUERIES[a].index; + let idxB = CATEGORY_QUERIES[b].index; + return (idxA > idxB ? 1 : idxA < idxB ? -1 : 0); }); - }, - minLength: 0, - open: function open() {}, - close: function close() { - // $(this).val(''); - return false; - }, - focus: function focus(event, ui) {}, - select: function select(event, ui) { - if (ui.item.value == -1) { - // Ignore 'No results found' - return false; - } // show temp message in case loading search page is slow - - - $(this).val("loading search results..."); // Load search page... - - var configId = document.getElementById("maprConfig").value; - document.location.href = "search/?query=".concat(configId, ":").concat(ui.item.value); - return false; - } -}).data("ui-autocomplete")._renderItem = function (ul, item) { - return $("
  • ").append("" + item.label + "").appendTo(ul); -}; // ------------ Render ------------------------- - -function render() { - document.getElementById('studies').innerHTML = ""; - var categories = Object.keys(CATEGORY_QUERIES); // Sort by index - - categories.sort(function (a, b) { - var idxA = CATEGORY_QUERIES[a].index; - var idxB = CATEGORY_QUERIES[b].index; - return idxA > idxB ? 1 : idxA < idxB ? -1 : 0; - }); // Link to the study in webclient... - - var linkFunc = function linkFunc(studyData) { - var type = studyData['@type'].split('#')[1].toLowerCase(); - return "".concat(BASE_URL, "webclient/?show=").concat(type, "-").concat(studyData['@id']); - }; - - categories.forEach(function (category) { - var cat = CATEGORY_QUERIES[category]; - var query = cat.query; // Find matching studies - - var matches = model.filterStudiesByMapQuery(query); - if (matches.length == 0) return; - var elementId = cat.label; - var div = document.createElement("div"); // If only ONE category... - - if (categories.length == 1) { - // list studies in a grid, without category.label - div.innerHTML = "
    "); - div.className = "row"; - } else { - div.innerHTML = "\n

    \n ").concat(cat.label, " (").concat(matches.length, ")\n

    \n
    \n
    \n
    \n "); - } + let allIds = []; - document.getElementById('studies').appendChild(div); - matches.forEach(function (study) { - return renderStudy(study, elementId, linkFunc); - }); - }); // Now we iterate all Studies in DOM, loading image ID for link and thumbnail + html = categories.map(category => { - loadStudyThumbnails(); -} + let cat = CATEGORY_QUERIES[category]; + let query = cat.query; -function renderStudy(studyData, elementId, linkFunc) { - // Add Project or Screen to the page - var title; + // Find matching studies + let matches = model.filterStudiesByMapQuery(query); + if (matches.length == 0) return ''; - for (var i = 0; i < TITLE_KEYS.length; i++) { - title = model.getStudyValue(studyData, TITLE_KEYS[i]); + let catIds = []; - if (title) { - break; - } + let catThumbs = matches.map(study => { + let idrId = study.Name.split("-")[0]; + // Ignore multiple projects/screens from same study/publication + if (cat.label !== "Others" && catIds.includes(idrId)) { + return ''; + } + if (cat.label === "Others" && allIds.includes(idrId)) { + return ''; + } + catIds.push(idrId); + allIds.push(idrId); + return studyHtml(study, studyContainers[idrId]); + }).join(""); + + return ` +
    +
    ${cat.label}
    +
    + ${catThumbs} +
    +
    ` + }).join(""); } - if (!title) { - title = studyData.Name; - } + document.getElementById('studies').innerHTML = html; + + // tooltips - NB: updated when thumbnails loaded + tippy('.studyThumb', { + content: getTooltipContent, + trigger: 'mouseenter click', // click to show - eg. on mobile + theme: 'light-border', + offset: [0, 2], + allowHTML: true, + moveTransition: 'transform 2s ease-out', + interactive: true, // allow click + }); +} - var type = studyData['@type'].split('#')[1].toLowerCase(); - var studyLink = linkFunc(studyData); // save for later - studyData.title = title; - var desc = studyData.Description; - var studyDesc; +// --------- Render utils ----------- - if (desc) { - // If description contains title, use the text that follows - if (title.length > 0 && desc.indexOf(title) > -1) { - desc = desc.split(title)[1]; - } // Remove blank lines (and first 'Experiment Description' line) +function imageCount(idrId) { + if (!model.studyStats) return ""; + let containers = model.studyStats[idrId]; + if (!containers) return ""; - studyDesc = desc.split('\n').filter(function (l) { - return l.length > 0; - }).filter(function (l) { - return l !== 'Experiment Description' && l !== 'Screen Description'; - }).join('\n'); + let imgCount = containers.map(row => row["5D Images"]) + .reduce((total, value) => total + parseInt(value, 10), 0); + return new Intl.NumberFormat().format(imgCount) + " Image" + (imgCount != "1" ? "s" : ""); +} - if (studyDesc.indexOf('Version History') > 1) { - studyDesc = studyDesc.split('Version History')[0]; +function renderThumbnails(data) { + // data is {'project-1': {'image':{'id': 2}, 'thumbnail': 'data:image/jpeg;base64,/9j/4AAQSkZ...'}} + for (let id in data) { + let obj_type = id.split('-')[0]; + let obj_id = id.split('-')[1]; + let elements = document.querySelectorAll(`div[data-obj_type="${obj_type}"][data-obj_id="${obj_id}"]`); + // This updates small grid thumbnails and the tooltip images + for (let e = 0; e < elements.length; e++) { + // Find all studies matching the study ID and set src on image + let element = elements[e]; + element.style.backgroundImage = `url(${data[id].thumbnail})`; + // tooltip content is child of this element + let thumb = element.querySelector(".tooltipThumb"); + if (thumb) { + thumb.src = data[id].thumbnail; + } + // add viewer-link for tooltip + let link = element.querySelector(".viewer_link"); + if (link) { + let url = `${BASE_URL}webclient/img_detail/${data[id].image.id}/`; + link.href = url; + } + // update tooltips + if (element._tippy) { + element._tippy.setContent(getTooltipContent(element)); + } } } +} - var shortName = getStudyShortName(studyData); - var authors = model.getStudyValue(studyData, "Publication Authors") || ""; // Function (and template) are defined where used in index.html - - var html = studyHtml({ - studyLink: studyLink, - studyDesc: studyDesc, - shortName: shortName, - title: title, - authors: authors, - BASE_URL: BASE_URL, - type: type - }, studyData); - var div = document.createElement("div"); - div.innerHTML = html; - div.className = "row study "; - div.dataset.obj_type = type; - div.dataset.obj_id = studyData['@id']; - document.getElementById(elementId).appendChild(div); -} // --------- Render utils ----------- - - -function studyHtml(props, studyData) { - var pubmed = model.getStudyValue(studyData, 'PubMed ID'); - - if (pubmed) { - pubmed = pubmed.split(" ")[1]; - } - ; - var author = props.authors.split(',')[0] || ''; - if (author) { - author = "".concat(author, " et al."); - author = author.length > 23 ? author.slice(0, 20) + '...' : author; - } +// ----------- Load / Filter Studies -------------------- - return "\n
    \n ".concat(props.shortName, "\n ").concat(pubmed ? " ").concat(author, "") : author, "\n
    \n
    \n \n
    \n
    \n

    \n ").concat(props.title, "\n

    \n
    \n
    \n ").concat(props.authors, "\n
    \n
    \n
    \n \n \n \n
    \n "); -} +model.loadStudyStats(IDR_STUDIES_URL, function (stats) { + // Load stats and show spinning counters... -function loadStudyThumbnails() { - var ids = []; // Collect study IDs 'project-1', 'screen-2' etc + // In case studies.tsv loading from github fails, show older values + let totalImages = 12840301; + let tbTotal = 307; + let studyCount = 104; - $('div.study').each(function () { - var obj_id = $(this).attr('data-obj_id'); - var obj_type = $(this).attr('data-obj_type'); + if (stats) { + let studyIds = Object.keys(stats); + // remove grouping of containers by idrId + let containers = Object.values(stats).flatMap(containers => containers); - if (obj_id && obj_type) { - ids.push(obj_type + '-' + obj_id); - } - }); // Load images - - model.loadStudiesThumbnails(ids, function (data) { - // data is e.g. { project-1: {thumbnail: base64data, image: {id:1}} } - for (var id in data) { - var obj_type = id.split('-')[0]; - var obj_id = id.split('-')[1]; - var elements = document.querySelectorAll("div[data-obj_type=\"".concat(obj_type, "\"][data-obj_id=\"").concat(obj_id, "\"]")); - - for (var e = 0; e < elements.length; e++) { - // Find all studies matching the study ID and set src on image - var element = elements[e]; - var studyImage = element.querySelector('.studyImage'); - studyImage.style.backgroundImage = "url(".concat(data[id].thumbnail, ")"); // viewer link - - var iid = data[id].image.id; - var link = "".concat(BASE_URL, "webclient/img_detail/").concat(iid, "/"); - element.querySelector('a.viewerLink').href = link; + if (SUPER_CATEGORY) { + try { + // filter studies and containers by cell or tissue + let query = SUPER_CATEGORY.query.split(":"); // e.g. "Sample Type:tissue" + studyIds = studyIds.filter(studyId => stats[studyId].some(row => row[query[0]] == query[1])); + let filtered = containers.filter(row => row[query[0]] == query[1]); + if (filtered.length != 0) { + // in case we filter out everything! + containers = filtered; + } + } catch (error) { + console.log("Failed to filter studies stats by category") } } - }); -} + let imageCounts = containers.map(row => row["5D Images"]); + totalImages = imageCounts.reduce((total, value) => total + parseInt(value, 10), 0); + let tbCounts = containers.map(row => row["Size (TB)"]); + tbTotal = tbCounts.reduce((total, value) => total + parseFloat(value, 10), 0); + studyCount = studyIds.length; + } -function renderStudyKeys() { - if (FILTER_KEYS.length > 0) { - var html = FILTER_KEYS.map(function (key) { - if (key.label && key.value) { - return ""); - } + animateValue(document.getElementById("imageCount"), 0, totalImages, 1500); + animateValue(document.getElementById("tbCount"), 0, tbTotal, 1500); + animateValue(document.getElementById("studyCount"), 0, studyCount, 1500); +}); - return ""); - }).join("\n"); - document.getElementById('studyKeys').innerHTML = html; // Show the and the whole form - document.getElementById('studyKeys').style.display = 'block'; - document.getElementById('search-form').style.display = 'block'; - } -} +async function init() { -renderStudyKeys(); // ----------- Load / Filter Studies -------------------- -// Do the loading and render() when done... + // Do the loading and render() when done... + await model.loadStudies(); -model.loadStudies(function () { // Immediately filter by Super category if (SUPER_CATEGORY && SUPER_CATEGORY.query) { model.studies = model.filterStudiesByMapQuery(SUPER_CATEGORY.query); } + // start loading thumbnails in batches... triggers render() when loaded + model.loadStudiesThumbnails(); + render(); -}); // Load MAPR config - -fetch(BASE_URL + 'mapr/api/config/').then(function (response) { - return response.json(); -}).then(function (data) { - mapr_settings = data; - var options = FILTER_MAPR_KEYS.map(function (key) { - var config = mapr_settings[key]; - - if (config) { - return ""); - } else { - return ""; - } - }); - if (options.length > 0) { - document.getElementById('maprKeys').innerHTML = options.join("\n"); // Show the and the whole form + document.getElementById("groupByType").addEventListener("change", function (event) { + render(); + }) - document.getElementById('maprKeys').style.display = 'block'; - document.getElementById('search-form').style.display = 'block'; - } -})["catch"](function (err) { - console.log("mapr not installed (config not available)"); -}); \ No newline at end of file + + // Load MAPR config + fetch(BASE_URL + 'mapr/api/config/') + .then(response => response.json()) + .then(data => { + mapr_settings = data; + + let options = FILTER_MAPR_KEYS.map(key => { + let config = mapr_settings[key]; + if (config) { + return ``; + } else { + return ""; + } + }); + if (options.length > 0) { + document.getElementById('maprKeys').innerHTML = options.join("\n"); + // Show the and the whole form + document.getElementById('maprKeys').style.display = 'block'; + document.getElementById('search-form').style.display = 'block'; + } + }) + .catch(function (err) { + console.log("mapr not installed (config not available)"); + }); + +} + +init(); diff --git a/idr_gallery/static/idr_gallery/images/logo-idr-dark.svg b/idr_gallery/static/idr_gallery/images/logo-idr-dark.svg new file mode 100644 index 00000000..f329dff5 --- /dev/null +++ b/idr_gallery/static/idr_gallery/images/logo-idr-dark.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/idr_gallery/static/idr_gallery/model.js b/idr_gallery/static/idr_gallery/model.js index 541d084d..7f5f2b2b 100644 --- a/idr_gallery/static/idr_gallery/model.js +++ b/idr_gallery/static/idr_gallery/model.js @@ -1,22 +1,6 @@ "use strict"; -function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } - -function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } - -function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } - -function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } } - -function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); } - -function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } - -function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } - -function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } - -// Copyright (C) 2019-2020 University of Dundee & Open Microscopy Environment. +// Copyright (C) 2019-2022 University of Dundee & Open Microscopy Environment. // All rights reserved. // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as @@ -29,517 +13,526 @@ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // NB: SOURCE FILES are under /src. Compiled files are under /static/ -var StudiesModel = function StudiesModel() { - "use strict"; - this.base_url = BASE_URL; - this.studies = []; - this.images = {}; - return this; -}; +class StudiesModel { + constructor() { + this.base_url = BASE_URL; + this.studies = []; + this.images = {}; + this.pubSubObject = $({}); + return this; + } -StudiesModel.prototype.getStudyById = function getStudyById(typeId) { - // E.g. 'project-1', or 'screen-2' - var objType = typeId.split('-')[0]; - var id = typeId.split('-')[1]; + subscribe() { + this.pubSubObject.on.apply(this.pubSubObject, arguments); + } - for (var i = 0; i < this.studies.length; i++) { - var study = this.studies[i]; + unsubscribe() { + this.pubSubObject.off.apply(this.pubSubObject, arguments); + } - if (study['@id'] == id && study['@type'].split('#')[1].toLowerCase() == objType) { - return study; - } + publish() { + this.pubSubObject.trigger.apply(this.pubSubObject, arguments); } -}; -StudiesModel.prototype.getStudiesNames = function getStudiesNames(filterQuery) { - var names = this.studies.map(function (s) { - return s.Name; - }); + getStudyById(typeId) { + // E.g. 'project-1', or 'screen-2' + var objType = typeId.split('-')[0]; + var id = typeId.split('-')[1]; - if (filterQuery) { - names = names.filter(function (name) { - return name.toLowerCase().indexOf(filterQuery) > -1; - }); - } + for (var i = 0; i < this.studies.length; i++) { + var study = this.studies[i]; - names.sort(function (a, b) { - return a.toLowerCase() > b.toLowerCase() ? 1 : -1; - }); - return names; -}; + if (study['@id'] == id && study['@type'].split('#')[1].toLowerCase() == objType) { + return study; + } + } + } -StudiesModel.prototype.getStudiesGroups = function getStudiesGroups(filterQuery) { - var names = []; - this.studies.forEach(function (study) { - var groupName = study['omero:details'].group.Name; + getStudiesNames(filterQuery) { + var names = this.studies.map(function (s) { + return s.Name; + }); - if (names.indexOf(groupName) === -1) { - names.push(groupName); + if (filterQuery) { + names = names.filter(function (name) { + return name.toLowerCase().indexOf(filterQuery) > -1; + }); } - }); - if (filterQuery) { - names = names.filter(function (name) { - return name.toLowerCase().indexOf(filterQuery) > -1; + names.sort(function (a, b) { + return a.toLowerCase() > b.toLowerCase() ? 1 : -1; }); + return names; } - names.sort(function (a, b) { - return a.toLowerCase() > b.toLowerCase() ? 1 : -1; - }); - return names; -}; - -StudiesModel.prototype.getStudyValue = function getStudyValue(study, key) { - if (!study.mapValues) return; + getStudiesGroups(filterQuery) { + var names = []; + this.studies.forEach(function (study) { + var groupName = study['omero:details'].group.Name; - for (var i = 0; i < study.mapValues.length; i++) { - var kv = study.mapValues[i]; + if (names.indexOf(groupName) === -1) { + names.push(groupName); + } + }); - if (kv[0] === key) { - return kv[1]; + if (filterQuery) { + names = names.filter(function (name) { + return name.toLowerCase().indexOf(filterQuery) > -1; + }); } - } -}; -StudiesModel.prototype.getStudyValues = function getStudyValues(study, key) { - if (!study.mapValues) { - return []; + names.sort(function (a, b) { + return a.toLowerCase() > b.toLowerCase() ? 1 : -1; + }); + return names; } - var matches = []; + getStudyValue(study, key) { + if (!study.mapValues) return; - for (var i = 0; i < study.mapValues.length; i++) { - var kv = study.mapValues[i]; + for (var i = 0; i < study.mapValues.length; i++) { + var kv = study.mapValues[i]; - if (kv[0] === key) { - matches.push(kv[1]); + if (kv[0] === key) { + return kv[1]; + } } } - return matches; -}; - -StudiesModel.prototype.getKeyValueAutoComplete = function getKeyValueAutoComplete(key, inputText) { - var _this = this; + getStudyTitle(study) { + var title; - inputText = inputText.toLowerCase(); // Get values for key from each study + for (var i = 0; i < TITLE_KEYS.length; i++) { + title = model.getStudyValue(study, TITLE_KEYS[i]); + if (title) { + break; + } + } + if (!title) { + title = study.Name; + } + return title; + } - var values = []; - this.studies.forEach(function (study) { - var v = _this.getStudyValues(study, key); + getStudyDescription(study, title) { - for (var i = 0; i < v.length; i++) { - values.push(v[i]); + if (!title) { + title = this.getStudyTitle(study); } - }); // We want values that match inputText - // Except for "Publication Authors", where we want words - // Create dict of {lowercaseValue: origCaseValue} + let desc = study.Description; + let studyDesc = ""; + if (desc) { + // If description contains title, use the text that follows + if (title.length > 0 && desc.indexOf(title) > -1) { + desc = desc.split(title)[1]; + } + // Remove blank lines (and first 'Experiment Description' line) + studyDesc = desc.split('\n').filter(function (l) { + return l.length > 0; + }).filter(function (l) { + return l !== 'Experiment Description' && l !== 'Screen Description'; + }).join('\n'); + + if (studyDesc.indexOf('Version History') > 1) { + studyDesc = studyDesc.split('Version History')[0]; + } + } + return studyDesc; + } - var matchCounts = values.reduce(function (prev, value) { + getStudyValues(study, key) { + if (!study.mapValues) { + return []; + } var matches = []; - - if (key == "Publication Authors") { - // Split surnames, ignoring AN initials. - var names = value.split(/,| and | & /).map(function (n) { - // Want the surname from e.g. 'Jan Ellenberg' or 'Held M' or 'Øyvind Ødegård-Fougner' - var words = n.split(" ").filter(function (w) { - return w.match(/[a-z]/g); - }); - if (words && words.length == 1) return words[0]; // Surname only - - return words && words.length > 1 ? words.slice(1).join(" ") : ''; - }).filter(function (w) { - return w.length > 0; - }); - matches = names.filter(function (name) { - return name.toLowerCase().indexOf(inputText) > -1; - }); - } else if (value.toLowerCase().indexOf(inputText) > -1) { - matches.push(value); + for (var i = 0; i < study.mapValues.length; i++) { + var kv = study.mapValues[i]; + if (kv[0] === key) { + matches.push(kv[1]); + } } + return matches; + } - matches.forEach(function (match) { - if (!prev[match.toLowerCase()]) { - // key is lowercase, value is original case - prev[match.toLowerCase()] = { - value: match, - count: 0 - }; - } // also keep count of matches - - - prev[match.toLowerCase()].count++; - }); - return prev; - }, {}); // Make into list and sort by: - // match at start of phrase > match at start of word > other match - - var matchList = []; - - for (key in matchCounts) { - var matchScore = 1; - - if (key.indexOf(inputText) == 0) { - // best match if our text STARTS WITH inputText - matchScore = 3; - } else if (key.indexOf(" " + inputText) > -1) { - // next best if a WORD starts with inputText - matchScore = 2; - } // Make a list of sort score, orig text (NOT lowercase keys) and count - - - matchList.push([matchScore, matchCounts[key].value, matchCounts[key].count]); - } // Sort by the matchScore (hightest first) - + getKeyValueAutoComplete(key, inputText) { + var _this = this; - matchList.sort(function (a, b) { - if (a[0] < b[0]) return 1; - if (a[0] > b[0]) return -1; // equal score. Sort by value (lowest first) + inputText = inputText.toLowerCase(); // Get values for key from each study - if (a[1].toLowerCase() > b[1].toLowerCase()) return 1; - return -1; - }); // Return the matches + var values = []; + this.studies.forEach(function (study) { + var v = _this.getStudyValues(study, key); - return matchList.map(function (m) { - // Auto-complete uses {label: 'X (n)', value: 'X'} - return { - label: "".concat(m[1], " (").concat(m[2], ")"), - value: m[1] - }; - }).filter(function (m) { - return m.value.length > 0; - }); -}; - -StudiesModel.prototype.loadStudies = function loadStudies(callback) { - var _this2 = this; - - // Load Projects AND Screens, sort them and render... - Promise.all([fetch(this.base_url + "api/v0/m/projects/?childCount=true"), fetch(this.base_url + "api/v0/m/screens/?childCount=true")]).then(function (responses) { - return Promise.all(responses.map(function (res) { - return res.json(); - })); - }).then(function (_ref) { - var _ref2 = _slicedToArray(_ref, 2), - projects = _ref2[0], - screens = _ref2[1]; - - _this2.studies = projects.data; - _this2.studies = _this2.studies.concat(screens.data); // ignore empty studies with no images - - _this2.studies = _this2.studies.filter(function (study) { - return study['omero:childCount'] > 0; - }); // sort by name, reverse - - _this2.studies.sort(function (a, b) { - var nameA = a.Name.toUpperCase(); - var nameB = b.Name.toUpperCase(); - - if (nameA < nameB) { - return 1; + for (var i = 0; i < v.length; i++) { + values.push(v[i]); + } + }); // We want values that match inputText + // Except for "Publication Authors", where we want words + // Create dict of {lowercaseValue: origCaseValue} + + var matchCounts = values.reduce(function (prev, value) { + var matches = []; + + if (key == "Publication Authors") { + // Split surnames, ignoring AN initials. + var names = value.split(/,| and | & /).map(function (n) { + // Want the surname from e.g. 'Jan Ellenberg' or 'Held M' or 'Øyvind Ødegård-Fougner' + var words = n.split(" ").filter(function (w) { + return w.match(/[a-z]/g); + }); + if (words && words.length == 1) return words[0]; // Surname only + + return words && words.length > 1 ? words.slice(1).join(" ") : ''; + }).filter(function (w) { + return w.length > 0; + }); + matches = names.filter(function (name) { + return name.toLowerCase().indexOf(inputText) > -1; + }); + } else if (value.toLowerCase().indexOf(inputText) > -1) { + matches.push(value); } - if (nameA > nameB) { - return -1; - } // names must be equal + matches.forEach(function (match) { + if (!prev[match.toLowerCase()]) { + // key is lowercase, value is original case + prev[match.toLowerCase()] = { + value: match, + count: 0 + }; + } // also keep count of matches + prev[match.toLowerCase()].count++; + }); + return prev; + }, {}); // Make into list and sort by: + // match at start of phrase > match at start of word > other match - return 0; - }); // load Map Anns for Studies... + var matchList = []; + for (key in matchCounts) { + var matchScore = 1; - _this2.loadStudiesMapAnnotations(callback); - })["catch"](function (err) { - console.error(err); - }); -}; + if (key.indexOf(inputText) == 0) { + // best match if our text STARTS WITH inputText + matchScore = 3; + } else if (key.indexOf(" " + inputText) > -1) { + // next best if a WORD starts with inputText + matchScore = 2; + } // Make a list of sort score, orig text (NOT lowercase keys) and count -StudiesModel.prototype.loadStudiesThumbnails = function loadStudiesThumbnails(ids, callback) { - var _this3 = this; + matchList.push([matchScore, matchCounts[key].value, matchCounts[key].count]); + } // Sort by the matchScore (hightest first) - var url = GALLERY_INDEX + "gallery-api/thumbnails/"; // remove duplicates - ids = _toConsumableArray(new Set(ids)); // find any thumbnails we already have in hand... + matchList.sort(function (a, b) { + if (a[0] < b[0]) return 1; + if (a[0] > b[0]) return -1; // equal score. Sort by value (lowest first) - var found = {}; - var toFind = []; - ids.forEach(function (id) { - var study = _this3.getStudyById(id); + if (a[1].toLowerCase() > b[1].toLowerCase()) return 1; + return -1; + }); // Return the matches - if (study && study.image && study.thumbnail) { - found[id] = { - image: study.image, - thumbnail: study.thumbnail + return matchList.map(function (m) { + // Auto-complete uses {label: 'X (n)', value: 'X'} + return { + label: "".concat(m[1], " (").concat(m[2], ")"), + value: m[1] }; - } else { - toFind.push(id); - } - }); - - if (Object.keys(found).length > 0) { - callback(found); - } - - toFind = toFind.map(function (id) { - return id.replace('-', '='); - }); - var batchSize = 10; - - while (toFind.length > 0) { - var data = toFind.slice(0, batchSize).join("&"); - fetch(url + '?' + data).then(function (response) { - return response.json(); - }).then(function (data) { - for (var studyId in data) { - var study = _this3.getStudyById(studyId); - - if (data[studyId]) { - study.image = data[studyId].image; - study.thumbnail = data[studyId].thumbnail; - } - } - - if (callback) { - callback(data); - } + }).filter(function (m) { + return m.value.length > 0; }); - toFind = toFind.slice(batchSize); } -}; - -StudiesModel.prototype.loadStudiesMapAnnotations = function loadStudiesMapAnnotations(callback) { - var _this4 = this; - - var url = this.base_url + "webclient/api/annotations/?type=map"; - var data = this.studies.map(function (study) { - return "".concat(study['@type'].split('#')[1].toLowerCase(), "=").concat(study['@id']); - }).join("&"); - url += '&' + data; - fetch(url).then(function (response) { - return response.json(); - }).then(function (data) { - // populate the studies array... - // dict of {'project-1' : key-values} - var annsByParentId = {}; - data.annotations.forEach(function (ann) { - var key = ann.link.parent["class"]; // 'ProjectI' - - key = key.substr(0, key.length - 1).toLowerCase(); - key += '-' + ann.link.parent.id; // project-1 - - if (!annsByParentId[key]) { - annsByParentId[key] = []; - } - annsByParentId[key] = annsByParentId[key].concat(ann.values); - }); // Add mapValues to studies... + async loadStudies() { + // Load Projects AND Screens, sort them and render... + await Promise.all([ + fetch(this.base_url + "api/v0/m/projects/?childCount=true"), + fetch(this.base_url + "api/v0/m/screens/?childCount=true"), + ]).then(responses => + Promise.all(responses.map(res => res.json())) + ).then(([projects, screens]) => { + this.studies = projects.data; + this.studies = this.studies.concat(screens.data); + + // ignore empty studies with no images + this.studies = this.studies.filter(study => study['omero:childCount'] > 0); + + // sort by name, reverse + this.studies.sort(function (a, b) { + var nameA = a.Name.toUpperCase(); + var nameB = b.Name.toUpperCase(); + if (nameA < nameB) { + return 1; + } + if (nameA > nameB) { + return -1; + } - _this4.studies = _this4.studies.map(function (study) { - // Also set 'type':'screen', 'objId': 'screen-123' - study.type = study['@type'].split('#')[1].toLowerCase(); - study.id = study['@id']; - study.objId = "".concat(study.type, "-").concat(study['@id']); - var values = annsByParentId[study.objId]; + // names must be equal + return 0; + }); - if (values) { - study.mapValues = values; + }).catch((err) => { + console.error(err); + }); - var releaseDate = _this4.getStudyValue(study, 'Release Date'); + // Load Map Annotations + await this.loadStudiesMapAnnotations(); + } - if (releaseDate) { - study.date = new Date(releaseDate); + loadStudiesThumbnails() { + let url = GALLERY_INDEX + "gallery-api/thumbnails/"; + + let toFind = this.studies.map(study => study.objId.replace("-", "=")); + let batchSize = 10; + while (toFind.length > 0) { + let data = toFind.slice(0, batchSize).join("&"); + fetch(url + '?' + data) + .then(response => response.json()) + .then(data => { + for (let studyId in data) { + let study = this.getStudyById(studyId); + if (data[studyId]) { + study.image = data[studyId].image; + study.thumbnail = data[studyId].thumbnail; + } + } + this.publish("thumbnails", data); + }); + toFind = toFind.slice(batchSize); + } + } - if (isNaN(study.date.getTime())) { - study.date = undefined; + async loadStudiesMapAnnotations() { + let url = this.base_url + "webclient/api/annotations/?type=map"; + let data = this.studies + .map(study => `${study['@type'].split('#')[1].toLowerCase()}=${study['@id']}`) + .join("&"); + url += '&' + data; + await fetch(url) + .then(response => response.json()) + .then(data => { + // populate the studies array... + // dict of {'project-1' : key-values} + let annsByParentId = {}; + data.annotations.forEach(ann => { + let key = ann.link.parent.class; // 'ProjectI' + key = key.substr(0, key.length - 1).toLowerCase(); + key += '-' + ann.link.parent.id; // project-1 + if (!annsByParentId[key]) { + annsByParentId[key] = []; } - } - } + annsByParentId[key] = annsByParentId[key].concat(ann.values); + }); + // Add mapValues to studies... + this.studies = this.studies.map(study => { + // Also set 'type':'screen', 'objId': 'screen-123' + study.type = study['@type'].split('#')[1].toLowerCase(); + study.id = study['@id']; + study.objId = `${study.type}-${study['@id']}`; + let values = annsByParentId[study.objId]; + if (values) { + study.mapValues = values; + let releaseDate = this.getStudyValue(study, 'Release Date'); + if (releaseDate) { + study.date = new Date(releaseDate); + if (isNaN(study.date.getTime())) { + study.date = undefined; + } + } + } + return study; + }); - return study; - }); + }) + } - if (callback) { - callback(); + filterStudiesAnyText(text) { + // Search for studies with text in their keys, values, or description. + // Returns a list of matching studies. Each study is returned along with text matches + // [study, ["key: value", "description"]] + let regexes = text.split(" ").map(token => new RegExp(token, "i")) + function matchSome(str) { + return regexes.some(re => re.test(str)); } + const re = new RegExp(text, "i") + return this.studies.map(study => { + let mapValues = []; + if (study.mapValues) { + mapValues = study.mapValues.map(kv => kv.join(": ")); + } + let studyStrings = mapValues.concat([study.Description]); - ; - }); -}; - -StudiesModel.prototype.filterStudiesByMapQuery = function filterStudiesByMapQuery(query) { - if (query.startsWith("FIRST") || query.startsWith("LAST")) { - // E.g. query is 'FIRST10:date' sort by 'date' and return first 10 - var limit = parseInt(query.replace('FIRST', '').replace('LAST', '')); - var attr = query.split(':')[1]; - var desc = query.startsWith("FIRST") ? -1 : 1; // first filter studies, remove those that don't have 'attr' - - var sorted = this.studies.filter(function (study) { - return study[attr] !== undefined; - }).sort(function (a, b) { - var aVal = a[attr]; - var bVal = b[attr]; // If string, use lowercase - - aVal = aVal.toLowerCase ? aVal.toLowerCase() : aVal; - bVal = bVal.toLowerCase ? bVal.toLowerCase() : bVal; - return aVal < bVal ? desc : aVal > bVal ? -desc : 0; - }); - return sorted.slice(0, limit); + // we want ALL the search tokens to match at least somewhere in studyStrings + let match = regexes.every(re => studyStrings.some(str => re.test(str))) + if (!match) return; + + // return [study, "key: value string showing matching text"] + let matches = mapValues.filter(matchSome); + if (matchSome(study.Description)) { + matches.push(study.Description); + } + return [study, matches] + }).filter(Boolean); } - var matches = this.studies.filter(function (study) { - // If no key-values loaded, filter out - if (!study.mapValues) { - return false; + filterStudiesByMapQuery(query) { + if (query.startsWith("FIRST") || query.startsWith("LAST")) { + // E.g. query is 'FIRST10:date' sort by 'date' and return first 10 + var limit = parseInt(query.replace('FIRST', '').replace('LAST', '')); + var attr = query.split(':')[1]; + var desc = query.startsWith("FIRST") ? -1 : 1; // first filter studies, remove those that don't have 'attr' + + var sorted = this.studies.filter(function (study) { + return study[attr] !== undefined; + }).sort(function (a, b) { + var aVal = a[attr]; + var bVal = b[attr]; // If string, use lowercase + + aVal = aVal.toLowerCase ? aVal.toLowerCase() : aVal; + bVal = bVal.toLowerCase ? bVal.toLowerCase() : bVal; + return aVal < bVal ? desc : aVal > bVal ? -desc : 0; + }); + return sorted.slice(0, limit); } - var match = false; // first split query by AND and OR + var matches = this.studies.filter(function (study) { + // If no key-values loaded, filter out + if (!study.mapValues) { + return false; + } + + var match = false; // first split query by AND and OR - var ors = query.split(' OR '); - ors.forEach(function (term) { - var allAnds = true; - var ands = term.split(' AND '); - ands.forEach(function (mustMatch) { - var queryKeyValue = mustMatch.split(":"); - var valueMatch = false; // check all key-values (may be duplicate keys) for value that matches + var ors = query.split(' OR '); + ors.forEach(function (term) { + var allAnds = true; + var ands = term.split(' AND '); + ands.forEach(function (mustMatch) { + var queryKeyValue = mustMatch.split(":"); + var valueMatch = false; // check all key-values (may be duplicate keys) for value that matches - for (var i = 0; i < study.mapValues.length; i++) { - var kv = study.mapValues[i]; + for (var i = 0; i < study.mapValues.length; i++) { + var kv = study.mapValues[i]; - if (kv[0] === queryKeyValue[0]) { - var value = queryKeyValue[1].trim(); + if (kv[0] === queryKeyValue[0]) { + var value = queryKeyValue[1].trim(); - if (value.substr(0, 4) === 'NOT ') { - value = value.replace('NOT ', ''); + if (value.substr(0, 4) === 'NOT ') { + value = value.replace('NOT ', ''); - if (kv[1].toLowerCase().indexOf(value.toLowerCase()) == -1) { + if (kv[1].toLowerCase().indexOf(value.toLowerCase()) == -1) { + valueMatch = true; + } + } else if (kv[1].toLowerCase().indexOf(value.toLowerCase()) > -1) { valueMatch = true; } - } else if (kv[1].toLowerCase().indexOf(value.toLowerCase()) > -1) { - valueMatch = true; } } - } // if not found, then our AND term fails - + // if not found, then our AND term fails + if (!valueMatch) { + allAnds = false; + } + }); - if (!valueMatch) { - allAnds = false; + if (allAnds) { + match = true; } }); - - if (allAnds) { - match = true; - } + return match; }); - return match; - }); - return matches; -}; - -StudiesModel.prototype.loadImage = function loadImage(obj_type, obj_id, callback) { - var _this5 = this; + return matches; + } - // Get a sample image ID for 'screen' or 'project' - var key = "".concat(obj_type, "-").concat(obj_id); // check cache + loadImage(obj_type, obj_id, callback) { + // Get a sample image ID for 'screen' or 'project' + let key = `${obj_type}-${obj_id}`; - if (this.images[key]) { - callback(this.images[key]); - return; - } + // check cache + if (this.images[key]) { + callback(this.images[key]); + return; + } - var limit = 20; - - if (obj_type == 'screen') { - var url = "".concat(this.base_url, "api/v0/m/screens/").concat(obj_id, "/plates/"); - url += '?limit=1'; // just get first plate - - fetch(url).then(function (response) { - return response.json(); - }).then(function (data) { - obj = data.data[0]; // Jump into the 'middle' of plate to make sure Wells have images - // NB: Some plates don't have Well at each Row/Column spot. Well_count < Rows * Cols * 0.5 - - var offset = Math.max(0, parseInt(obj.Rows * obj.Columns * 0.25) - limit); - var url = "".concat(_this5.base_url, "api/v0/m/plates/").concat(obj['@id'], "/wells/?limit=").concat(limit, "&offset=").concat(offset); - return fetch(url); - }).then(function (response) { - return response.json(); - }).then(function (data) { - var wellSample; - - for (var w = 0; w < data.data.length; w++) { - if (data.data[w].WellSamples) { - wellSample = data.data[w].WellSamples[0]; + let url = `${GALLERY_INDEX}gallery-api/${obj_type}s/${obj_id}/images/?limit=1` + fetch(url) + .then(response => response.json()) + .then(data => { + let images = data.data; + if (images.length > 0) { + this.images[key] = images[0] } - } - - if (!wellSample) { - console.log('No WellSamples in first Wells!', data); + callback(this.images[key]); return; - } - - _this5.images[key] = wellSample.Image; - callback(_this5.images[key]); - return; - }); - } else if (obj_type == 'project') { - var _url = "".concat(this.base_url, "api/v0/m/projects/").concat(obj_id, "/datasets/"); + }) + } - _url += '?limit=1'; // just get first plate + loadStudyStats = function (url, callback) { + let self = this; + $.get(url, function (data) { + let tsvRows = data.split('\n'); + let columns; + // read tsv => dicts + let rowsAsObj = tsvRows.map(function (row, count) { + let values = row.split('\t'); + if (count == 0) { + columns = values; + return; + } + if (values.length === 0) return; + let row_data = {}; + for (let c = 0; c < values.length; c++) { + if (c < columns.length) { + row_data[columns[c]] = values[c]; + } + } + return row_data + }).filter(Boolean); + + // Group rows by Study + let stats = {}; + rowsAsObj.forEach(row => { + let studyName = row["Study"]; + if (!studyName) return; + let studyId = studyName.split("-")[0]; + if (!stats[studyId]) { + stats[studyId] = []; + } + stats[studyId].push(row); + }); - fetch(_url).then(function (response) { - return response.json(); - }).then(function (data) { - obj = data.data[0]; + self.studyStats = stats; - if (!obj) { - // No Dataset in Project: ' + obj_id; - return; + if (callback) { + callback(stats); } - - var url = "".concat(_this5.base_url, "api/v0/m/datasets/").concat(obj['@id'], "/images/?limit=1"); - return fetch(url); - }) // Handle undefined if no Datasets in Project... - .then(function (response) { - return response ? response.json() : {}; - }).then(function (data) { - if (data && data.data && data.data[0]) { - var image = data.data[0]; - _this5.images[key] = image; - callback(_this5.images[key]); + }).fail(function () { + console.log("Failed to load studies.tsv") + if (callback) { + callback(); } - })["catch"](function (error) { - console.error("Error loading Image for Project: " + obj_id, error); }); } -}; - -StudiesModel.prototype.getStudyImage = function getStudyImage(obj_type, obj_id, callback) { - var _this6 = this; - - // Get a sample image ID for 'screen' or 'project' - var key = "".concat(obj_type, "-").concat(obj_id); // check cache - - if (this.images[key]) { - callback(this.images[key]); - return; - } - - var url = "".concat(GALLERY_INDEX, "gallery-api/").concat(obj_type, "s/").concat(obj_id, "/images/?limit=1"); - fetch(url).then(function (response) { - return response.json(); - }).then(function (data) { - var images = data.data; +} - if (images.length > 0) { - _this6.images[key] = images[0]; +function animateValue(obj, start, end, duration) { + // https://css-tricks.com/animating-number-counters/ + let startTimestamp = null; + const step = (timestamp) => { + if (!startTimestamp) startTimestamp = timestamp; + let progress = Math.min((timestamp - startTimestamp) / duration, 1); + // If we want easing... + // progress = Math.sin(Math.PI * progress / 2); + let number = Math.floor(progress * (end - start) + start); + obj.innerHTML = new Intl.NumberFormat().format(number); + if (progress < 1) { + window.requestAnimationFrame(step); } - - callback(_this6.images[key]); - return; - }); -}; + }; + window.requestAnimationFrame(step); +} function toTitleCase(text) { if (!text || text.length == 0) return text; @@ -624,4 +617,32 @@ if (typeof Object.assign !== 'function') { writable: true, configurable: true }); -} \ No newline at end of file +} + + +function getStudyTitle(model, study) { + let title; + for (let i = 0; i < TITLE_KEYS.length; i++) { + title = model.getStudyValue(study, TITLE_KEYS[i]); + if (title) { + break; + } + } + if (!title) { + title = studyData.Name; + } + return title; +} + +const escapeHTML = str => + str.replace( + /[&<>'"]/g, + tag => + ({ + '&': '&', + '<': '<', + '>': '>', + "'": ''', + '"': '"' + }[tag] || tag) + ); diff --git a/idr_gallery/static/idr_gallery/search.js b/idr_gallery/static/idr_gallery/search.js index fa8ff60e..91db7dd4 100644 --- a/idr_gallery/static/idr_gallery/search.js +++ b/idr_gallery/static/idr_gallery/search.js @@ -1,399 +1,376 @@ -"use strict"; - // Copyright (C) 2019-2020 University of Dundee & Open Microscopy Environment. // All rights reserved. + // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. + // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. + // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . + // NB: SOURCE FILES are under /src. Compiled files are under /static/ + // Model for loading Projects, Screens and their Map Annotations -var model = new StudiesModel(); -var mapr_settings; - -function renderStudyKeys() { - if (FILTER_KEYS.length > 0) { - var html = FILTER_KEYS.map(function (key) { - if (key.label && key.value) { - return ""); - } +let model = new StudiesModel(); - return ""); - }).join("\n"); - document.getElementById('studyKeys').innerHTML = html; // Show the and the whole form +model.subscribe('thumbnails', (event, data) => { + // Will get called when each batch of thumbnails is loaded + renderThumbnails(data); +}); - document.getElementById('studyKeys').style.display = 'block'; - document.getElementById('search-form').style.display = 'block'; - } -} +let mapr_settings; -renderStudyKeys(); // FIRST, populate forms from query string +// FIRST, populate forms from query string function populateInputsFromSearch() { - var search = window.location.search.substr(1); - var query = ''; + let search = window.location.search.substr(1); + let query = ''; var searchParams = search.split('&'); - for (var i = 0; i < searchParams.length; i++) { var paramSplit = searchParams[i].split('='); - if (paramSplit[0] === 'query') { query = paramSplit[1].replace(/%20/g, " "); } } - if (query) { - var splitIndex = query.indexOf(':'); - var configId = query.slice(0, splitIndex); - var value = query.slice(splitIndex + 1); - + let splitIndex = query.indexOf(':'); + let configId = query.slice(0, splitIndex); + let value = query.slice(splitIndex + 1); if (configId && value) { document.getElementById("maprConfig").value = configId; document.getElementById("maprQuery").value = value; - var key = configId.replace('mapr_', ''); - var placeholder = "Type to filter values..."; - + let key = configId.replace('mapr_', ''); + let placeholder = `Type to filter values...`; if (mapr_settings && mapr_settings[key]) { - placeholder = "Type ".concat(mapr_settings[key]['default'][0], "..."); + placeholder = `Type ${mapr_settings[key]['default'][0]}...`; } - document.getElementById('maprQuery').placeholder = placeholder; } } } +populateInputsFromSearch(); -populateInputsFromSearch(); // ------------ Handle MAPR searching or filtering --------------------- + +// ------------ Handle MAPR searching or filtering --------------------- function filterStudiesByMapr(value) { $('#studies').removeClass('studiesLayout'); - var configId = document.getElementById("maprConfig").value.replace("mapr_", ""); + let configId = document.getElementById("maprConfig").value.replace("mapr_", ""); document.getElementById('studies').innerHTML = ""; - var key = mapr_settings[value] ? mapr_settings[value].all.join(" or ") : value; - showFilterSpinner("Finding images with ".concat(configId, ": ").concat(value, "...")); // Get all terms that match (NOT case_sensitive) + let key = mapr_settings[value] ? mapr_settings[value].all.join(" or ") : value; + showFilterSpinner(`Finding images with ${configId}: ${value}...`); - var url = "".concat(BASE_URL, "mapr/api/").concat(configId, "/?value=").concat(value, "&case_sensitive=false&orphaned=true"); - $.getJSON(url, function (data) { - var maprTerms = data.maps.map(function (term) { - return term.id; - }); - var termUrls = maprTerms.map(function (term) { - return "".concat(BASE_URL, "mapr/api/").concat(configId, "/?value=").concat(term); - }); // Get results for All terms - - Promise.all(termUrls.map(function (url) { - return fetch(url); - })).then(function (responses) { - return Promise.all(responses.map(function (res) { - return res.json(); - })); - }).then(function (responses) { - hideFilterSpinner(); // filter studies by each response - - var studiesByTerm = responses.map(function (data) { - return filterStudiesByMaprResponse(data); - }); - renderMaprMessage(studiesByTerm, maprTerms); // Show table for each... + // Get all terms that match (NOT case_sensitive) + let url = `${BASE_URL}mapr/api/${configId}/?value=${value}&case_sensitive=false&orphaned=true`; + $.getJSON(url, (data) => { - studiesByTerm.forEach(function (studies, idx) { - if (studies.length > 0) { - renderMaprResultsTable(studies, maprTerms[idx]); - } - }); - }); // .fail(() => { + let maprTerms = data.maps.map(term => term.id); + let termUrls = maprTerms.map(term => `${BASE_URL}mapr/api/${configId}/?value=${term}`); + + // Get results for All terms + Promise.all(termUrls.map(url => fetch(url))) + .then(responses => Promise.all(responses.map(res => res.json()))) + .then((responses) => { + hideFilterSpinner(); + + // filter studies by each response + let studiesByTerm = responses.map(data => filterStudiesByMaprResponse(data)); + + renderMaprMessage(studiesByTerm, maprTerms); + + // Show table for each... + studiesByTerm.forEach((studies, idx) => { + if (studies.length > 0) { + renderMaprResultsTable(studies, maprTerms[idx]); + } + }); + }) + // .fail(() => { // document.getElementById('filterCount').innerHTML = "Request failed. Server may be busy." // }) }); } + function filterStudiesByMaprResponse(data) { // filter studies by 'screens' and 'projects' - var imageCounts = {}; - data.screens.forEach(function (s) { - imageCounts["screen-".concat(s.id)] = s.extra.counter; - }); - data.projects.forEach(function (s) { - imageCounts["project-".concat(s.id)] = s.extra.counter; - }); + let imageCounts = {}; + data.screens.forEach(s => { imageCounts[`screen-${s.id}`] = s.extra.counter }); + data.projects.forEach(s => { imageCounts[`project-${s.id}`] = s.extra.counter }); - var filterFunc = function filterFunc(study) { - var studyId = study['@type'].split('#')[1].toLowerCase() + '-' + study['@id']; + let filterFunc = study => { + let studyId = study['@type'].split('#')[1].toLowerCase() + '-' + study['@id']; return imageCounts.hasOwnProperty(studyId); - }; + } - var filteredStudies = model.studies.filter(filterFunc).map(function (study) { - var studyId = study['@type'].split('#')[1].toLowerCase() + '-' + study['@id']; - var studyData = Object.assign({}, study); + let filteredStudies = model.studies.filter(filterFunc).map(study => { + let studyId = study['@type'].split('#')[1].toLowerCase() + '-' + study['@id']; + let studyData = Object.assign({}, study); studyData.imageCount = imageCounts[studyId]; return studyData; }); return filteredStudies; } + function renderMaprMessage(studiesByTerm, maprTerms) { // for each term e.g. TOP2, top2 etc sum image counts from each study - var imageCount = studiesByTerm.reduce(function (count, studies) { - return count + studies.reduce(function (count, study) { - return count + study.imageCount; - }, 0); - }, 0); - var studyCount = studiesByTerm.reduce(function (count, studies) { - return count + studies.length; + let imageCount = studiesByTerm.reduce((count, studies) => { + return count + studies.reduce((count, study) => count + study.imageCount, 0); }, 0); - var terms = maprTerms.join('/'); - var filterMessage = ""; + let studyCount = studiesByTerm.reduce((count, studies) => count + studies.length, 0); + let terms = maprTerms.join('/'); + let filterMessage = ""; if (studyCount === 0) { filterMessage = noStudiesMessage(); } else { - var configId = document.getElementById("maprConfig").value.replace('mapr_', ''); - var key = configId; - + let configId = document.getElementById("maprConfig").value.replace('mapr_', ''); + let key = configId; if (mapr_settings && mapr_settings[configId]) { key = mapr_settings[key].label; } - - filterMessage = "

    \n Found ".concat(imageCount, " images with\n ").concat(key, ": ").concat(terms, "\n in ").concat(studyCount, " stud").concat(studyCount == 1 ? 'y' : 'ies', "

    "); + filterMessage = `

    + Found ${imageCount} images with + ${key}: ${terms} + in ${studyCount} stud${studyCount == 1 ? 'y' : 'ies'}

    `; } - document.getElementById('filterCount').innerHTML = filterMessage; } + function renderMaprResultsTable(maprData, term) { - var configId = document.getElementById("maprConfig").value.replace("mapr_", ""); - var elementId = 'maprResultsTable' + term; - var html = "\n

    ".concat(term, "

    \n \n \n \n \n \n \n \n \n \n \n \n
    Study IDOrganismImage countTitleSample ImagesLink
    "); + let configId = document.getElementById("maprConfig").value.replace("mapr_", ""); + let elementId = 'maprResultsTable' + term; + let html = ` +

    ${term}

    + + + + + + + + + + + +
    Study IDOrganismImage countTitleSample ImagesLink
    `; $('#studies').append(html); renderMapr(maprData, term); -} // ----- event handling -------- +} +// ----- event handling -------- -document.getElementById('maprConfig').onchange = function (event) { +document.getElementById('maprConfig').onchange = (event) => { document.getElementById('maprQuery').value = ''; - var value = event.target.value.replace('mapr_', ''); - var placeholder = "Type to filter values..."; - + let value = event.target.value.replace('mapr_', ''); + let placeholder = `Type to filter values...`; if (mapr_settings[value]) { - placeholder = "Type ".concat(mapr_settings[value]['default'][0], "..."); + placeholder = `Type ${mapr_settings[value]['default'][0]}...`; } - - document.getElementById('maprQuery').placeholder = placeholder; // Show all autocomplete options... - + document.getElementById('maprQuery').placeholder = placeholder; + // Show all autocomplete options... $("#maprQuery").focus(); render(); -}; // We want to show auto-complete options when user -// clicks on the field. - +} +// We want to show auto-complete options when user +// clicks on the field. function showAutocomplete(event) { - var configId = document.getElementById("maprConfig").value; - var autoCompleteValue = event.target.value; - + let configId = document.getElementById("maprConfig").value; + let autoCompleteValue = event.target.value; if (configId.indexOf('mapr_') != 0) { // If not MAPR search, show all auto-complete results autoCompleteValue = ''; } - $("#maprQuery").autocomplete("search", autoCompleteValue); } -document.getElementById('maprQuery').onfocus = function (event) { +document.getElementById('maprQuery').onfocus = (event) => { showAutocomplete(event); -}; - -document.getElementById('maprQuery').onclick = function (event) { +} +document.getElementById('maprQuery').onclick = (event) => { showAutocomplete(event); -}; // ------ AUTO-COMPLETE ------------------- +} +// ------ AUTO-COMPLETE ------------------- function showSpinner() { document.getElementById('spinner').style.visibility = 'visible'; } - function hideSpinner() { document.getElementById('spinner').style.visibility = 'hidden'; -} // timeout to avoid flash of spinner - - -var filterSpinnerTimout; - +} +// timeout to avoid flash of spinner +let filterSpinnerTimout; function showFilterSpinner(message) { - filterSpinnerTimout = setTimeout(function () { + filterSpinnerTimout = setTimeout(() => { document.getElementById('filterSpinnerMessage').innerHTML = message ? message : ''; document.getElementById('filterSpinner').style.display = 'block'; }, 500); } - function hideFilterSpinner() { clearTimeout(filterSpinnerTimout); document.getElementById('filterSpinnerMessage').innerHTML = ''; document.getElementById('filterSpinner').style.display = 'none'; } -$("#maprQuery").keyup(function (event) { - if (event.which == 13) { - $(event.target).autocomplete("close"); - filterAndRender(); // Add to browser history. Handled by onpopstate on browser Back - - var configId = document.getElementById("maprConfig").value; - window.history.pushState({}, "", "?query=".concat(configId, ":").concat(event.target.value)); - } -}).autocomplete({ - autoFocus: false, - delay: 1000, - source: function source(request, response) { - // if configId is not from mapr, we filter on mapValues... - var configId = document.getElementById("maprConfig").value; +$("#maprQuery") + .keyup(event => { + if (event.which == 13) { + $(event.target).autocomplete("close"); + filterAndRender(); + // Add to browser history. Handled by onpopstate on browser Back + let configId = document.getElementById("maprConfig").value; + window.history.pushState({}, "", `?query=${configId}:${event.target.value}`); + } + }) + .autocomplete({ + autoFocus: false, + delay: 1000, + source: function (request, response) { + + // if configId is not from mapr, we filter on mapValues... + let configId = document.getElementById("maprConfig").value; + if (configId.indexOf('mapr_') != 0) { + + let matches; + if (configId === 'Name') { + matches = model.getStudiesNames(request.term); + } else if (configId === 'Group') { + matches = model.getStudiesGroups(request.term); + } else { + matches = model.getKeyValueAutoComplete(configId, request.term); + } + response(matches); - if (configId.indexOf('mapr_') != 0) { - var matches; + // When not mapr, we filter while typing + filterAndRender(); + return; + } - if (configId === 'Name') { - matches = model.getStudiesNames(request.term); - } else if (configId === 'Group') { - matches = model.getStudiesGroups(request.term); - } else { - matches = model.getKeyValueAutoComplete(configId, request.term); + // Don't handle empty query for mapr + if (request.term.length == 0) { + return; } - response(matches); // When not mapr, we filter while typing + // Auto-complete to filter by mapr... + configId = configId.replace('mapr_', ''); + let case_sensitive = false; - filterAndRender(); - return; - } // Don't handle empty query for mapr - - - if (request.term.length == 0) { - return; - } // Auto-complete to filter by mapr... - - - configId = configId.replace('mapr_', ''); - var case_sensitive = false; - var requestData = { - case_sensitive: case_sensitive - }; - var url; - - if (request.term.length === 0) { - // Try to list all top-level values. - // This works for 'wild-card' configs where number of values is small e.g. Organism - // But will return empty list for e.g. Gene - url = "".concat(BASE_URL, "mapr/api/").concat(configId, "/"); - requestData.orphaned = true; - } else { - // Find auto-complete matches - url = "".concat(BASE_URL, "mapr/api/autocomplete/").concat(configId, "/"); - requestData.value = case_sensitive ? request.term : request.term.toLowerCase(); - requestData.query = true; // use a 'like' HQL query - } + let requestData = { + case_sensitive: case_sensitive, + } + let url; + if (request.term.length === 0) { + // Try to list all top-level values. + // This works for 'wild-card' configs where number of values is small e.g. Organism + // But will return empty list for e.g. Gene + url = `${BASE_URL}mapr/api/${configId}/`; + requestData.orphaned = true + } else { + // Find auto-complete matches + url = `${BASE_URL}mapr/api/autocomplete/${configId}/`; + requestData.value = case_sensitive ? request.term : request.term.toLowerCase(); + requestData.query = true; // use a 'like' HQL query + } - showSpinner(); - $.ajax({ - dataType: "json", - type: 'GET', - url: url, - data: requestData, - success: function success(data) { - hideSpinner(); - - if (request.term.length === 0) { - // Top-level terms in 'maps' - if (data.maps && data.maps.length > 0) { - var terms = data.maps.map(function (m) { - return m.id; - }); - terms.sort(); - response(terms); + showSpinner(); + $.ajax({ + dataType: "json", + type: 'GET', + url: url, + data: requestData, + success: function (data) { + hideSpinner(); + if (request.term.length === 0) { + // Top-level terms in 'maps' + if (data.maps && data.maps.length > 0) { + let terms = data.maps.map(m => m.id); + terms.sort(); + response(terms); + } } - } else if (data.length > 0) { - response($.map(data, function (item) { - return item; - })); - } else { - response([{ - label: 'No results found.', - value: -1 - }]); + else if (data.length > 0) { + response($.map(data, function (item) { + return item; + })); + } else { + response([{ label: 'No results found.', value: -1 }]); + } + }, + error: function (data) { + hideSpinner(); + // E.g. status 504 for timeout + response([{ label: 'Loading auto-complete terms failed. Server may be busy.', value: -1 }]); } - }, - error: function error(data) { - hideSpinner(); // E.g. status 504 for timeout - - response([{ - label: 'Loading auto-complete terms failed. Server may be busy.', - value: -1 - }]); + }); + }, + minLength: 0, + open: function () { }, + close: function () { + // $(this).val(''); + return false; + }, + focus: function (event, ui) { }, + select: function (event, ui) { + if (ui.item.value == -1) { + // Ignore 'No results found' + return false; } - }); - }, - minLength: 0, - open: function open() {}, - close: function close() { - // $(this).val(''); - return false; - }, - focus: function focus(event, ui) {}, - select: function select(event, ui) { - if (ui.item.value == -1) { - // Ignore 'No results found' + $(this).val(ui.item.value); + filterAndRender(); + // Add to browser history. Handled by onpopstate on browser Back + let configId = document.getElementById("maprConfig").value; + window.history.pushState({}, "", `?query=${configId}:${ui.item.value}`); + return false; } - - $(this).val(ui.item.value); - filterAndRender(); // Add to browser history. Handled by onpopstate on browser Back - - var configId = document.getElementById("maprConfig").value; - window.history.pushState({}, "", "?query=".concat(configId, ":").concat(ui.item.value)); - return false; + }).data("ui-autocomplete")._renderItem = function (ul, item) { + return $("
  • ") + .append("" + item.label + "") + .appendTo(ul); } -}).data("ui-autocomplete")._renderItem = function (ul, item) { - return $("
  • ").append("" + item.label + "").appendTo(ul); -}; // ------------ Render ------------------------- -function filterAndRender() { - var configId = document.getElementById("maprConfig").value; - var value = document.getElementById("maprQuery").value; +// ------------ Render ------------------------- +function filterAndRender() { + let configId = document.getElementById("maprConfig").value; + let value = document.getElementById("maprQuery").value; if (!value) { render(); return; } - if (configId.indexOf('mapr_') != 0) { // filter studies by Key-Value pairs - var filterFunc = function filterFunc(study) { - var toMatch = value.toLowerCase(); - + let filterFunc = study => { + let toMatch = value.toLowerCase(); if (configId === 'Name') { return study.Name.toLowerCase().indexOf(toMatch) > -1; } - if (configId === 'Group') { var group = study['omero:details'].group; return group.Name.toLowerCase().indexOf(toMatch) > -1; - } // Filter by Map-Annotation Key-Value - - - var show = false; - + } + // Filter by Map-Annotation Key-Value + let show = false; if (study.mapValues) { - study.mapValues.forEach(function (kv) { + study.mapValues.forEach(kv => { if (kv[0] === configId && kv[1].toLowerCase().indexOf(toMatch) > -1) { show = true; } }); } - return show; - }; - + } render(filterFunc); } else { filterStudiesByMapr(value); @@ -401,60 +378,69 @@ function filterAndRender() { } function renderMapr(maprData, term) { - maprData.sort(function (a, b) { + + maprData.sort((a, b) => { return a.Name > b.Name ? 1 : -1; }); - var elementId = 'maprResultsTable' + term; - var configId = document.getElementById("maprConfig").value; - var linkFunc = function linkFunc(studyData) { - var type = studyData['@type'].split('#')[1].toLowerCase(); - var maprKey = configId.replace('mapr_', ''); - return "/mapr/".concat(maprKey, "/?value=").concat(term, "&show=").concat(type, "-").concat(studyData['@id']); - }; + let elementId = 'maprResultsTable' + term; + + let configId = document.getElementById("maprConfig").value; + let linkFunc = (studyData) => { + let type = studyData['@type'].split('#')[1].toLowerCase(); + let maprKey = configId.replace('mapr_', ''); + return `/mapr/${maprKey}/?value=${term}&show=${type}-${studyData['@id']}`; + } + let elementSelector = `[data-id="${elementId}"]`; - var elementSelector = "[data-id=\"".concat(elementId, "\"]"); - maprData.forEach(function (s) { - return renderStudy(s, elementSelector, linkFunc, maprHtml); - }); // load images for each study... + maprData.forEach(s => renderStudy(s, elementSelector, linkFunc, maprHtml)); - $("[data-id=\"".concat(elementId, "\"] tr")).each(function () { + // load images for each study... + $(`[data-id="${elementId}"] tr`).each(function () { // load children in MAPR jsTree query to get images - var element = this; - var studyId = element.id; - var objId = studyId.split("-")[1]; - var objType = studyId.split("-")[0]; + let element = this; + let studyId = element.id; + let objId = studyId.split("-")[1]; + let objType = studyId.split("-")[0]; if (!objId || !objType) return; - var childType = objType === "project" ? "datasets" : "plates"; - var configId = document.getElementById("maprConfig").value.replace('mapr_', ''); - var maprValue = term; // We want to link to the dataset or plate... - - var imgContainer; - var url = "".concat(BASE_URL, "mapr/api/").concat(configId, "/").concat(childType, "/?value=").concat(maprValue, "&id=").concat(objId); - fetch(url).then(function (response) { - return response.json(); - }).then(function (data) { - var firstChild = data[childType][0]; - imgContainer = "".concat(firstChild.extra.node, "-").concat(firstChild.id); - var imagesUrl = "".concat(BASE_URL, "mapr/api/").concat(configId, "/images/?value=").concat(maprValue, "&id=").concat(firstChild.id, "&node=").concat(firstChild.extra.node); - return fetch(imagesUrl); - }).then(function (response) { - return response.json(); - }).then(function (data) { - var html = data.images.slice(0, 3).map(function (i) { - return "\n \n
    \n \n \n
    \n
    "); - }).join(""); - var linkHtml = "\n more...\n "); // Find the container and add placeholder images html - - $("#" + element.id + " .exampleImages").html(html); - $("#" + element.id + " .exampleImagesLink").append(linkHtml); // Update the src to load the thumbnails. Timeout to let placeholder render while we wait for thumbs - - setTimeout(function () { - $('img', "#" + element.id).each(function (index, img) { - img.src = img.dataset.src; - }); - }, 0); - }); + let childType = objType === "project" ? "datasets" : "plates"; + let configId = document.getElementById("maprConfig").value.replace('mapr_', ''); + let maprValue = term; + // We want to link to the dataset or plate... + let imgContainer; + let url = `${BASE_URL}mapr/api/${configId}/${childType}/?value=${maprValue}&id=${objId}`; + fetch(url) + .then(response => response.json()) + .then(data => { + let firstChild = data[childType][0]; + imgContainer = `${firstChild.extra.node}-${firstChild.id}`; + let imagesUrl = `${BASE_URL}mapr/api/${configId}/images/?value=${maprValue}&id=${firstChild.id}&node=${firstChild.extra.node}`; + return fetch(imagesUrl); + }) + .then(response => response.json()) + .then(data => { + let html = data.images.slice(0, 3).map(i => ` + +
    + + +
    +
    `).join(""); + let linkHtml = ` + more... + ` + // Find the container and add placeholder images html + $("#" + element.id + " .exampleImages").html(html); + $("#" + element.id + " .exampleImagesLink").append(linkHtml); + // Update the src to load the thumbnails. Timeout to let placeholder render while we wait for thumbs + setTimeout(() => { + $('img', "#" + element.id).each((index, img) => { + img.src = img.dataset.src; + }); + }, 0); + }); }); } @@ -467,132 +453,115 @@ function render(filterFunc) { return; } - var studiesToRender = model.studies; - + let studiesToRender = model.studies; if (filterFunc) { studiesToRender = model.studies.filter(filterFunc); } - var filterMessage = ""; - + let filterMessage = ""; if (studiesToRender.length === 0) { filterMessage = noStudiesMessage(); } else if (studiesToRender.length < model.studies.length) { - var configId = document.getElementById("maprConfig").value.replace('mapr_', ''); - configId = mapr_settings && mapr_settings[configId] || configId; - var maprValue = document.getElementById('maprQuery').value; - filterMessage = "

    \n Found ".concat(studiesToRender.length, " studies with\n ").concat(configId, ": ").concat(maprValue, "

    "); + let configId = document.getElementById("maprConfig").value.replace('mapr_', ''); + configId = (mapr_settings && mapr_settings[configId]) || configId; + let maprValue = document.getElementById('maprQuery').value; + filterMessage = `

    + Found ${studiesToRender.length} studies with + ${configId}: ${maprValue}

    `; } + document.getElementById('filterCount').innerHTML = filterMessage; - document.getElementById('filterCount').innerHTML = filterMessage; // By default, we link to the study itself in IDR... + // By default, we link to the study itself in IDR... + let linkFunc = (studyData) => { + let type = studyData['@type'].split('#')[1].toLowerCase(); + return `${BASE_URL}webclient/?show=${type}-${studyData['@id']}`; + } + let htmlFunc = studyHtml; - var linkFunc = function linkFunc(studyData) { - var type = studyData['@type'].split('#')[1].toLowerCase(); - return "".concat(BASE_URL, "webclient/?show=").concat(type, "-").concat(studyData['@id']); - }; + studiesToRender.forEach(s => renderStudy(s, '#studies', linkFunc, htmlFunc)); - var htmlFunc = studyHtml; - studiesToRender.forEach(function (s) { - return renderStudy(s, '#studies', linkFunc, htmlFunc); - }); - loadStudyThumbnails(); -} // When no studies match the filter, show message/link. + // loadStudyThumbnails(); + model.loadStudiesThumbnails(); +} +// When no studies match the filter, show message/link. function noStudiesMessage() { - var filterMessage = "No matching studies."; - + let filterMessage = "No matching studies."; if (SUPER_CATEGORY) { - var currLabel = SUPER_CATEGORY.label; - var configId = document.getElementById("maprConfig").value; - var maprQuery = document.getElementById("maprQuery").value; - var others = []; - - for (var cat in SUPER_CATEGORIES) { + let currLabel = SUPER_CATEGORY.label; + let configId = document.getElementById("maprConfig").value; + let maprQuery = document.getElementById("maprQuery").value; + let others = []; + for (let cat in SUPER_CATEGORIES) { if (SUPER_CATEGORIES[cat].label !== currLabel) { - others.push("").concat(SUPER_CATEGORIES[cat].label, "")); + others.push(`${SUPER_CATEGORIES[cat].label}`); } } - if (others.length > 0) { filterMessage += " Try " + others.join(" or "); } } - return filterMessage; } -function renderStudy(studyData, elementSelector, linkFunc, htmlFunc) { - // Add Project or Screen to the page - var title; - - for (var i = 0; i < TITLE_KEYS.length; i++) { - title = model.getStudyValue(studyData, TITLE_KEYS[i]); - - if (title) { - break; - } - } - if (!title) { - title = studyData.Name; - } +function renderStudy(studyData, elementSelector, linkFunc, htmlFunc) { - var type = studyData['@type'].split('#')[1].toLowerCase(); - var studyLink = linkFunc(studyData); // save for later + // Add Project or Screen to the page + var title = model.getStudyTitle(studyData); + let type = studyData['@type'].split('#')[1].toLowerCase(); + let studyLink = linkFunc(studyData); + // save for later studyData.title = title; - var desc = studyData.Description; - var studyDesc; - - if (desc) { - // If description contains title, use the text that follows - if (title.length > 0 && desc.indexOf(title) > -1) { - desc = desc.split(title)[1]; - } // Remove blank lines (and first 'Experiment Description' line) - - studyDesc = desc.split('\n').filter(function (l) { - return l.length > 0; - }).filter(function (l) { - return l !== 'Experiment Description' && l !== 'Screen Description'; - }).join('\n'); + var studyDesc = model.getStudyDescription(studyData, title); - if (studyDesc.indexOf('Version History') > 1) { - studyDesc = studyDesc.split('Version History')[0]; - } - } + let shortName = getStudyShortName(studyData); + let authors = model.getStudyValue(studyData, "Publication Authors") || ""; - var shortName = getStudyShortName(studyData); - var authors = model.getStudyValue(studyData, "Publication Authors") || ""; - var div = htmlFunc({ - studyLink: studyLink, - studyDesc: studyDesc, - shortName: shortName, - title: title, - authors: authors, - BASE_URL: BASE_URL, - type: type - }, studyData); + let div = htmlFunc({ studyLink, studyDesc, shortName, title, authors, BASE_URL, type }, studyData); document.querySelector(elementSelector).appendChild(div); -} // --------- Render utils ----------- +} +// --------- Render utils ----------- function studyHtml(props, studyData) { - var pubmed = model.getStudyValue(studyData, 'PubMed ID'); + let pubmed = model.getStudyValue(studyData, 'PubMed ID'); if (pubmed) { pubmed = pubmed.split(" ")[1]; } - - var author = props.authors.split(',')[0] || ''; - + let author = props.authors.split(',')[0] || ''; if (author) { - author = "".concat(author, " et al."); + author = `${author} et al.`; author = author.length > 23 ? author.slice(0, 20) + '...' : author; } - - var html = "\n
    \n ".concat(props.shortName, "\n ").concat(pubmed ? " ").concat(author, "") : author, "\n
    \n
    \n \n
    \n
    \n

    \n ").concat(props.title, "\n

    \n
    \n
    \n ").concat(props.authors, "\n
    \n
    \n
    \n \n \n \n
    \n "); + let html = ` +
    + ${props.shortName} + ${pubmed ? ` ${author}` : author} +
    +
    + +
    +
    +

    + ${props.title} +

    +
    +
    + ${props.authors} +
    +
    +
    + + + +
    + ` var div = document.createElement("div"); div.innerHTML = html; div.id = props.type + '-' + studyData['@id']; @@ -603,7 +572,18 @@ function studyHtml(props, studyData) { } function maprHtml(props, studyData) { - var html = " \n \n \n ").concat(props.shortName, "\n \n \n ").concat(model.getStudyValue(studyData, 'Organism'), "\n ").concat(studyData.imageCount, "\n ").concat(props.title.slice(0, 40)).concat(props.title.length > 40 ? '...' : '', "\n loading...\n \n "); + let html = ` + + + ${props.shortName} + + + ${model.getStudyValue(studyData, 'Organism')} + ${studyData.imageCount} + ${props.title.slice(0, 40)}${props.title.length > 40 ? '...' : ''} + loading... + + ` var tr = document.createElement("tr"); tr.innerHTML = html; tr.id = props.type + '-' + studyData['@id']; @@ -612,86 +592,80 @@ function maprHtml(props, studyData) { return tr; } -function loadStudyThumbnails() { - var ids = []; // Collect study IDs 'project-1', 'screen-2' etc - - $('div.study').each(function () { - var obj_id = $(this).attr('data-obj_id'); - var obj_type = $(this).attr('data-obj_type'); - - if (obj_id && obj_type) { - ids.push(obj_type + '-' + obj_id); - } - }); // Load images - - model.loadStudiesThumbnails(ids, function (data) { - // data is e.g. { project-1: {thumbnail: base64data, image: {id:1}} } - for (var id in data) { - if (!data[id]) continue; // may be null - - var obj_type = id.split('-')[0]; - var obj_id = id.split('-')[1]; - var elements = document.querySelectorAll("div[data-obj_type=\"".concat(obj_type, "\"][data-obj_id=\"").concat(obj_id, "\"]")); - - for (var e = 0; e < elements.length; e++) { - // Find all studies matching the study ID and set src on image - var element = elements[e]; - var studyImage = element.querySelector('.studyImage'); - - if (data[id].thumbnail) { - studyImage.style.backgroundImage = "url(".concat(data[id].thumbnail, ")"); - } // viewer link - - if (data[id].image && data[id].image.id) { - var iid = data[id].image.id; - var link = "".concat(BASE_URL, "webclient/img_detail/").concat(iid, "/"); - element.querySelector('a.viewerLink').href = link; - } +function renderThumbnails(data) { + + // data is e.g. { project-1: {thumbnail: base64data, image: {id:1}} } + for (let id in data) { + if (!data[id]) continue; // may be null + let obj_type = id.split('-')[0]; + let obj_id = id.split('-')[1]; + let elements = document.querySelectorAll(`div[data-obj_type="${obj_type}"][data-obj_id="${obj_id}"]`); + for (let e = 0; e < elements.length; e++) { + // Find all studies matching the study ID and set src on image + let element = elements[e]; + let studyImage = element.querySelector('.studyImage'); + if (data[id].thumbnail) { + studyImage.style.backgroundImage = `url(${data[id].thumbnail})`; + } + // viewer link + if (data[id].image && data[id].image.id) { + let iid = data[id].image.id; + let link = `${BASE_URL}webclient/img_detail/${iid}/`; + element.querySelector('a.viewerLink').href = link; } } - }); -} // ----------- Load / Filter Studies -------------------- -// Do the loading and render() when done... + } +} +// ----------- Load / Filter Studies -------------------- + +async function init() { + // Do the loading and render() when done... + await model.loadStudies(); -model.loadStudies(function () { // Immediately filter by Super category if (SUPER_CATEGORY && SUPER_CATEGORY.query) { model.studies = model.filterStudiesByMapQuery(SUPER_CATEGORY.query); } + // load thumbs for all studies, even if not needed + // URL will be same each time (before filtering) so response will be cached + model.loadStudiesThumbnails(); + filterAndRender(); -}); // Handle browser Back and Forwards - redo filtering +} + +init(); -window.onpopstate = function (event) { +// Handle browser Back and Forwards - redo filtering +window.onpopstate = (event) => { populateInputsFromSearch(); filterAndRender(); -}; // Load MAPR config +} -fetch(BASE_URL + 'mapr/api/config/').then(function (response) { - return response.json(); -}).then(function (data) { - mapr_settings = data; - var options = FILTER_MAPR_KEYS.map(function (key) { - var config = mapr_settings[key]; +// Load MAPR config +fetch(BASE_URL + 'mapr/api/config/') + .then(response => response.json()) + .then(data => { + mapr_settings = data; - if (config) { - return ""); - } else { - return ""; + let options = FILTER_MAPR_KEYS.map(key => { + let config = mapr_settings[key]; + if (config) { + return ``; + } else { + return ""; + } + }); + if (options.length > 0) { + document.getElementById('maprKeys').innerHTML = options.join("\n"); + // Show the and the whole form + document.getElementById('maprKeys').style.display = 'block'; + document.getElementById('search-form').style.display = 'block'; } + populateInputsFromSearch(); + }).catch(function (err) { + console.log("mapr not installed (config not available)"); }); - - if (options.length > 0) { - document.getElementById('maprKeys').innerHTML = options.join("\n"); // Show the and the whole form - - document.getElementById('maprKeys').style.display = 'block'; - document.getElementById('search-form').style.display = 'block'; - } - - populateInputsFromSearch(); -})["catch"](function (err) { - console.log("mapr not installed (config not available)"); -}); \ No newline at end of file diff --git a/idr_gallery/static/idr_gallery/studies.css b/idr_gallery/static/idr_gallery/studies.css index d6232283..1a038989 100644 --- a/idr_gallery/static/idr_gallery/studies.css +++ b/idr_gallery/static/idr_gallery/studies.css @@ -149,34 +149,3 @@ maprText { position: relative; border-radius: 10px; } - -/* Always show scrollbars https://gist.github.com/IceCreamYou/cd517596e5847a88e2bb0a091da43fb4 */ -::-webkit-scrollbar-track:vertical { - border-left: 1px solid #E7E7E7; - box-shadow: 1px 0 1px 0 #F6F6F6 inset, -1px 0 1px 0 #F6F6F6 inset; -} - -::-webkit-scrollbar-track:horizontal { - border-top: 1px solid #E7E7E7; - box-shadow: 0 1px 1px 0 #F6F6F6 inset, 0 -1px 1px 0 #F6F6F6 inset; -} - -::-webkit-scrollbar { - -webkit-appearance: none; - background-color: #FAFAFA; - width: 16px; -} - -::-webkit-scrollbar-thumb { - background-clip: padding-box; - background-color: #C1C1C1; - border-color: transparent; - border-radius: 9px 8px 8px 9px; - border-style: solid; - border-width: 3px 3px 3px 4px; /* Workaround because margins aren't supported */ - box-shadow: 0 0 1px rgba(255, 255, 255, 0.5); -} - -::-webkit-scrollbar-thumb:hover { - background-color: rgba(0, 0, 0, 0.5); -} diff --git a/idr_gallery/static/idr_gallery/tabs.js b/idr_gallery/static/idr_gallery/tabs.js new file mode 100644 index 00000000..6c3eb603 --- /dev/null +++ b/idr_gallery/static/idr_gallery/tabs.js @@ -0,0 +1,62 @@ +// Load tabs JSON from config.json file at INDEX_JSON_URL and use to create tabs html +// Then init the tabs with Foundation +$(function () { + if (INDEX_JSON_URL) { + $.getJSON(INDEX_JSON_URL, function (data) { + if (!data.tabs) return; + + let headersHtml = data.tabs.map((tab, index) => { + return `
  • + + ${tab.title} +
  • ` + }).join("\n"); + + function videosHtml(videos) { + if (!videos) return ""; + return videos.map(video => { + return ` +
    +
    + + +
    +
    +

    ${video.title}

    + ${video.text} +
    +
    + ` + }).join("\n"); + } + + let contentHtml = data.tabs.map((tab, index) => { + return `
    + ${tab.text} + ${videosHtml(tab.videos)} +
    ` + }).join("\n"); + + let html = ` +
    + ${contentHtml} +
    ` + + $("#tabs").html(html); + new Foundation.Tabs($('#tabs .tabs')); + }); + } + + // handle expand/collapse of videos >> + $("#tabs").on("click", ".expandVideo", function () { + let $row = $(this).closest(".row"); + // toggle from 2 columns to 1 + $('.columns', $row).toggleClass("small-12 medium-12 large-12") + .toggleClass("small-6 medium-6 large-6"); + }); +}); diff --git a/idr_gallery/templates/idr_gallery/categories/background_carousel.html b/idr_gallery/templates/idr_gallery/categories/background_carousel.html new file mode 100644 index 00000000..689653aa --- /dev/null +++ b/idr_gallery/templates/idr_gallery/categories/background_carousel.html @@ -0,0 +1,166 @@ + + +{% for img in idr_images %} +
    +{% endfor %} + +
    + {% for img in idr_images %} +
    + + + + + +
    + {% endfor %} +
    + + diff --git a/idr_gallery/templates/idr_gallery/categories/base.html b/idr_gallery/templates/idr_gallery/categories/base.html index f4f68088..89e83f7b 100644 --- a/idr_gallery/templates/idr_gallery/categories/base.html +++ b/idr_gallery/templates/idr_gallery/categories/base.html @@ -105,7 +105,7 @@
    -

    © 2016-2020 University of Dundee & Open Microscopy Environment. Creative Commons Attribution 4.0 International License.

    +

    © 2016-2022 University of Dundee & Open Microscopy Environment. Creative Commons Attribution 4.0 International License.

    OMERO is distributed under the terms of the GNU GPL. For more information, visit openmicroscopy.org


    diff --git a/idr_gallery/templates/idr_gallery/categories/index.html b/idr_gallery/templates/idr_gallery/categories/index.html index 089eaf5d..63833771 100644 --- a/idr_gallery/templates/idr_gallery/categories/index.html +++ b/idr_gallery/templates/idr_gallery/categories/index.html @@ -1,82 +1,464 @@ - {% extends "idr_gallery/categories/base.html" %} {% block content %} -
    + -
    - {% if gallery_heading %}

    {{ gallery_heading }}

    {% endif %} + + + + -

    - {% if not super_category %} - {{ subheading_html|safe }} - {% endif %} -

    -
    + + + +
    + + {% include 'idr_gallery/categories/background_carousel.html' %} +
    +
    +
    + IDR logo + IDR logo
    -
    - - + +

    + The Image Data Resource (IDR) is a public repository of image datasets from published scientific studies, + where the community can submit, search and access high-quality bio-image data. +

    +
    + {% for c, data in super_categories.items %} + {{ data.label }} + {% endfor %}
    +
    - {% if not super_category %} -
    -
     
    - {% for category, data in super_categories.items %} - -
    - -
    - {{ data.label }} -
    -
    -
    - {% endfor %} -
    -
    - {% endif %} +
    +
    + +
    +
    + + +
    +
    +
    -
    +
    +
    +
    +
    + Studies +
    +
    + Images +
    +
    + TB +
    +
    +
    +
    +
    +
    +
    + + + + + +
    +
    + + +
    +
    +
    -
    - Loading Studies... +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + + + + + +
    +
    - +
    + + + + + {% endblock %} diff --git a/idr_gallery/templates/idr_gallery/categories/search.html b/idr_gallery/templates/idr_gallery/categories/search.html index 04587896..b1ec099f 100644 --- a/idr_gallery/templates/idr_gallery/categories/search.html +++ b/idr_gallery/templates/idr_gallery/categories/search.html @@ -57,8 +57,15 @@

    Search by:

    @@ -97,18 +104,19 @@

    Search by:

    - + + {% endblock %} diff --git a/idr_gallery/views.py b/idr_gallery/views.py index 59d92eaa..9ca33002 100644 --- a/idr_gallery/views.py +++ b/idr_gallery/views.py @@ -20,6 +20,126 @@ logger = logging.getLogger(__name__) MAX_LIMIT = max(1, API_MAX_LIMIT) +IDR_IMAGES = [ + { + "src": "https://idr.openmicroscopy.org/webclient/render_image/13965767/294/0/", + "title": "idr0124 Esteban: Heart morphogenesis", + "image_id": 13965767, + "thumbnail": "data:image/jpeg;base64,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" + }, + { + "light": True, + "src": "https://idr.openmicroscopy.org/webgateway/render_image_region/13461816/0/0/?tile=3,2,4,1024,512", + "title": "idr0096 Tratwal: Marrowquant", + "image_id": 13461816, + }, + { + "src": "https://idr.openmicroscopy.org/webgateway/render_image_region/9846152/45/0/?tile=4,0,0,1024,512", + "title": "idr0048 Abdeladim: Chroms", + "image_id": 9846152, + }, + { + "light": True, + "src": "https://idr.openmicroscopy.org/webgateway/render_image_region/13383922/0/0/?region=412,1224,1536,1024", + "title": "idr0043 Uhlen: Human Protein Atlas", + "image_id": 13383922, + }, + { + "src": "https://idr.openmicroscopy.org/webclient/render_image/4990991/0/0/?c=1|144:2250$0000FF,-2|972:2528$FFFFFF,3|126:3456$FF0000,4|131:2749$00FF00", + "title": "idr0050 Springer: Cyto-skeletal systems", + "image_id": 4990991, + }, + { + "src": "https://idr.openmicroscopy.org/webgateway/render_image/9846154/268/0/?region=1024,0,2048,1024&c=1|4283:12901$FF00FF,2|1278:8356$FFFF00", + "title": "idr0085 Walsh: MF-HREM", + "image_id": 9846154, + }, + { + "src": "https://idr.openmicroscopy.org/webclient/render_image/9753804/0/0/", + "title": "idr0056 Stojic: Long noncoding RNA", + "image_id": 9753804, + }, + { + "src": "https://idr.openmicroscopy.org/webgateway/render_image/9836841/0/0/", + "title": "idr0077 Valuchova: Flower lightsheet", + "image_id": 9836841, + }, +] + +CELL_IMAGES = [ + { + "src": "https://idr.openmicroscopy.org/webgateway/render_image/13417268/34/0/?c=1|5000:13880$FF0000,2|10353:50528$00FF00,3|14416:36737$0000FF", + "title": "idr0107 Morgan: HEI10", + "image_id": 13417268, + }, + { + "src": "https://idr.openmicroscopy.org/webgateway/render_image/12570400/0/0/?c=1|91:1391$fire.lut", + "title": "idr0093 Mueller: Genome-wide siRNA screen", + "image_id": 12570400, + }, + { + "src": "https://idr.openmicroscopy.org/webclient/render_image/4991918/0/0/?c=1|28:178$00FF00,3|22:110$FF0000", + "title": "idr0050 Springer: Cyto-skeletal systems", + "image_id": 4991918, + }, + { + "light": True, + "src": "https://idr.openmicroscopy.org/webgateway/render_image/9846137/92/0/?c=1|85:153$hilo.lut&m=c", + "title": "idr0086 Miron: Chromatin micrographs", + "image_id": 9846137, + }, + { + "src": "https://idr.openmicroscopy.org/webgateway/render_image/3005394/0/0/", + "title": "idr0028 Pascual-Vargas: Rho GTPases", + "image_id": 3005394, + }, + { + "src": "https://idr.openmicroscopy.org/webclient/render_image/9753804/0/0/", + "title": "idr0056 Stojic: Long noncoding RNA", + "image_id": 9753804, + }, + { + "src": "https://idr.openmicroscopy.org/webclient/render_image/3231645/0/0/?c=1|464:8509$FF0000,2|518:21105$00FF00,3|519:19845$0000FF", + "title": "idr0033 Rohban: Cell painting", + "image_id": 3231645, + }, +] + +TISSUE_IMAGES = [ + { + "light": True, + "src": "https://idr.openmicroscopy.org/webgateway/render_image_region/13461816/0/0/?tile=3,2,4,1024,512", + "title": "idr0096 Tratwal: Marrowquant", + "image_id": 13461816, + }, + { + "src": "https://idr.openmicroscopy.org/webgateway/render_image_region/8343616/0/0/?region=2048,6072,2024,1024&c=1|0:105$red_hot.lut&m=c", + "title": "idr0066 Voigt: Meso SPIM", + "image_id": 8343616, + }, + { + "src": "https://idr.openmicroscopy.org/webgateway/render_image_region/9846152/45/0/?tile=4,0,0,1024,512", + "title": "idr0048 Abdeladim: Chroms", + "image_id": 9846152, + }, + { + "light": True, + "src": "https://idr.openmicroscopy.org/webgateway/render_image_region/13383922/0/0/?region=412,1224,1536,1024", + "title": "idr0043 Uhlen: Human Protein Atlas", + "image_id": 13383922, + }, + { + "src": "https://idr.openmicroscopy.org/webgateway/render_image/9846154/268/0/?region=1024,0,2048,1024&c=1|4283:12901$FF00FF,2|1278:8356$FFFF00", + "title": "idr0085 Walsh: MF-HREM", + "image_id": 9846154, + }, + { + "src": "https://idr.openmicroscopy.org/webgateway/render_image/9836841/0/0/", + "title": "idr0077 Valuchova: Flower lightsheet", + "image_id": 9836841, + } +] +# //background-position-y: 15%; @login_required() @render_response() @@ -31,20 +151,22 @@ def index(request, super_category=None, conn=None, **kwargs): category_queries = settings.CATEGORY_QUERIES if len(category_queries) > 0: - context = {'template': "idr_gallery/categories/index.html"} + context = {'template': "webgallery/categories/index.html"} context['favicon'] = settings.FAVICON context['gallery_title'] = settings.GALLERY_TITLE context['gallery_heading'] = settings.GALLERY_HEADING context['top_right_links'] = settings.TOP_RIGHT_LINKS context['top_left_logo'] = settings.TOP_LEFT_LOGO + context['INDEX_JSON_URL'] = settings.INDEX_JSON_URL + context['IDR_STUDIES_URL'] = settings.IDR_STUDIES_URL try: - href = context['top_left_logo'].get('href', 'idr_gallery_index') + href = context['top_left_logo'].get('href', 'webgallery_index') context['top_left_logo']['href'] = reverse(href) except NoReverseMatch: pass context['subheading_html'] = settings.SUBHEADING_HTML context['footer_html'] = settings.FOOTER_HTML - context['filter_keys'] = json.dumps(settings.FILTER_KEYS) + context['filter_keys'] = settings.FILTER_KEYS context['TITLE_KEYS'] = json.dumps(settings.TITLE_KEYS) context['STUDY_SHORT_NAME'] = json.dumps(settings.STUDY_SHORT_NAME) context['filter_mapr_keys'] = json.dumps( @@ -52,6 +174,7 @@ def index(request, super_category=None, conn=None, **kwargs): context['super_categories'] = settings.SUPER_CATEGORIES category = settings.SUPER_CATEGORIES.get(super_category) if category is not None: + category['id'] = super_category label = category.get('label', context['gallery_heading']) title = category.get('title', label) context['gallery_heading'] = title @@ -61,7 +184,15 @@ def index(request, super_category=None, conn=None, **kwargs): if settings.BASE_URL is not None: base_url = settings.BASE_URL context['base_url'] = base_url + context['gallery_index'] = reverse('webgallery_index') + if settings.GALLERY_INDEX is not None: + context['gallery_index'] = settings.GALLERY_INDEX context['category_queries'] = json.dumps(category_queries) + context["idr_images"] = IDR_IMAGES + if super_category == "cell": + context["idr_images"] = CELL_IMAGES + elif super_category == "tissue": + context["idr_images"] = TISSUE_IMAGES return context my_groups = list(conn.getGroupsMemberOf()) @@ -96,7 +227,7 @@ def index(request, super_category=None, conn=None, **kwargs): 'image': len(images) > 0 and images[0] or None}) # This is used by @render_response - context = {'template': "idr_gallery/index.html"} + context = {'template': "webgallery/index.html"} context['groups'] = groups return context @@ -221,7 +352,7 @@ def show_group(request, group_id, conn=None, **kwargs): count_images, param_all, conn.SERVICE_OPTS) ddata['imageCount'] = image_count[0][0].val datasets.append(ddata) - context = {'template': "idr_gallery/show_group.html"} + context = {'template': "webgallery/show_group.html"} context['group'] = group context['group_owners'] = group_owners context['group_members'] = group_members @@ -258,7 +389,7 @@ def show_project(request, project_id, conn=None, **kwargs): "description": ds.getDescription(), "images": images}) - context = {'template': "idr_gallery/show_project.html"} + context = {'template': "webgallery/show_project.html"} context['project'] = project context['datasets'] = datasets @@ -277,7 +408,7 @@ def show_dataset(request, dataset_id, conn=None, **kwargs): if dataset is None: raise Http404 - context = {'template': "idr_gallery/show_dataset.html"} + context = {'template': "webgallery/show_dataset.html"} context['dataset'] = dataset return context @@ -300,7 +431,7 @@ def show_image(request, image_id, conn=None, **kwargs): if isinstance(ann, omero.gateway.TagAnnotationWrapper): tags.append(ann) - context = {'template': "idr_gallery/show_image.html"} + context = {'template': "webgallery/show_image.html"} context['image'] = image context['tags'] = tags @@ -310,20 +441,20 @@ def show_image(request, image_id, conn=None, **kwargs): @render_response() def search(request, super_category=None, conn=None, **kwargs): - context = {'template': "idr_gallery/categories/search.html"} + context = {'template': "webgallery/categories/search.html"} context['favicon'] = settings.FAVICON context['gallery_title'] = settings.GALLERY_TITLE context['gallery_heading'] = settings.GALLERY_HEADING context['top_right_links'] = settings.TOP_RIGHT_LINKS context['top_left_logo'] = settings.TOP_LEFT_LOGO try: - href = context['top_left_logo'].get('href', 'idr_gallery_index') + href = context['top_left_logo'].get('href', 'webgallery_index') context['top_left_logo']['href'] = reverse(href) except NoReverseMatch: pass context['subheading_html'] = settings.SUBHEADING_HTML context['footer_html'] = settings.FOOTER_HTML - context['filter_keys'] = json.dumps(settings.FILTER_KEYS) + context['filter_keys'] = settings.FILTER_KEYS context['super_categories'] = settings.SUPER_CATEGORIES context['SUPER_CATEGORIES'] = json.dumps(settings.SUPER_CATEGORIES) context['TITLE_KEYS'] = json.dumps(settings.TITLE_KEYS) @@ -332,6 +463,7 @@ def search(request, super_category=None, conn=None, **kwargs): settings.FILTER_MAPR_KEYS) category = settings.SUPER_CATEGORIES.get(super_category) if category is not None: + category['id'] = super_category label = category.get('label', context['gallery_heading']) title = category.get('title', label) context['gallery_heading'] = title @@ -341,11 +473,14 @@ def search(request, super_category=None, conn=None, **kwargs): if settings.BASE_URL is not None: base_url = settings.BASE_URL context['base_url'] = base_url + context['gallery_index'] = reverse('webgallery_index') + if settings.GALLERY_INDEX is not None: + context['gallery_index'] = settings.GALLERY_INDEX context['category_queries'] = json.dumps(settings.CATEGORY_QUERIES) return context -def _get_study_images(conn, obj_type, obj_id, limit=1, offset=0): +def _get_study_images(conn, obj_type, obj_id, limit=1, offset=0, tag_text=None): query_service = conn.getQueryService() params = omero.sys.ParametersI() @@ -353,6 +488,10 @@ def _get_study_images(conn, obj_type, obj_id, limit=1, offset=0): params.theFilter = omero.sys.Filter() params.theFilter.limit = wrap(limit) params.theFilter.offset = wrap(offset) + and_text_value = "" + if tag_text is not None: + params.addString("tag_text", tag_text) + and_text_value = " and annotation.textValue = :tag_text" if obj_type == "project": query = "select i from Image as i"\ @@ -360,7 +499,9 @@ def _get_study_images(conn, obj_type, obj_id, limit=1, offset=0): " join dl.parent as dataset"\ " left outer join dataset.projectLinks"\ " as pl join pl.parent as project"\ - " where project.id = :id" + " left outer join i.annotationLinks as al"\ + " join al.child as annotation"\ + " where project.id = :id%s" % and_text_value elif obj_type == "screen": query = ("select i from Image as i" @@ -369,8 +510,10 @@ def _get_study_images(conn, obj_type, obj_id, limit=1, offset=0): " join well.plate as pt" " left outer join pt.screenLinks as sl" " join sl.parent as screen" - " where screen.id = :id" - " order by well.column, well.row") + " left outer join i.annotationLinks as al"\ + " join al.child as annotation"\ + " where screen.id = :id%s" + " order by well.column, well.row" % and_text_value) objs = query_service.findAllByQuery(query, params, conn.SERVICE_OPTS) @@ -413,7 +556,10 @@ def api_thumbnails(request, conn=None, **kwargs): image_ids = {} for obj_type, ids in zip(['project', 'screen'], [project_ids, screen_ids]): for obj_id in ids: - images = _get_study_images(conn, obj_type, obj_id) + images = _get_study_images(conn, obj_type, obj_id, tag_text="Study Example Image") + if len(images) == 0: + # None found with Tag - just load untagged image + images = _get_study_images(conn, obj_type, obj_id) if len(images) > 0: image_ids[images[0].id.val] = "%s-%s" % (obj_type, obj_id)