Skip to content

Commit 79e68d0

Browse files
committed
Enable toggling of annotations
1 parent 5729631 commit 79e68d0

File tree

6 files changed

+252
-179
lines changed

6 files changed

+252
-179
lines changed

pages/components/components.js

Lines changed: 188 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -308,54 +308,208 @@ class InChIOptionsLatestMoInElement extends InChIOptionsElement {
308308
}
309309
}
310310

311+
function createAnnotation(text, color) {
312+
const annotation = document.createElement("div");
313+
annotation.textContent = text;
314+
annotation.classList.add(color);
315+
annotation.classList.add("active");
316+
annotation.style.color = "black";
317+
annotation.style.fontWeight = "500";
318+
annotation.style.paddingLeft = "1%";
319+
annotation.style.paddingRight = "1%";
320+
321+
return annotation;
322+
}
323+
324+
function getAnnotationData(inchi, auxinfo) {
325+
// Returns a map of maps of maps. Innermost maps can be empty.
326+
const inchiParsed = parseInchi(inchi);
327+
const auxinfoParsed = parseAuxinfo(auxinfo);
328+
329+
const annotationData = new Map();
330+
331+
annotationData.set("canonicalIndex", auxinfoParsed.get("N"));
332+
annotationData.set("equivalenceClass", auxinfoParsed.get("E"));
333+
annotationData.set("hydrogenGroup", inchiParsed.get("h"));
334+
annotationData.set(
335+
"hydrogenGroupClass",
336+
mapCanonicalAtomIndicesToMobileHydrogenGroupClasses(
337+
annotationData.get("hydrogenGroup"),
338+
auxinfoParsed.get("gE")
339+
)
340+
);
341+
342+
return annotationData;
343+
}
344+
311345
class NGLViewerElement extends HTMLElement {
312346
constructor() {
313347
super();
314-
}
315348

316-
connectedCallback() {
317-
this.innerHTML = `<div id="ngl-viewport" style="width: 100%; height: 600px;" /div>`;
318-
const viewport = this.querySelector("#ngl-viewport");
349+
this.annotationColors = {
350+
index: "annotation-index",
351+
canonicalIndex: "annotation-canonical-index",
352+
equivalenceClass: "annotation-equivalence-class",
353+
hydrogenGroup: "annotation-hydrogen-group",
354+
hydrogenGroupClass: "annotation-hydrogen-group-class",
355+
};
319356

320-
this.stage = new NGL.Stage(viewport, { backgroundColor: "white" });
357+
const annotationButtonTexts = {
358+
index: "Index",
359+
canonicalIndex: "Canonical Index",
360+
equivalenceClass: "Equivalence Class",
361+
hydrogenGroup: "Hydrogen Group",
362+
hydrogenGroupClass: "Hydrogen Group Class",
363+
};
321364

322-
const resizeObserver = new window.ResizeObserver(() =>
323-
this.stage.handleResize()
365+
this.annotationButtons = Object.keys(this.annotationColors).map((id) => ({
366+
id,
367+
text: annotationButtonTexts[id],
368+
color: this.annotationColors[id],
369+
}));
370+
371+
this.annotationSelection = Object.fromEntries(
372+
Object.keys(this.annotationColors).map((id) => [id, false])
324373
);
325-
resizeObserver.observe(viewport);
326-
}
327-
}
328374

329-
class AtomLabelLegendElement extends HTMLElement {
330-
constructor() {
331-
super();
375+
this.innerHTML = `<div id="annotation-selection" class="mt-2"></div>
376+
<div id="ngl-viewport" style="width: 100%; height: 600px;"></div>`;
377+
378+
this.stage = undefined;
379+
this.structure = undefined;
380+
this.annotationData = undefined;
381+
this.annotationSelectionElement = undefined;
332382
}
333383

334384
connectedCallback() {
335-
const annotationLegend = document.createElement("div");
336-
annotationLegend.style.display = "flex";
337-
annotationLegend.style.flexWrap = "wrap";
385+
const viewportElement = this.querySelector("#ngl-viewport");
386+
this.stage = new NGL.Stage(viewportElement, { backgroundColor: "white" });
387+
const resizeObserver = new ResizeObserver(() => this.stage.handleResize());
388+
resizeObserver.observe(viewportElement);
338389

339-
annotationLegend.appendChild(
340-
createAnnotation("Original Index", annotationColors.index)
341-
);
342-
annotationLegend.appendChild(
343-
createAnnotation("Canonical Index", annotationColors.canonicalIndex)
344-
);
345-
annotationLegend.appendChild(
346-
createAnnotation("Equivalence Class", annotationColors.equivalenceClass)
347-
);
348-
annotationLegend.appendChild(
349-
createAnnotation("Hydrogen Group", annotationColors.mobileHydrogenGroup)
350-
);
351-
annotationLegend.appendChild(
352-
createAnnotation(
353-
"Hydrogen Group Class",
354-
annotationColors.mobileHydrogenGroupClass
355-
)
390+
this.annotationSelectionElement = this.querySelector(
391+
"#annotation-selection"
356392
);
393+
this.annotationButtons.forEach((button) => {
394+
const buttonElement = document.createElement("button");
395+
buttonElement.id = button.id;
396+
buttonElement.textContent = button.text;
397+
buttonElement.classList.add(button.color);
398+
buttonElement.classList.add("annotation-button");
399+
buttonElement.disabled = true;
400+
401+
buttonElement.addEventListener("click", () => {
402+
const isActive = buttonElement.classList.toggle("active");
403+
this.annotationSelection[button.id] = isActive;
404+
this.annotateStructure();
405+
});
406+
407+
this.annotationSelectionElement.appendChild(buttonElement);
408+
});
409+
}
410+
411+
async loadStructure(molfile, inchi, auxinfo) {
412+
this.stage.removeAllComponents();
413+
414+
const molfileBlob = new Blob([molfile], { type: "text/plain" });
415+
try {
416+
this.structure = await this.stage.loadFile(molfileBlob, { ext: "sdf" });
417+
this.structure.addRepresentation("ball+stick", {
418+
multipleBond: "symmetric",
419+
});
420+
this.annotationData = getAnnotationData(inchi, auxinfo);
421+
this.annotationButtons.forEach((button) => {
422+
const buttonElement = this.annotationSelectionElement.querySelector(
423+
`#${button.id}`
424+
);
425+
const annotationAvailable =
426+
button.id === "index"
427+
? true
428+
: this.annotationData.get(button.id).size > 0;
429+
buttonElement.disabled = !annotationAvailable;
430+
buttonElement.classList.remove("active");
431+
});
432+
this.annotationSelection = Object.fromEntries(
433+
Object.keys(this.annotationColors).map((id) => [id, false])
434+
);
435+
436+
this.structure.autoView();
437+
} catch (error) {
438+
console.log(error);
439+
this.structure = undefined;
440+
this.annotationData = undefined;
441+
this.annotationButtons.forEach((button) => {
442+
const buttonElement = this.annotationSelectionElement.querySelector(
443+
`#${button.id}`
444+
);
445+
buttonElement.disabled = true;
446+
buttonElement.classList.remove("active");
447+
});
448+
}
449+
}
450+
451+
annotateStructure() {
452+
if (!(this.structure && this.annotationData)) {
453+
return;
454+
}
455+
456+
this.structure.removeAllAnnotations();
457+
this.structure.structure.eachAtom((atom) => {
458+
const annotations = document.createElement("div");
459+
annotations.style.display = "flex";
460+
annotations.style.height = "20px";
461+
462+
const atomIndex = atom.index + 1;
463+
const canonicalIndex = this.annotationData
464+
.get("canonicalIndex")
465+
.get(atomIndex);
466+
const equivalenceClass = this.annotationData
467+
.get("equivalenceClass")
468+
.get(canonicalIndex);
469+
const hydrogenGroup = this.annotationData
470+
.get("hydrogenGroup")
471+
.get(canonicalIndex);
472+
const hydrogenGroupClass = this.annotationData
473+
.get("hydrogenGroupClass")
474+
.get(canonicalIndex);
475+
476+
if (this.annotationSelection.index) {
477+
annotations.appendChild(
478+
createAnnotation(atomIndex, this.annotationColors.index)
479+
);
480+
}
481+
482+
if (canonicalIndex && this.annotationSelection.canonicalIndex) {
483+
annotations.appendChild(
484+
createAnnotation(canonicalIndex, this.annotationColors.canonicalIndex)
485+
);
486+
}
357487

358-
this.appendChild(annotationLegend);
488+
if (equivalenceClass && this.annotationSelection.equivalenceClass) {
489+
annotations.appendChild(
490+
createAnnotation(
491+
equivalenceClass,
492+
this.annotationColors.equivalenceClass
493+
)
494+
);
495+
}
496+
if (hydrogenGroup && this.annotationSelection.hydrogenGroup) {
497+
annotations.appendChild(
498+
createAnnotation(hydrogenGroup, this.annotationColors.hydrogenGroup)
499+
);
500+
}
501+
if (hydrogenGroupClass && this.annotationSelection.hydrogenGroupClass) {
502+
annotations.appendChild(
503+
createAnnotation(
504+
hydrogenGroupClass,
505+
this.annotationColors.hydrogenGroupClass
506+
)
507+
);
508+
}
509+
510+
this.structure.addAnnotation(atom.positionToVector3(), annotations);
511+
});
512+
this.structure.setVisibility(true); // Re-render the structure.
359513
}
360514
}
361515

@@ -371,4 +525,3 @@ customElements.define(
371525
InChIOptionsLatestMoInElement
372526
);
373527
customElements.define("inchi-ngl-viewer", NGLViewerElement);
374-
customElements.define("inchi-atom-label-legend", AtomLabelLegendElement);

pages/components/inchi-tools.html

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@
115115
<div class="ratio inputtextarea">
116116
<div class="w-100">
117117
<textarea
118-
id="inchi-tab2-molfileTextarea"
118+
id="inchi-tab2-molfile"
119119
class="form-control pe-5 h-100"
120120
style="resize: none"
121121
autocomplete="off"
@@ -127,7 +127,7 @@
127127
<button
128128
class="btn btn-outline-secondary border-0"
129129
style="position: absolute; top: 10px; right: 10px"
130-
onclick="document.getElementById('inchi-tab2-molfileTextarea').value = '';updateInchiTab2()"
130+
onclick="document.getElementById('inchi-tab2-molfile').value = '';updateInchiTab2()"
131131
>
132132
<i class="bi bi-trash"></i>
133133
</button>
@@ -160,9 +160,8 @@
160160
title="Log"
161161
></inchi-result-field>
162162
</div>
163-
<div class="col-xl-7">
164-
<inchi-atom-label-legend></inchi-atom-label-legend>
165-
<inchi-ngl-viewer id="inchi-ngl-viewer-tab2"></inchi-ngl-viewer>
163+
<div class="col-xl-7 bounding-box">
164+
<inchi-ngl-viewer id="inchi-tab2-ngl-viewer"></inchi-ngl-viewer>
166165
</div>
167166
</div>
168167
</div>

pages/css/index.css

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,39 @@ button.form-select.multiselect:focus {
7878
background-color: #f9f9f9;
7979
width: 100%;
8080
}
81+
82+
.annotation-button {
83+
background-color: #fff;
84+
border: 3px solid transparent;
85+
cursor: pointer;
86+
transition: border-color 150ms ease, box-shadow 150ms ease;
87+
}
88+
89+
.annotation-button:not([disabled]):hover {
90+
border: 3px solid #add8e6;
91+
}
92+
93+
.annotation-button:not([disabled]):focus-visible {
94+
border: 3px solid #add8e6;
95+
outline: none;
96+
}
97+
98+
.annotation-index.active {
99+
background-color: rgb(209, 213, 220, 0.7);
100+
}
101+
102+
.annotation-canonical-index.active {
103+
background-color: rgb(0, 255, 0, 0.7);
104+
}
105+
106+
.annotation-equivalence-class.active {
107+
background-color: rgb(255, 255, 0, 0.7);
108+
}
109+
110+
.annotation-hydrogen-group.active {
111+
background-color: rgb(255, 102, 255, 0.7);
112+
}
113+
114+
.annotation-hydrogen-group-class.active {
115+
background-color: rgb(0, 255, 255, 0.7);
116+
}

pages/inchi.js

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,10 @@ function mapCanonicalAtomIndicesToMobileHydrogenGroupClasses(
237237
) {
238238
const canonicalAtomIndicesToMobileHydrogenGroupClasses = new Map();
239239
if (
240-
!canonicalAtomIndicesToMobileHydrogenGroups ||
241-
!mobileHydrogenGroupsToMobileHydrogenGroupClasses
240+
!canonicalAtomIndicesToMobileHydrogenGroups.size ||
241+
!mobileHydrogenGroupsToMobileHydrogenGroupClasses.size
242242
) {
243-
return undefined;
243+
return canonicalAtomIndicesToMobileHydrogenGroupClasses;
244244
}
245245
for (const [
246246
canonicalAtomIndex,
@@ -260,17 +260,19 @@ function mapCanonicalAtomIndicesToMobileHydrogenGroupClasses(
260260

261261
function parseInchi(inchi) {
262262
const layerParsers = { h: parseMobileHydrogenGroups };
263-
const layerNames = Object.keys(layerParsers);
264-
const layerResults = new Map();
265-
const layers = inchi.split("/");
263+
const layerResults = new Map(
264+
Object.keys(layerParsers).map((name) => [name, new Map()])
265+
);
266266

267+
const layers = inchi.split("/");
267268
for (const layer of layers) {
268-
for (const layerName of layerNames) {
269-
if (layer.startsWith(layerName)) {
270-
const parser = layerParsers[layerName];
271-
const layerContent = layer.slice(layerName.length);
272-
layerResults.set(layerName, parser(layerContent));
273-
}
269+
const parserMatch = Object.entries(layerParsers).find(([layerName]) =>
270+
layer.startsWith(layerName)
271+
);
272+
if (parserMatch) {
273+
const [layerName, parser] = parserMatch;
274+
const layerContent = layer.slice(layerName.length);
275+
layerResults.set(layerName, parser(layerContent));
274276
}
275277
}
276278

@@ -284,7 +286,9 @@ function parseAuxinfo(auxinfo) {
284286
gE: parseMobileHydrogenGroupClasses,
285287
rC: parseCoordinates,
286288
};
287-
const layerResults = new Map();
289+
const layerResults = new Map(
290+
Object.keys(layerParsers).map((key) => [key, new Map()])
291+
);
288292
const layers = auxinfo.split("/");
289293

290294
for (const layer of layers) {

0 commit comments

Comments
 (0)