3dmol
Version:
JavaScript/TypeScript molecular visualization library
1,571 lines (1,290 loc) • 66 kB
JavaScript
/**
* $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 = {