import { abortAble } from 'lineupengine'; import { ERenderMode, ICellRenderer, ICellRendererFactory, IDataRow, IGroupCellRenderer, IOrderedGroup, IRenderContext, ISummaryRenderer, renderMissingDOM, } from 'lineupjs'; import { SMILESColumn } from './SMILESColumn'; const template = '
'; function getImageURL(structure: string, substructure: string | null = null, align: string | null = null): string { return `/api/rdkit/?structure=${encodeURIComponent(structure)}${substructure ? `&substructure=${encodeURIComponent(substructure)}` : ''}${ align ? `&align=${encodeURIComponent(align)}` : '' }`; } async function fetchImage({ url, data, method }: { url: string; data?: any; method?: string }): Promise { const response = await fetch(url, { headers: { 'Content-Type': 'application/json', }, // mode: '*cors', // no-cors, *cors, same-origin method, redirect: 'follow', ...(data ? { body: JSON.stringify(data), } : {}), }); if (!response.ok) { throw Error((await response.json().catch(() => null))?.message || response.statusText); } return response.text(); } async function getReducedImages(structures: string[]): Promise { // maximum common substructure if (structures.length > 2) { return fetchImage({ url: '/api/rdkit/mcs/', data: structures, method: 'POST' }); } // similarity if (structures.length === 2) { const reference = structures[0]!; const probe = structures.length > 1 ? structures[1]! : structures[0]!; return fetchImage({ url: `/api/rdkit/similarity/?structure=${encodeURIComponent(probe)}&reference=${encodeURIComponent(reference)}`, method: 'GET' }); } // single = first structure return fetchImage({ url: `/api/rdkit/?structure=${encodeURIComponent(structures[0]!)}`, method: 'GET' }); } function svgToImageSrc(svg: string): string { return `data:image/svg+xml;base64,${btoa(svg)}`; } function svgToCSSBackground(svg: string): string { return `url('${svgToImageSrc(svg)}')`; } export class SMILESRenderer implements ICellRendererFactory { readonly title: string = 'SMILES'; canRender(col: SMILESColumn, mode: ERenderMode): boolean { return col instanceof SMILESColumn && (mode === ERenderMode.CELL || mode === ERenderMode.GROUP); } create(col: SMILESColumn): ICellRenderer { return { template, update: (n: HTMLLinkElement, d: IDataRow) => { if (!renderMissingDOM(n, col, d)) { if (d.v.images?.[0]) { n.style.backgroundImage = svgToCSSBackground(d.v.images[0]); return null; } const value = col?.getValue(d); // Load aysnc to avoid triggering return abortAble( new Promise((resolve) => { window.setTimeout(() => resolve(value), 500); }), ).then((image) => { if (!value || typeof image === 'symbol') { return; } n.style.backgroundImage = `url('${getImageURL(value, col.getFilter()?.filter, col.getAlign())}')`; n.title = value; }); } return null; }, }; } createGroup(col: SMILESColumn, context: IRenderContext): IGroupCellRenderer { return { template, update: (n: HTMLImageElement, group: IOrderedGroup) => { context.tasks.groupRows(col, group, 'SMILESRendererGroup', (rows) => { return abortAble(getReducedImages(Array.from(rows.map((row) => col.getLabel(row))))).then((res: any) => { n.style.backgroundImage = res ? svgToCSSBackground(res) : ''; }); }); }, }; } createSummary(): ISummaryRenderer { // no renderer return { template: `
`, update: () => {}, }; } }