UNPKG

3dmol

Version:

JavaScript/TypeScript molecular visualization library

1,571 lines (1,290 loc) 66 kB
/** * $3Dmol.UI - UI creates panels in the viewer to assist control of the viewport * @constructor * @param {$3Dmol.StateManager} stateManager StateManager is required to have interaction between glviewer and the ui. * @param {Object} config Loads the user defined parameters to generate the ui * @param {Object} parentElement Refers the parent division used to hold the canvas for 3Dmol.js */ $3Dmol.UI = (function () { /* The dictionaries are for dropdown menus and validation of the viewer */ // prop : It is used to add the option for property in context menu in the 3dmol ui // the code for prop can be found under /ui/ui.js -> UI -> ContextMenu -> setProperties -> submit.ui.on // gui : It is used to generate forms for different features in the 3dmol ui // the code for gui can be found under /ui/form.js -> Form (Form defination) // floatType : separates integer from float since these are used in // input validation of the 3dmol ui var validAtomSpecs = { "resn": { type: "string", valid: true, prop: true, gui: true }, // Parent residue name "x": { type: "number", floatType: true, valid: false, step: 0.1, prop: true }, // Atom's x coordinate "y": { type: "number", floatType: true, valid: false, step: 0.1, prop: true }, // Atom's y coordinate "z": { type: "number", floatType: true, valid: false, step: 0.1, prop: true }, // Atom's z coordinate "color": { type: "color", gui: false }, // Atom's color, as hex code "surfaceColor": { type: "color", gui: false }, // Hex code for color to be used for surface patch over this atom "elem": { type: "element", gui: true, prop: true }, // Element abbreviation (e.g. 'H', 'Ca', etc) "hetflag": { type: "boolean", valid: false, gui: true }, // Set to true if atom is a heteroatom "chain": { type: "string", gui: true, prop: true }, // Chain this atom belongs to, if specified in input file (e.g 'A' for chain A) "resi": { type: "array_range", gui: true }, // Residue number "icode": { type: "number", valid: false, step: 0.1 }, "rescode": { type: "number", valid: false, step: 0.1, prop: true }, "serial": { type: "number", valid: false, step: 0.1 }, // Atom's serial id numbermodels "atom": { type: "string", valid: false, gui: true, prop: true }, // Atom name; may be more specific than 'elem' (e.g 'CA' for alpha carbon) "bonds": { type: "array", valid: false }, // Array of atom ids this atom is bonded to "ss": { type: "string", valid: false }, // Secondary structure identifier (for cartoon render; e.g. 'h' for helix) "singleBonds": { type: "boolean", valid: false }, // true if this atom forms only single bonds or no bonds at all "bondOrder": { type: "array", valid: false }, // Array of this atom's bond orders, corresponding to bonds identfied by 'bonds' "properties": { type: "properties", valid: false }, // Optional mapping of additional properties "b": { type: "number", floatType: true, valid: false, step: 0.1, prop: true }, // Atom b factor data "pdbline": { type: "string", valid: false }, // If applicable, this atom's record entry from the input PDB file (used to output new PDB from models) "clickable": { type: "boolean", valid: false, gui: false }, // Set this flag to true to enable click selection handling for this atom "contextMenuEnabled": { type: "boolean", valid: false, gui: false }, // Set this flag to true to enable click selection handling for this atom "callback": { type: "function", valid: false }, // Callback click handler function to be executed on this atom and its parent viewer "invert": { type: "boolean", valid: false }, // for selection, inverts the meaning of the selection //unsure about this "reflectivity": { type: "number", floatType: true, gui: false, step: 0.1 }, //for describing the reflectivity of a model "altLoc": { type: "invalid", valid: false }, //alternative location, e.g. in PDB "sym": { type: 'number', gui: false }, //which symmetry }; //type is irrelivent here becuase htey are are invalid var validExtras = { // valid atom specs are ok too "model": { type: "string", valid: false }, // a single model or list of models from which atoms should be selected "bonds": { type: "number", valid: false, gui: true }, // overloaded to select number of bonds, e.g. {bonds: 0} will select all nonbonded atoms "predicate": { type: "string", valid: false }, // user supplied function that gets passed an {AtomSpec} and should return true if the atom should be selected "invert": { type: "boolean", valid: false, gui: true }, // if set, inverts the meaning of the selection "byres": { type: "boolean", valid: false, gui: true }, // if set, expands the selection to include all atoms of any residue that has any atom selected "expand": { type: "number", valid: false, gui: false }, // expands the selection to include all atoms within a given distance from the selection "within": { type: "string", valid: false }, // intersects the selection with the set of atoms within a given distance from another selection "and": { type: "string", valid: false }, // and boolean logic "or": { type: "string", valid: false }, // or boolean logic "not": { type: "string", valid: false }, // not boolean logic }; var validAtomSelectionSpecs = $3Dmol.extend({}, validAtomSpecs); validAtomSelectionSpecs = $3Dmol.extend(validAtomSelectionSpecs, validExtras); var validLineSpec = { "hidden": { type: "boolean", gui: true }, "linewidth": { type: "number", floatType: true, gui: true, step: 0.1, default: 1.0 }, "colorscheme": { type: "colorscheme", gui: true }, "color": { type: "color", gui: true }, "opacity": { type: "number", floatType: true, gui: true, step: 0.1, default: 1, min: 0, max: 1 }, }; var validCrossSpec = { "hidden": { type: "boolean", gui: true }, "linewidth": { type: "number", floatType: true, gui: false, step: 0.1, default: 1.0, min: 0 },//deprecated "colorscheme": { type: "colorscheme", gui: true }, "color": { type: "color", gui: true }, "radius": { type: "number", floatType: true, gui: true, step: 0.1, default: 1, min: 0.1 }, "scale": { type: "number", floatType: true, gui: true, step: 0.1, default: 1, min: 0 }, "opacity": { type: "number", floatType: true, gui: true, step: 0.1, default: 1, min: 0, max: 1 }, }; var validStickSpec = { "hidden": { type: "boolean", gui: true }, "colorscheme": { type: "colorscheme", gui: true }, "color": { type: "color", gui: true }, "radius": { type: "number", floatType: true, gui: true, step: 0.1, default: 0.25, min: 0.1 }, "singleBonds": { type: "boolean", gui: true }, "opacity": { type: "number", floatType: true, gui: true, step: 0.1, default: 1, min: 0, max: 1 }, }; var validSphereSpec = { "hidden": { type: "boolean", gui: false }, // needed in the new gui it has separate function to hide the spheres "singleBonds": { type: "boolean", gui: true }, "colorscheme": { type: "colorscheme", gui: true }, "color": { type: "color", gui: true }, "radius": { type: "number", floatType: true, gui: true, step: 0.1, default: 1.5, min: 0 }, "scale": { type: "number", floatType: true, gui: true, step: 0.1, default: 1.0, min: 0.1 }, "opacity": { type: "number", floatType: true, gui: true, step: 0.1, default: 1, min: 0, max: 1 }, }; var validCartoonSpec = { "style": { validItems: ["trace", "oval", "rectangle", "parabola", "edged"], gui: true }, "color": { type: "color", gui: true, spectrum: true }, "arrows": { type: "boolean", gui: true }, "ribbon": { type: "boolean", gui: true }, "hidden": { type: "boolean", gui: true }, "tubes": { type: "boolean", gui: true }, "thickness": { type: "number", floatType: true, gui: true, step: 0.1, default: 1, min: 0 }, "width": { type: "number", floatType: true, gui: true, step: 0.1, default: 1, min: 0 }, "opacity": { type: "number", floatType: true, gui: true, step: 0.1, default: 1, min: 0, max: 1 }, }; var validAtomStyleSpecs = { "line": { validItems: validLineSpec, type: "form", gui: true }, // draw bonds as lines "cross": { validItems: validCrossSpec, type: "form", gui: true }, // draw atoms as crossed lines (aka stars) "stick": { validItems: validStickSpec, type: "form", gui: true }, // draw bonds as capped cylinders "sphere": { validItems: validSphereSpec, type: "form", gui: true }, // draw atoms as spheres "cartoon": { validItems: validCartoonSpec, type: "form", gui: true }, // draw cartoon representation of secondary structure "colorfunc": { validItems: null, type: "js", valid: false }, "clicksphere": { validItems: validSphereSpec, type: "form" } //invisible style for click handling }; var validSurfaceSpecs = { "opacity": { type: "number", floatType: true, gui: true, step: 0.01, default: 1, min: 0, max: 1 }, "colorscheme": { type: "colorscheme", gui: true }, "color": { type: "color", gui: true }, "voldata": { type: "number", floatType: true, gui: false }, "volscheme": { type: "number", floatType: true, gui: false }, "map": { type: "number", gui: false } }; function UI(stateManager, config, parentElement) { config = config || {} // Extract the viewer and then render it var icons = new $3Dmol.UI.Icons(); var _editingForm = null; var mainParent = $(parentElement[0]); // Generates the necessary UI elements var HEIGHT = config.height; this.tools = generateUI(config); /** * Creates all the jquery object of different UI features */ function generateUI() { var modelToolBar = new ModelToolbar(); mainParent.append(modelToolBar.ui); setLocation(mainParent, modelToolBar.ui, 'left', 'top'); // modelToolBar.updateInputLength(); var contextMenu = new ContextMenu(); mainParent.append(contextMenu.ui); setPosition(contextMenu.ui, 100, 100) var surfaceMenu = new SurfaceMenu(); mainParent.append(surfaceMenu.ui); setLocation(mainParent, surfaceMenu.ui, 'right', 'top', 0, modelToolBar.ui.height() + 5); var selectionBox = new SelectionBox(icons.select); mainParent.append(selectionBox.ui); setLocation(mainParent, selectionBox.ui, 'left', 'top', 0, modelToolBar.ui.height() + 5); // Fixing Context Menu Behaviour selectionBox.ui.on('mousedown', () => { stateManager.exitContextMenu(); }); surfaceMenu.ui.on('mousedown', () => { stateManager.exitContextMenu(); }); return { modelToolBar: modelToolBar, selectionBox: selectionBox, contextMenu: contextMenu, surfaceMenu: surfaceMenu } } /** * Resize the panel with respect to the new viewport * * @function $3Dmol.UI#resize */ this.resize = function () { var selectionBox = this.tools.selectionBox; var surfaceMenu = this.tools.surfaceMenu; var modelToolBar = this.tools.modelToolBar; var HEIGHT = mainParent.height(); setLocation(mainParent, modelToolBar.ui, 'left', 'top'); // modelToolBar.updateInputLength(); setLocation(mainParent, selectionBox.ui, 'left', 'top', 0, modelToolBar.ui.height() + 5); selectionBox.updateScrollBox(HEIGHT); setLocation(mainParent, surfaceMenu.ui, 'right', 'top', 0, modelToolBar.ui.height() + 5); surfaceMenu.updateScrollBox(HEIGHT); } /* * ModelToolbar is part of $3Dmol.UI to edit or change the model loaded into the viewer * * @function ModelToolbar */ function ModelToolbar() { var boundingBox = this.ui = $('<div></div>'); boundingBox.css({ 'position': 'relative', 'min-width': '150px' }); var modelButton = new button(icons.molecule, 20, { tooltip: 'Toggle Model Selection Bar' }); boundingBox.append(modelButton.ui); modelButton.ui.css({ 'display': 'inline-block', 'top': '3px', }); var control = { urlType: { active: true, value: null, key: 'Model type' }, url: { active: true, value: null, key: 'Url' }, }; var surroundingBox = $('<div></div>'); surroundingBox.css({ 'display': 'inline-block', 'background': '#e4e4e4', 'padding': '2px', 'border-radius': '3px', // 'width' : '90%' }); boundingBox.append(surroundingBox); var currentModelBox = $('<div></div>'); currentModelBox.css({ }); var currentModel = $('<div></div>'); currentModel.css({ 'display': 'inline-block', 'font-family': 'Arial', 'font-size': '12px', 'font-weight': 'bold', // 'padding' : '3px' }); currentModelBox.append(currentModel); var changeButton = new button(icons.change, 16, { tooltip: 'Change Model', backgroundColor: 'white', bfr: 0.5 }); changeButton.ui.css({ 'display': 'inline-block', 'margin-left': '4px', }); currentModelBox.append(changeButton.ui); currentModelBox.hide(); surroundingBox.append(currentModelBox); var formBox = $('<div></div>'); surroundingBox.append(formBox); var dbs = 'pdb,mmtf,cid'.split(','); var list = this.list = new $3Dmol.UI.Form.ListInput(control.urlType, dbs); list.showAlertBox = false; list.ui.css({ 'display': 'inline-block', }) formBox.append(list.ui); var input = this.url = new $3Dmol.UI.Form.Input(control.url); formBox.append(input.ui); input.ui.css({ 'display': 'inline-block', 'width': '125px' }); // input.setWidth(125); var submitButton = new button(icons.tick, 16, { bfr: 0.5, backgroundColor: 'lightgreen', tooltip: 'Add Model' }); submitButton.ui.css({ 'margin': '0px' }) formBox.append(submitButton.ui); this.updateInputLength = function () { // var width = parentElement.width()*0.3; // boundingBox.width(width); // input.setWidth(width - 12); } modelButton.ui.on('click', () => { surroundingBox.toggle(); }); submitButton.ui.on('click', function () { var validateDb = list.validate(); var validateId = input.validate(); if (validateId && validateDb) { stateManager.addModel(control); } }); /* * Sets the title in the ui with specified value * * @function ModelToolbar#setModel * @param {String} heading Name of the molecule that is to be displayed on the title */ this.setModel = function (heading) { currentModel.text(heading); currentModelBox.show(); formBox.hide(); } changeButton.ui.on('click', function () { currentModelBox.hide(); formBox.show(); input.setValue(''); }); boundingBox.on('keypress', function (e) { if (e.key == 'Enter' || e.key == 'Return') { submitButton.ui.trigger('click') } }); } /* * Selection box creates the UI panel to manipulate selections and style that are drawn * on the viewport * * @function SelectionBox * @param {$3Dmol.UI.Icons} icon takes the svg code for the icon that is to be used to display * selection box * @return {Object} Jquery element of div */ function SelectionBox(icon, side = 'left') { var selectionBox = this.ui = $('<div></div>'); _editingForm = false; var selectionObjects = []; var selections = $('<div></div>'); var scrollBox = $('<div></div>'); selections.css('opacity', '0.9'); var showArea = $('<div></div>'); var addArea = $('<div></div>'); var plusButton = new button(icons.plus, 20, { tooltip: 'Add New Selection' }); plusButton.ui.css('margin', '0px'); var hideButton = new button(icon, 20, { tooltip: 'Toggle Selection Menu' }); this.selectionObjects = []; // Content selectionBox.append(hideButton.ui); selectionBox.append(showArea); selectionBox.css('position', 'absolute'); scrollBox.append(selections); showArea.append(scrollBox); addArea.append(plusButton.ui); var alertBox = new AlertBox(); showArea.append(alertBox.ui); showArea.append(addArea); alertBox.ui.css('width', 162); // CSS if (side == 'left') { selectionBox.css('text-align', 'left'); } else if (side == 'right') { selectionBox.css('text-align', 'right'); } else { // Add alert box code selectionBox.css('text-align', 'right'); } showArea.css('box-sizing', 'border-box'); showArea.css('padding', '3px'); // showArea.css('width', '162px'); scrollBox.css('max-height', HEIGHT * 0.8); scrollBox.css('overflow-y', 'auto'); scrollBox.css('overflow-x', 'visible'); selections.css('box-sizing', 'content-box'); this.updateScrollBox = function (height) { scrollBox.css('max-height', height * 0.8); } // Action var hidden = true; showArea.hide(); hideButton.ui.click(toggleHide); function toggleHide() { if (hidden) { showArea.show(100); } else { showArea.hide(100); } hidden = !hidden; } /* * Card for manipulation of a selection form and related styles * * @function Selection */ function Selection() { var boundingBox = this.ui = $('<div></div>'); var sid = this.id = null; selectionObjects.push(this); boundingBox.css({ 'background': '#e8e8e8', 'padding': '4px 4px 2px 4px', 'border-radius': '6px', 'margin-bottom': '3px', 'position': 'relative', 'width': '156px' }); var header = $('<div></div>'); boundingBox.append(header); var heading = $('<div></div>'); var controls = $('<div></div>'); header.append(heading, controls); heading.css({ 'font-family': 'Arial', 'font-weight': 'bold', 'font-size': '12px', 'display': 'inline-block', 'width': '60px' }); controls.css({ 'display': 'inline-block' }); header.hide(); controls.editMode = false; var removeButton = new button(icons.minus, 16, { bfr: 0.5, backgroundColor: '#f06f6f', tooltip: 'Remove Selection' }); var editButton = new button(icons.pencil, 16, { tooltip: 'Edit Selection' }); var visibleButton = new button(icons.visible, 16, { tooltip: 'Show / Hide Selection' }); controls.append(removeButton.ui) controls.append(editButton.ui); controls.append(visibleButton.ui); var parameters = $('<div></div>'); boundingBox.append(parameters); var styleHolder = $('<div></div>'); removeButton.ui.on('click', function () { stateManager.removeSelection(sid); boundingBox.detach(); //delete this; }); editButton.ui.on('click', function () { parameters.toggle(); }); var hidden = false; visibleButton.ui.on('click', () => { stateManager.toggleHide(sid); if (hidden) { hidden = false; visibleButton.setSVG(icons.visible); } else { hidden = true; visibleButton.setSVG(icons.invisible); } }); var styleBox = new StyleBox(); styleHolder.append(styleBox.ui); styleBox.ui.css({ 'position': 'static', // 'left' : '0', 'width': 'px', 'border-radius': '4px' }); styleBox.ui.hide(); var allControl = this.allSelector = { key: 'Select All Atom', value: null, active: true } var allCheckBox = new $3Dmol.UI.Form.Checkbox(allControl); parameters.append(allCheckBox.ui); var selectionFormControl = this.selectionValue = { key: 'Selection Spec', value: null, active: true } var selectionSpecForm = new $3Dmol.UI.Form(validAtomSelectionSpecs, selectionFormControl); parameters.append(selectionSpecForm.ui); var submitControls = $('<div></div>'); var submit = new button(icons.tick, 16, { backgroundColor: 'lightgreen', tooltip: 'Submit' }); var cancel = new button(icons.cross, 16, { backgroundColor: 'lightcoral', tooltip: 'Cancel' }); submitControls.append(submit.ui, cancel.ui); var alertBox = new AlertBox(); parameters.append(alertBox.ui); parameters.append(submitControls); boundingBox.append(styleHolder); allCheckBox.update = function () { selectionSpecForm.ui.toggle(); } function finalizeSelection(id) { header.show(); controls.editMode = true; sid = this.id = id; heading.text('Sel#' + id); boundingBox.attr('data-id', id); parameters.hide(); styleBox.setSid(id); styleBox.ui.show(); } function checkAndAddSelection(sid = null) { var validate = selectionSpecForm.validate(); if (validate) { selectionSpecForm.getValue(); var checkAtoms = stateManager.checkAtoms(selectionFormControl.value); if (Object.keys(selectionFormControl.value).length == 0) { alertBox.error('Please enter some input'); } else { if (checkAtoms) { var id = stateManager.addSelection(selectionFormControl.value, sid); finalizeSelection(id); if (sid == null) _editingForm = false; } else { alertBox.error('No atom selected'); } } } else { alertBox.error('Invalid Input'); } } function removeSelf() { // delete selectionToRemove; } submit.ui.on('click', () => { if (controls.editMode == false) { if (allControl.value) { let id = stateManager.addSelection({}); finalizeSelection(id); _editingForm = false; } else { checkAndAddSelection(); } } else { if (allControl.value) { stateManager.addSelection({}, sid); finalizeSelection(sid); } else { checkAndAddSelection(sid); } } }); var self = this; cancel.ui.on('click', () => { if (controls.editMode) { parameters.hide(); } else { boundingBox.detach(); removeSelf(self); _editingForm = false; } }); boundingBox.on('keyup', (e) => { if (e.key == 'Enter') { submit.ui.trigger('click'); } }); /* * @function Selection#setProperty * @param {string} id Id of the selection created in StateManager * @param {Object} specs Defination of the selection that will be used to set default * values in the form */ this.setProperty = function (id, specs) { // check for all selection if (Object.keys(specs).length == 0) { allCheckBox.setValue(true) } else { selectionSpecForm.setValue(specs); } // finalize the selection finalizeSelection(id); } /* * Adds style to the given selection * * @function Selection#addStyle * @param {String} selId Id of the selection to inititate the StyleBox * @param {String} styleId Id of the style that is created through StateManager * @param {AtomStyleSpecs} styleSpecs */ this.addStyle = function (selId, styleId, styleSpecs) { styleBox.addStyle(selId, styleId, styleSpecs); } } plusButton.ui.on('click', () => { if (!_editingForm) { var newSelection = new Selection(); selections.append(newSelection.ui); _editingForm = true; } else { alertBox.warning('Please complete the previous form'); } }); /* * Remove all the selection card from the ui */ this.empty = function () { selections.empty(); _editingForm = false; } /* * Adds or create new selection card * * @function SelectionBox#editSelection * @param {String} id Id created in StateManager and passed down to this function during call * @param {AtomSelectionSpec} selSpec Selection spec that is used to generate the selection form * @param {String} styleId Id of style created in StateManager * @param {AtomStyleSpecs} styleSpec Style spec if specified add the selection to the current selection */ this.editSelection = function (id, selSpec, styleId, styleSpec) { // if selection does not exist create new // This thing works but I am not sure how! // Search selection with id var selectionUI = selections.children('[data-id=' + id + ']'); if (selectionUI.length == 0) { var selection = new Selection(); selection.setProperty(id, selSpec); selections.append(selection.ui); if (styleId != null) { selection.addStyle(id, styleId, styleSpec); } } } } /* * Creates StyleBox for listing out different styles inside the selection * * @function StyleBox * @param {String} selId Id of the selection for which the style box is created * @param {String} side Alignment of text inside the box */ function StyleBox(selId, side = 'left') { var styleBox = this.ui = $('<div></div>'); _editingForm = false; var sid = this.sid = selId; // selection id this.setSid = function (id) { sid = this.sid = id; } var styles = $('<div></div>'); var scrollBox = $('<div></div>'); styles.css('opacity', '0.9'); var showArea = $('<div></div>'); var addArea = $('<div></div>'); addArea.css('text-align', 'center'); var plusButton = new button(icons.plus, 20, { tooltip: 'Add New Style' }); plusButton.ui.css('margin', '0px'); this.selectionObjects = []; // Content styleBox.append(showArea); styleBox.css('position', 'absolute'); scrollBox.append(styles); showArea.append(scrollBox); var alertBox = new AlertBox(); showArea.append(alertBox.ui); addArea.append(plusButton.ui); showArea.append(addArea); // CSS if (side == 'left') { styleBox.css('text-align', 'left'); } else if (side == 'right') { styleBox.css('text-align', 'right'); } else { // Add alert box code styleBox.css('text-align', 'right'); } showArea.css('box-sizing', 'border-box'); showArea.css('padding', '3px'); // showArea.css('width', '162px'); showArea.css('background-color', '#a4a4a4') showArea.css('border-radius', '4px'); // scrollBox.css('max-height', HEIGHT*0.8); scrollBox.css('overflow', 'hidden'); // styles.css('max-height', HEIGHT*0.8); // styles.css('overflow', 'auto'); styles.css('box-sizing', 'content-box'); /* * Style card to define the value of the style * * @param {string} sid Id of the selction for which the style box is created * and this stye will be added under that selection */ function Style(sid) { var boundingBox = this.ui = $('<div></div>'); var stid = this.id = null; // style id boundingBox.css({ 'background': '#e8e8e8', 'padding': '4px 4px 2px 4px', 'border-radius': '6px', 'margin-bottom': '3px', 'position': 'relative' }); var header = $('<div></div>'); boundingBox.append(header); var heading = $('<div></div>'); var controls = $('<div></div>'); header.append(heading, controls); heading.css({ 'font-family': 'Arial', 'font-weight': 'bold', 'font-size': '12px', 'display': 'inline-block', 'width': '60px' }); controls.css({ 'display': 'inline-block' }); header.hide(); controls.editMode = false; var removeButton = new button(icons.minus, 16, { bfr: 0.5, backgroundColor: '#f06f6f', tooltip: 'Remove Style' }); var editButton = new button(icons.pencil, 16, { tooltip: 'Edit Style' }); var visibleButton = new button(icons.visible, 16, { tooltip: 'Show / Hide Style' }); controls.append(removeButton.ui) controls.append(editButton.ui); controls.append(visibleButton.ui); var parameters = $('<div></div>'); boundingBox.append(parameters); removeButton.ui.on('click', { parent: this, stid: stid }, function () { stateManager.removeStyle(sid, stid); boundingBox.detach(); //delete this; }); editButton.ui.on('click', function () { parameters.toggle(); }); var hidden = false; visibleButton.ui.on('click', () => { stateManager.toggleHideStyle(sid, stid); if (hidden) { hidden = false; visibleButton.setSVG(icons.visible); } else { hidden = true; visibleButton.setSVG(icons.invisible); } }); var styleFormControl = this.selectionValue = { key: 'Style Spec', value: null, active: true } var styleSpecForm = new $3Dmol.UI.Form(validAtomStyleSpecs, styleFormControl); parameters.append(styleSpecForm.ui); var submitControls = $('<div></div>'); var submit = new button(icons.tick, 16, { backgroundColor: 'lightgreen', tooltip: 'Submit' }); var cancel = new button(icons.cross, 16, { backgroundColor: 'lightcoral', tooltip: 'Cancel' }); submitControls.append(submit.ui, cancel.ui); var alertBox = new AlertBox(); parameters.append(alertBox.ui); parameters.append(submitControls); function finalizeStyle(id) { header.show(); controls.editMode = true; stid = id; heading.text('Sty#' + id); parameters.hide(); } function checkAndAddStyle(stid = null) { var validate = styleSpecForm.validate(); if (validate) { styleSpecForm.getValue(); if (Object.keys(styleFormControl.value).length == 0) { alertBox.error('Please enter some value'); } else { var id = stateManager.addStyle(styleFormControl.value, sid, stid); finalizeStyle(id); if (stid == null) _editingForm = false; } } else { alertBox.error('Invalid Input'); } } submit.ui.on('click', () => { if (controls.editMode == false) { checkAndAddStyle(); } else { var id = stid styleSpecForm.getValue(); if (Object.keys(styleFormControl.value).length == 0) { alertBox.error('Please enter some value'); } else { checkAndAddStyle(id); } } }); cancel.ui.on('click', () => { if (controls.editMode) { parameters.hide(); } else { boundingBox.detach(); //delete this; } }); boundingBox.on('keyup', (e) => { if (e.key == 'Enter') { submit.ui.trigger('click'); } }); /** * @function Style#updateStyle * @param {String} styleId Id of the style created by StateManager * @param {AtomStyleSpecs} styleSpec Specs for defining the style and setting default values */ this.updateStyle = function (styleId, styleSpec) { styleSpecForm.setValue(styleSpec); finalizeStyle(styleId); } } plusButton.ui.on('click', () => { if (!_editingForm) { var newStyle = new Style(sid); styles.append(newStyle.ui); _editingForm = true; } else { alertBox.warning('Please complete editing the current form'); } }); /** * @function StyleBox#addStyle * @param {String} selectionId Id of the selection for which styles will be created * @param {String} styleId Id of the style part of the selection * @param {AtomStyleSpecs} styleSpecs Style specs that will be used to create * style for the specified selection and set default values in the Style card */ this.addStyle = function (selectionId, styleId, styleSpecs) { var style = new Style(selectionId); styles.append(style.ui); style.updateStyle(styleId, styleSpecs); } } /* * Add alert messages to different panels * * @function AlertBox * @param {Object} config Configuraiton for alert box display */ function AlertBox(config) { var boundingBox = this.ui = $('<div></div>'); config = config || {} var delay = config.delay || 5000; var autohide = (config.autohide == undefined) ? true : config.autohide; boundingBox.css({ 'font-family': 'Arial', 'font-size': '14px', 'padding': '3px', 'border-radius': '4px', 'margin-top': '2px', 'margin-bottm': '2px', 'font-weight': 'bold', 'text-align': 'center', }); boundingBox.hide(); function hide() { if (autohide) { setTimeout(() => { boundingBox.hide(); }, delay); } } /** * Generate Internal alert message * @param {String} msg Error Message */ this.error = function (msg) { boundingBox.css({ 'background': 'lightcoral', 'color': 'darkred', 'border': '1px solid darkred' }); boundingBox.text(msg); boundingBox.show(); hide(); } /** * Generates Internal warning message * @param {String} msg Warming message */ this.warning = function (msg) { boundingBox.css({ 'background': '#fff3cd', 'color': '#856409', 'border': '1px solid #856409' }); boundingBox.text(msg); boundingBox.show(); hide(); } /** * Generates Internal Info message * @param {String} msg Info message */ this.message = function (msg) { boundingBox.css({ 'background': 'lightgreen', 'color': 'green', 'border': '1px solid green' }); boundingBox.text(msg); boundingBox.show(); hide(); } } /* * Creates the panel for manipulation of labels on the viewport * * @function ContextMenu */ function ContextMenu() { var boundingBox = this.ui = $('<div></div>'); boundingBox.css('position', 'absolute'); // boundingBox.css('border', '1px solid black'); boundingBox.css('border-radius', '3px'); boundingBox.css('background', '#f1f1f1'); boundingBox.css('z-index', 99); var contentBox = $('<div></div>'); contentBox.css('position', 'relative'); boundingBox.css('opacity', '0.85'); boundingBox.append(contentBox); contentBox.css({ 'background': '#f1f1f1', 'border-radius': '4px', 'padding': '4px', 'width': '140px' }); // Context Box // Remove Label Button var labelMenuStyle = { 'background': '#d3e2ee', 'padding': '2px', 'font-family': 'Arial', 'font-weight': 'bold', 'font-size': '12px', 'border-radius': '2px', // 'margin-top':'3px' } var removeLabelMenu = $('<div></div>'); removeLabelMenu.text('Remove Label'); removeLabelMenu.css(labelMenuStyle); removeLabelMenu.css('margin-bottom', '3px'); contentBox.append(removeLabelMenu); removeLabelMenu.hide(); // Label Property List var propertyKeys = Object.keys(validAtomSpecs); var propertyList = []; var propertyObjectList = []; propertyKeys.forEach((prop) => { var propObj = validAtomSpecs; if (propObj[prop].prop === true) { propertyList.push(prop); } }); // Property Menu var propertyMenu = $('<div></div>'); contentBox.append(propertyMenu); /* * Property object used in property menu * * @function Property * @param {String} key Name of the atom property * @param {*} value Value of the property */ function Property(key, value) { this.row = $('<tr></tr>'); var propLabelValue = this.control = { key: '', value: null, active: true, name: key, } this.key = key; this.value = value; var checkbox = new $3Dmol.UI.Form.Checkbox(propLabelValue); var checkboxHolder = $('<td></td>'); checkboxHolder.append(checkbox.ui); var keyHolder = $('<td></td>'); var separatorHolder = $('<td></td>').text(':'); var valueHolder = $('<td></td>'); this.row.append(checkboxHolder, keyHolder, separatorHolder, valueHolder); keyHolder.text(key); if (typeof (value) == "number") { valueHolder.text(value.toFixed(2)); } else { valueHolder.text(value.replace(/\^/g, '')); } } /* * @param {AtomSpec} atom Value of different property of the atom, if the atom has prop : true * then that option is made visible in the context menu */ function setProperties(atom) { propertyMenu.empty(); propertyObjectList = []; var propertyTable = $('<table></table>'); propertyList.forEach((prop) => { var propObj = new Property(prop, atom[prop]); propertyTable.append(propObj.row); propertyObjectList.push(propObj); }); propertyMenu.append(propertyTable); var labelStyleHolder = $('<div><div>'); var labelStyle = $('<div><div>'); labelStyle.text('Style'); labelStyle.css({ 'display': 'inline-block', 'font-family': 'Arial', 'font-size': '14px', 'margin-right': '6px', 'margin-left': '6px' }); var stylesForLabel = new $3Dmol.UI.Form.ListInput(labelStyle, Object.keys($3Dmol.labelStyles)); stylesForLabel.ui.css({ 'display': 'inline-block' }); stylesForLabel.setValue('milk'); labelStyleHolder.append(labelStyle, stylesForLabel.ui); propertyMenu.append(labelStyleHolder); var submit = new button(icons.tick, 18, { backgroundColor: 'lightgreen', tooltip: 'Submit' }); var cancel = new button(icons.cross, 18, { backgroundColor: 'lightcoral', tooltip: 'Cancel' }); var controlButtons = $('<div></div>'); controlButtons.append(submit.ui, cancel.ui); // controlButtons.css('text-align', 'center'); var alertBox = new AlertBox(); propertyMenu.append(alertBox.ui); propertyMenu.append(controlButtons); submit.ui.on('click', () => { var props = processPropertyList(); var labelStyleValidation = stylesForLabel.validate(); if (props != null) { if (labelStyleValidation) { stateManager.addAtomLabel(props, atom, stylesForLabel.getValue().value); stateManager.exitContextMenu(false); } else { alertBox.error('Select style for label'); } } else { alertBox.error('No value selected for label'); } }); cancel.ui.on('click', () => { stateManager.exitContextMenu(); }); } // Previous Labels var labelHolder = $('<div></div>'); contentBox.append(labelHolder); // Add Menu var addMenu = $('<div></div>'); contentBox.append(addMenu); addMenu.css('width', '100%'); var addLabelMenu = $('<div></div>'); addMenu.append(addLabelMenu); addLabelMenu.text('Add Label'); addLabelMenu.css(labelMenuStyle); addLabelMenu.css('margin-bottom', '3px'); addLabelMenu.hide(); // Edit Menu var editMenu = $('<div></div>'); contentBox.append(editMenu); contentBox.css({ 'position': 'relative', }); editMenu.css({ 'background': '#dfdfdf', 'border-radius': '3px', 'font-family': 'Arial', 'font-weight': 'bold', 'font-size': '12px', // 'position': 'absolute', // 'left' : '105%', // 'top' : '0',, 'box-sizing': 'border-box', 'width': '100%', }); editMenu.hide(); var alertBox = new AlertBox({ autohide: false }); contentBox.append(alertBox.ui); // Add Label Inputs /* * Generate input elements that are used as form values in the context menu under addLabelForm * @returns {Object} that holds different input elements */ function generateAddLabelForm() { var addLabelForm = $('<div></div>'); var addLabelValue = { text: { key: 'Label Text', value: null, active: true, }, style: { key: 'Style', value: null, active: true, }, sel: { key: 'Selection', value: null, active: true, } } var formModifierControl = $('<div></div>'); var removeButton = new button(icons.minus, 16); var tick = new button(icons.tick, 16, { backgroundColor: 'lightgreen', tooltip: 'Submit' }); var cross = new button(icons.cross, 16, { backgroundColor: 'lightcoral', tooltip: 'Cancel' }); formModifierControl.append(removeButton.ui, tick.ui, cross.ui); removeButton.ui.hide(); addLabelForm.append(formModifierControl); var addLabelTextBox = $('<div></div>'); var lt = $('<div></div>').text('Label Text'); var addLabelTextInput = new $3Dmol.UI.Form.Input(addLabelValue.text); addLabelTextBox.append(lt, addLabelTextInput.ui); var width = 126//editMenu.innerWidth()*0.8; addLabelTextInput.setWidth(width); addLabelForm.append(addLabelTextBox); var addLabelStyleBox = $('<div></div>'); var ls = $('<div></div>').text('Label Style'); var addLabelStyleInput = new $3Dmol.UI.Form.ListInput(addLabelValue.style, Object.keys($3Dmol.labelStyles)); addLabelStyleInput.setValue('milk'); addLabelStyleBox.append(ls, addLabelStyleInput.ui); addLabelForm.append(addLabelStyleBox); var selectionList = stateManager.getSelectionList(); var addLabelSelectionBox = $('<div></div>'); var lsl = $('<div></div>').text('Label Selection'); var addLabelSelectionInput = new $3Dmol.UI.Form.ListInput(addLabelValue.sel, selectionList); addLabelSelectionBox.append(lsl, addLabelSelectionInput.ui); addLabelForm.append(addLabelSelectionBox); // CSS addLabelForm.css({ 'padding': '2px', }); tick.ui.on('click', () => { var validate = true; if (!addLabelStyleInput.validate()) validate = false; if (!addLabelTextInput.validate()) validate = false; if (!addLabelSelectionInput.validate()) validate = false; if (validate) { stateManager.addLabel(addLabelValue); } }); cross.ui.on('click', () => { stateManager.exitContextMenu(); }); removeButton.ui.on('click', () => { stateManager.removeLabel() }); addLabelForm.on('keyup', (e) => { if (e.key == 'Enter') { tick.ui.trigger('click'); } }); return { boundingBox: addLabelForm, text: addLabelTextInput, style: addLabelStyleInput, selection: addLabelSelectionInput, editMode: function () { removeButton.ui.show(); } } } function processPropertyList() { var propsForLabel = {}; propertyObjectList.forEach((propObj) => { if (propObj.control.value === true) { propsForLabel[propObj.key] = propObj.value; } }); if (Object.keys(propsForLabel).length != 0) { return propsForLabel } else { return null; } } // Context Menu UI Funciton boundingBox.hide(); this.hidden = true; this.atom = null; removeLabelMenu.on('click', { atom: this.atom }, function () { stateManager.removeAtomLabel(removeLabelMenu.atom); }); /** * Shows the context menu * * @function ContextMenu#show * * @param {Number} x x coordinate of the mouse * @param {Number} y y coordinate of the mouse in the viewport in pixels * @param {AtomSpec} atom Value of the atoms that is selected * @param {Boolean} atomExist if atom label is previously added it is set true else false */ this.show = function (x, y, atom, atomExist) { if (atomExist) { removeLabelMenu.show(); removeLabelMenu.atom = atom; } else { removeLabelMenu.hide(); removeLabelMenu.atom = null; } alertBox.ui.hide(); addLabelMenu.hide(); if (stateManager.getSelectionList().length == 0) { alertBox.message('Please create selections before adding label'); } else { addLabelMenu.show(); } unsetForm(); setPosition(boundingBox, x, y); boundingBox.show(); this.hidden = false; if (atom) { setProperties(atom); this.atom = atom; } else { propertyMenu.empty(); } } /** * Hides the context menu and if needed process the propertyMenu * * @function ContextMenu#hide * @param {Boolean} processContextMenu If true then submission of the property to add label is executed */ this.hide = function (processContextMenu) { if (processContextMenu) { var propsForLabel = processPropertyList(); if (propsForLabel != null) { stateManager.addAtomLabel(propsForLabel, this.atom); } } boundingBox.hide(); this.hidden = true; unsetForm(); } addLabelMenu.on('click', function () { var addLabelMenuForm = generateAddLabelForm(); setForm(addLabelMenuForm); }); function setForm(form) { editMenu.children().detach(); editMenu.append(form.boundingBox); editMenu.show(); } function unsetForm() { editMenu.children().detach(); editMenu.hide(); } } /* * Creates UI panel for surface manipulations * * @function SurfaceMenu */ function SurfaceMenu() { var boundingBox = this.ui = $('<div></div>'); var _editingForm = false; // Selection Layout boundingBox.css({ 'position': 'absolute', 'width': '140px', 'text-align': 'right' }); var surfaceButton = new button(icons.surface, 20, { tooltip: 'Toggle Surface Menu' }); boundingBox.append(surfaceButton.ui); var displayBox = $('<div></div>'); boundingBox.append(displayBox); // Overflow fix boundingBox.css({ 'overflow': 'visible', }); var newSurfaceSpace = $('<div></div>'); newSurfaceSpace.css({ 'max-height': HEIGHT * 0.8, 'overflow-y': 'auto', 'overflow-x': 'hidden' }); this.updateScrollBox = function (height) { newSurfaceSpace.css('max-height', height * 0.8); } // newSurfaceSpace.append(controlButton); // controlButton.hide(); displayBox.append(newSurfaceSpace); var alertBox = new AlertBox(); displayBox.append(alertBox.ui); var addArea = $('<div></div>'); var addButton = new button(icons.plus, 20, { tooltip: 'Add New Surface' }); addArea.append(addButton.ui); displayBox.append(addArea); displayBox.hide(); var surfaces = this.surfaces = []; /* * Creates cards for manipulation of surface * * @function Surface */ function Surface() { var control = {