Files
3Dmol.js/viewer.js
David Koes 44ecf5f4de https
2023-08-12 19:38:48 -04:00

1305 lines
48 KiB
JavaScript

// 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
};
var validLabelResSpecs = {
"font":{type:"string",gui:true},
"fontSize":{type:"number", floatType : true,gui:true,step:1,default:12,min:1},
"fontColor":{type:"color",gui:true},
"fontOpacity":{type:"number", floatType : true,gui:true,step:0.1,default:1,min:0,max:1},
"borderThickness":{type:"number", floatType : true,gui:true,step:0.1,default:1,min:0},
"borderColor":{type:"color",gui:true},
"borderOpacity":{type:"number", floatType : true,gui:true,step:0.1,default:1,min:0,max:1},
"backgroundColor":{type:"color",gui:true},
"backgroundOpacity":{type:"number", floatType : true,gui:true,step:0.1,default:1,min:0,max:1},
"position":{type:"array",valid:false},
"inFront":{type:"boolean",gui:true},
"showBackground":{type:"boolean",gui:true},
"fixed":{type:"boolean",gui:true},
"alignment":{validItems:["topLeft","topCenter","topRight","centerLeft","center","centerRight","bottomLeft","bottomCenter","bottomRight"],gui:true},
"scale":{type:"boolean",gui:false},
};
//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 }
};
//removes style,labelres, and surface from a copy of the selection object and returns it
var augmentSelection = function (selection) {
var copiedObject = jQuery.extend(true, {}, selection);// deep copy
if (copiedObject.style != undefined) {
delete copiedObject.style;
} if (copiedObject.labelres != undefined) {
delete copiedObject.labelres;
} if (copiedObject.surface != undefined) {
delete copiedObject.surface;
}
return copiedObject;
}
// removes eveyrhting but style,surface, and labelres
var removeAllButValid = function (object) {
var copy = jQuery.extend(true, {}, object);
for (var i in object) {
if (i != "surface" || i != "labelres" || i != "style")
delete copy[i]
}
return copy;
}
Object.size = function (obj) {
var size = 0, key;
for (key in obj) {
if (obj.hasOwnProperty(key)) size++;
}
return size;
};
var createAttribute = function (name, value, parent) {
var attribute = $('<li/>', {
class: 'attribute'
});
var other = false;
var validNames;
var type;
if (parent.type == "line" || parent.type == "stick" || parent.type == "cross" || parent.type == "sphere" || parent.type == "cartoon") {
type = "style";
validNames = validAtomStyleSpecs[parent.type].validItems;
other = false;
} else if (parent.type.toLowerCase() == "surface") {
type = "surface";
validNames = validSurfaceSpecs;
other = true;
} else if (parent.type.toLowerCase() == "labelres") {
type = "labelres";
validNames = validLabelResSpecs;
other = true;
} else if (name != "") {
// undefined name
return undefined;
}
if (validNames[name] == undefined && name != "")
return undefined
var attribute_name = $('<select>', {
class: 'attribute_name',
}).appendTo(attribute);
var obj_type = type;
$.each(validNames, function (key, value) {
if (value.gui) {
attribute_name.append($("<option>").attr('value', key).text(key));
}
});
attribute_name.val(name.toString())
if (name.toString() == "") {
var list;
if (type == "style")
list = query.selections[parent.index][type][parent.type];
else
list = query.selections[parent.index][type];
var index;
for (var i in validNames) {
if (validNames[i].gui && list[i] == undefined) {
index = i;
break;
}
}
name = index;
if (name == undefined)
return;// all of the attribute names are being used
attribute_name.val(index)
}
// delete button
var delete_selection = $("<span/>", {
html: "&#x2715;",
class: "delete_attribute",
"data-index": parent.index,
"data-attr": name,
"data-type": parent.type.toLowerCase(),
"click": function () {
if (other)
deleteOtherAttribute(this);
else
deleteStyleAttribute(this);
}
}).appendTo(attribute);
var itemIsDescrete = function (key) {
if (key == "")
return false;
var type = validNames[key].type;
return type == "boolean" || type == "color" || type == "colorscheme" || validNames[key].validItems != undefined
}
var attribute_value;
if (itemIsDescrete(name)) {
var validItemsValue;
if (validNames[name].type != undefined)
var type = validNames[name].type.toLowerCase();
else
var type = undefined
if (type == "boolean") {
validItemsValue = ["false", "true"];
} else if (type == "colorscheme") {
validItemsValue = Object.keys($3Dmol.builtinColorSchemes).concat(['greenCarbon', 'cyanCarbon', 'yellowCarbon', 'whiteCarbon', 'magentaCarbon']);
} else if (type == "color") {
validItemsValue = Object.keys($3Dmol.htmlColors);
if (parent.type == 'cartoon') validItemsValue.unshift('spectrum');
} else if (type == undefined) {
validItemsValue = validNames[name].validItems;
}
var attribute_value = $('<select/>', {
class: 'attribute_value',
}).appendTo(attribute);
$.each(validItemsValue, function (key, value) {
attribute_value.append($("<option>").attr('value', value).text(value));
});
attribute_value.val(value.toString());
if (value == "") {
attribute_value.val(validItemsValue[0])
}
} else {
if (value == "")
value = validNames[name].default
attribute_value = $('<input/>', {
class: 'attribute_value',
value: value,
}).appendTo(attribute);
}
attribute_name.change(function () {
var validItemsValue;
var type = validNames[attribute_name.val()].type
if (type == "boolean") {
validItemsValue = ["false", "true"];
} else if (type == "colorscheme") {
validItemsValue = undefined;
} else if (type == "color") {
validItemsValue = undefined;
} else if (type == undefined) {
validItemsValue = validNames[name].validItems;
}
var defa = validNames[attribute_name.val()].default;
var val;
if (validItemsValue != undefined) {
val = validItemsValue[0];
} else {
val = defa
}
if (attribute_value.children()[0] != undefined)
attribute_value.children()[0].value = val;
else
attribute_value.val(val);
render(obj_type == "surface");
});
attribute_value.change(function () {
render(obj_type == "surface");
});
if (name != "" && attribute_value.prop("tagName") == "INPUT" && validNames[name].type == "number") {
validNames[name].type == "number"
attribute_value.attr("type", "number")
attribute_value.attr("step", validNames[name].step)
attribute_value.addClass("spinner")
var max = validNames[name].max;
var min = validNames[name].min;
if (max != undefined)
attribute_value.attr("max", max);
if (min != undefined)
attribute_value.attr("min", min);
}
return attribute;
}
var createOtherModelSpec = function (spec, type, selection_index) {
var attributes = $('<ul/>', {
"class": type.toLowerCase() + '_attributes',
});
for (var attribute_index in spec) {
var attribute = createAttribute(attribute_index, spec[attribute_index], { type: type, index: selection_index })
if (attribute != undefined)
attribute.appendTo(attributes);
}
var add_attribute = $('<button/>', {
"class": "add_attribute",
"text": "Add Attribute",
"data-index": selection_index,
"data-type": type,
"click": function () { addOtherAttribute(this) },
}).appendTo(attributes);
return attributes;
}
var createStyleSpec = function (style_spec_object, style_spec_type, model_spec_type, selection_index) {
var style_spec = $('<li/>', {
"class": "style_spec",
});
var validNames = validAtomStyleSpecs;
var style_spec_name = $('<select>', {
class: 'style_spec_name',
}).appendTo(style_spec);
style_spec_name.change(function () {
var obj = query.selections[selection_index]["style"][style_spec_type];
for (var i in obj) {
if (!validNames[style_spec_name.val()].validItems.hasOwnProperty(i)) {
delete query.selections[selection_index]["style"][style_spec_type][i];
}
}
query.selections[selection_index]["style"][style_spec_name.val()] = query.selections[selection_index]["style"][style_spec_type];
delete query.selections[selection_index]["style"][style_spec_type];
buildHTMLTree(query)
render();
});
$.each(validNames, function (key, value) {
if (value.gui) {
style_spec_name.append($("<option>").attr('value', key).text(key));
}
});
style_spec_name.val(style_spec_type.toString())
if (style_spec_type == "") {
var list = query.selections[selection_index].style;
var index = 0
for (var i in validNames) {
if (validNames[i].gui && list[i] == undefined) {
index = i;
break;
}
}
if (index == 0)
return;
style_spec_name.val(index)
}
var delete_selection = $("<span/>", {
html: "&#x2715;",
class: "delete_style_spec",
"data-index": selection_index,
"data-type": model_spec_type,
"data-attr": style_spec_type,
"click": function () { deleteStyleSpec(this) },
}).appendTo(style_spec);
var style_spec_attributes = $('<ul/>', {
class: 'style_spec_attributes',
}).appendTo(style_spec);
for (var attribute_index in style_spec_object) {
var attribute = createAttribute(attribute_index, style_spec_object[attribute_index], { type: style_spec_type, index: selection_index })
if (attribute != undefined)
attribute.appendTo(style_spec_attributes);
}
var add_attribute = $('<button/>', {
"class": "add_attribute",
"text": "Add Attribute",
"data-index": selection_index,
"data-type": model_spec_type,
"data-styletype": style_spec_type,
"click": function () { addAttribute(this) },
}).appendTo(style_spec);
return style_spec;
}
var createStyle = function (model_spec_object, model_spec_type, selection_index) {
var style = $('<span/>', {
"class": "style",
});
var style_specs = $('<ul/>', {
"class": 'style_specs',
}).appendTo(style);
for (var attribute_index in model_spec_object) {
var spec = createStyleSpec(model_spec_object[attribute_index], attribute_index, model_spec_type, selection_index)
if (spec != undefined)
spec.appendTo(style_specs);
}
var add_style_spec = $('<button/>', {
"class": "add_style_spec",
"text": "Add Style Spec",
"data-index": selection_index,
"data-type": model_spec_type,
"click": function () { addStyleSpec(this) },
}).appendTo(style);
return style;
}
var validNames = {
"style": "Style",
"surface": "Surface",
"labelres": "LabelRes",
}
var createModelSpecification = function (model_spec_type, model_spec_object, selection_index) {
var model_specification = null;
if (model_spec_type == "style") {
model_specification = createStyle(model_spec_object, model_spec_type, selection_index)
} else if (model_spec_type == "surface") {
model_specification = createOtherModelSpec(model_spec_object, "Surface", selection_index)
} else if (model_spec_type == "labelres") {
model_specification = createOtherModelSpec(model_spec_object, "LabelRes", selection_index)
}
return model_specification;
}
// this function creates the selection object
var createSelection = function (spec, object, index, type) {
// creates container
var selection = $("<li/>", {
class: "selection"
});
var createHeader = function () {
var selection_type = $('<p>', {
class: 'selection_type',
text: validNames[type],
}).appendTo(selection);
// add together sub selections
var attribute_pairs = [];
for (var subselection in spec) {
var obj = spec[subselection];
if (typeof (obj) === 'object' && Object.keys(obj).length === 0)
obj = ""; // empty object
attribute_pairs.push(subselection + ":" + obj);
}
var modifier = attribute_pairs.join(";");
if (modifier == "")
modifier = "all"
var selection_spec = $('<input/>', {
class: 'selection_spec',
value: modifier,
}).appendTo(selection);
selection_spec.change(function () {
render(type == "surface");
})
}
// delete button
var delete_selection = $("<div/>", {
html: "&#x2715;",
class: "delete_selection",
"data-index": index,
"data-type": "",
"click": function () { deleteSelection(this); }
}).appendTo(selection);
createHeader()
// check if style exists and if so create the object
var ret = createModelSpecification(type, object, index);
delete_selection.attr("data-type", type);
ret.appendTo(selection);
return selection;
}
/*
* builds an html tree that goes inside of the selection portion of the viewer
* page
*/
var buildHTMLTree = function (query) {
// get parent object for the html tree
var parent = $('#selection_list');
parent.text("");
// list file type and path
// $("#model_type").attr("value",query.file.type);
document.getElementById("model_type").value = query.file.type
$("#model_type").change(function () {
var val = $("#model_type").val().toUpperCase();
if (prev_type != val) {
render(true);
run();
}
prev_type = val
})
$("#model_input").attr("value", query.file.path);
$("#model_input").change(function () {
var val = $("#model_input").val().toUpperCase();
if (prev_in != val) {
if (val.match(/^[1-9][A-Za-z0-9]{3}$/) || $("#model_type").val().toLowerCase() != "pdb") {
render(true);
run();
var width = $("#sidenav").width();
} else {
if (prev_in != val)
alert("Invalid PDB")
}
}
prev_in = val;
})
var arr = []
// loops through selections and creates a selection tree
for (var selection_index in query.selections) {
var selection_object = query.selections[selection_index];
var aug = augmentSelection(selection_object);
if (selection_object.style != undefined) {
arr.push(createSelection(aug, selection_object.style, selection_index, "style"));
}
if (selection_object.surface != undefined) {
arr.push(createSelection(aug, selection_object.surface, selection_index, "surface"))
}
if (selection_object.labelres != undefined) {
arr.push(createSelection(aug, selection_object.labelres, selection_index, "labelres"))
}
}
for (var i in arr) {
if (arr[i] != undefined)
parent.append(arr[i])
}
}
// takes the queyr object and creates a url for it
var queryToURL = function (query) {
var isSame = function (obj1, obj2) {
for (var key in obj1) {
if (Array.isArray(obj1[key])) {
if (Array.isArray(obj2[key]))
return arraysEqual(obj1[key], obj2[key])
return false;
}
if (obj2[key] == undefined || obj2[key] != obj1[key])
return false;
}
return typeof (obj1) == typeof (obj2); // {} != 0
}
var url = "";
// unpacks everything except for style which has multiple layers
var unpackOther = function (object) {
var objs = []
$.each(object, function (key, value) {
if (isSame(value, {}))
value = ""
// array values
if (Array.isArray(value)) {
// sperate by commas
objs.push(key + ":" + value.join(","));
} else {
objs.push(key + ":" + value);
}
});
return objs.join(";");
}
var unpackStyle = function (object) {
var subStyles = []
$.each(object, function (sub_style, sub_style_object) {
var string = "";
string += sub_style;
if (Object.size(sub_style_object) != 0)
string += ":";
var assignments = []
$.each(sub_style_object, function (key, value) {
assignments.push(key + "~" + value);
});
string += assignments.join(",");
subStyles.push(string)
});
return subStyles.join(";");
}
var unpackSelection = function (object) {
var copiedObject = jQuery.extend(true, {}, object)
var objs = [];
var string = "";
for (var obj in object) {
if (obj == "style") {
objs.push("style=" + unpackStyle(object.style))
} else if (obj == "labelres" || obj == "surface") {
objs.push(obj + "=" + unpackOther(object[obj]))
}
}
var unpacked = unpackOther(augmentSelection(object));
var select = "select=" + unpacked
if (select == "select=")
select = "select=all"
objs.unshift(select);// prepend
return objs.join("&");
}
var objects = [];
var str = query.file.type + "=" + query.file.path;
if (query.file.helper != "")
str += "&type=" + query.file.helper
objects.push(str);
for (var selection in query.selections) {
objects.push(unpackSelection(query.selections[selection]))
}
return objects.join("&");
}
function File(path, type) {
this.path = path;
this.type = type;
this.helper = "";
}
var Query = function () {
this.selections = [];
this.file = new File();
}
function setURL(urlPath) {
window.history.pushState('page2', "Title", "viewer.html?" + urlPath);
}
// this function will look through the dictionaries defined in glmodel and
// validate if the types are correct and return a dictionary with flags for the
// types that are incorecct
var count = 0;
// takes the search url string and makes a query object for it
var urlToQuery = function (url) {
// url= decodeURIComponent(url)
if (url == "" || url.startsWith('session=') || url.startsWith('SESSION='))
return new Query();
var query = new Query();
var tokens = url.split("&");
// still using indexOf because otherwise i would need to check to see if the
// first substring in the string is "select" and check to see if the string
// isnt to small
function stringType(string) {
if (string == "select")
return "select"
else if (string == "pdb" || string == "cid" || string == "url")
return "file"
else if (string == "style" || string == "surface" || string == "labelres") {
count++;
return string;
} else if (string == "type") {
return string
}
throw "Illegal url string : " + string;
return;
}
var currentSelection = null;
for (var token in tokens) {
var uri = decodeURIComponent(tokens[token]);
var i = uri.indexOf('=');
var left = uri.slice(0, i);
var type = stringType(left);// left side of first equals
var string = uri.slice(i + 1);// right side of equals
var object = $3Dmol.specStringToObject(string);
if (type == "file") {
query.file = new File(string, left);
} else if (type == "select") {
currentSelection = object
query.selections.push(currentSelection);
} else if (type == "style" || type == "surface" || type == "labelres") {
if (currentSelection == null) {
currentSelection = {}
query.selections.push(currentSelection)
}
currentSelection[type] = object;
} else if (type == type) {
query.file.helper = string;
}
}
if (query.selections[0] === {})
delete query.selections[0]
return query;
}
var updateQueryFromHTML = function () {
// File/PDB/URL updating
query.file.path = $("#model_input").val();
query.file.type = $("#model_type").val();
var updateOther = function (other) {
var object = {};
var otherList = $(other).children(".attribute");
otherList.each(function (li) {
object[$(otherList[li]).children(".attribute_name")[0].value] = $(otherList[li]).children(".attribute_value")[0].value
});
return object;
}
var updateStyle = function (styl) {
var object = {};
var list = $(styl).children(".style_specs");
list = $(list).children(".style_spec")
list.each(function (li) {
var subtype = $(list[li]).children(".style_spec_name")[0].value;
object[subtype] = {};
var otherList = $(list[li]).children(".style_spec_attributes")[0];
otherList = $(otherList).children(".attribute")
otherList.each(function (li) {
var tag = object[subtype][$(otherList[li]).children(".attribute_name")[0].value] = $(otherList[li]).children(".attribute_value")[0].tagName
object[subtype][$(otherList[li]).children(".attribute_name")[0].value] = $(otherList[li]).children(".attribute_value")[0].value;
});
});
return object;
}
var updateSelectionElements = function (selection_string) {
return $3Dmol.specStringToObject(selection_string);
}
function arraysEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length != b.length) return false;
for (var i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
}
return true;
}
var isSame = function (obj1, obj2) {
for (var key in obj1) {
if (Array.isArray(obj1[key])) {
if (Array.isArray(obj2[key]))
return arraysEqual(obj1[key], obj2[key])
return false;
}
if (obj2[key] == undefined || obj2[key] != obj1[key])
return false;
}
return typeof (obj1) == typeof (obj2); // 0 != {}
}
function combine(obj1, src1) {
for (var key in src1) {
if (src1.hasOwnProperty(key)) obj1[key] = src1[key];
}
return obj1;
}
var selects = [];
var listItems = $(".selection")
listItems.each(function (index, value) {
if (listItems.hasOwnProperty(index) && listItems[index].id != "spacer") {
var getSubObject = function () {
var attr = $(value);
var attribute = attr[0]
var type = $(attribute).children()[1].innerHTML.toLowerCase()
if (type == "style") {
var style = updateStyle($(attribute).children(".style")[0])
return { "style": style }
} else if (type == "surface") {
var surface = updateOther($(attribute).children(".surface_attributes")[0])
return { "surface": surface }
} else if (type == "labelres") {
var labelres = updateOther($(attribute).children(".labelres_attributes")[0])
return { "labelres": labelres }
}
}
var val = getSubObject();
var selection_spec = $(listItems[index]).children(".selection_spec")[0].value;
var selection = updateSelectionElements(selection_spec);
var extended = combine(selection, val)
selects.push(extended)
}
});
query.selections = selects;
}
var query = urlToQuery(window.location.search.substring(1));
// this function compresses the html object back into a url
var render = function (surfaceEdited) {
surfaceEdited = surfaceEdited == undefined ? false : surfaceEdited;
// calls update query
updateQueryFromHTML();
var url = queryToURL(query);
setURL(url);
buildHTMLTree(query);
glviewer.setStyle({}, { line: {} });
runcmds(url.split("&"), glviewer, surfaceEdited);
glviewer.render();
}
// these functions all edit the query object
var addSelection = function (type) {
var surface = type == "surface"
if (type == "style")
query.selections.push({ "style": { line: {} } })
else if (type == "surface")
query.selections.push({ "surface": {} })
else if (type == "labelres")
query.selections.push({ "labelres": {} })
buildHTMLTree(query);
render(surface);
}
var deleteSelection = function (spec) {
delete query.selections[spec.dataset.index][spec.dataset.type];
if (query.selections[spec.dataset.index].surface == undefined && query.selections[spec.dataset.index].style == undefined && query.selections[spec.dataset.index].labelres == undefined)
delete query.selections[spec.dataset.index]
buildHTMLTree(query);
render(spec.dataset.type == "surface");
}
var addModelSpec = function (type, selection) {
var current_selection;
current_selection = query.selections[selection.dataset.index]
if (type == "style" || type == "surface" || type == "labelres") {
if (current_selection[type] == null)
current_selection[type] = {};
else
console.err(type + " already defined for selection");// TODO error
// handling
}
buildHTMLTree(query);
render();
}
var addStyleSpec = function (model_spec) {
var defaultKey = "";
var defaultValue = {};
query.selections[model_spec.dataset.index][model_spec.dataset.type][defaultKey] = defaultValue;
buildHTMLTree(query);
render();
}
var deleteStyleSpec = function (spec) {
delete query.selections[spec.dataset.index][spec.dataset.type][spec.dataset.attr]
buildHTMLTree(query);
render();
}
var addOtherAttribute = function (spec) {
var defaultKey = "";
var defaultValue = "";
query.selections[spec.dataset.index][spec.dataset.type.toLowerCase()][defaultKey] = defaultValue;
buildHTMLTree(query);
render();
}
var deleteOtherAttribute = function (spec) {
delete query.selections[spec.dataset.index][spec.dataset.type][spec.dataset.attr]
buildHTMLTree(query);
render(spec.dataset.type == "surface");
}
var addAttribute = function (style_spec) {
var defaultKey = "";
var defaultValue = "";
query.selections[style_spec.dataset.index][style_spec.dataset.type][style_spec.dataset.styletype][defaultKey] = defaultValue;
buildHTMLTree(query);
render();
}
var deleteStyleAttribute = function (spec) {
delete query.selections[spec.dataset.index]["style"][spec.dataset.type][spec.dataset.attr]
buildHTMLTree(query);
render();
}
// this function reads the form changes and upates the query accordingly
var center = function () {
glviewer.center({}, 1000, true);
}
var vrml = function () {
var filename = "3dmol.wrl";
var text = glviewer.exportVRML();
var blob = new Blob([text], { type: "text/plain;charset=utf-8" });
saveAs(blob, filename);
}
var savePng = function () {
var filename = "3dmol.png";
var text = glviewer.pngURI();
var ImgData = text;
var link = document.createElement('a');
link.href = ImgData;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// initializes the sidebar based on the given url
var initSide = function (url) {
var list = document.createElement('ul')
document.getElementById('container').appendChild(list);
// updating on back button
$(window).on('popstate', function () {
query = urlToQuery(window.location.search.substring(1));
buildHTMLTree(query);
render(true);
});
buildHTMLTree(query);
}
var showCreateSession = function () {
$('#session_list2').hide();
$('#session_list1').toggle();
}
var showJoinSession = function () {
$('#session_list1').hide();
$('#session_list2').toggle();
}
var toggle = true;
var width = 420;
var prev_in = $("#model_input").val();
var prev_type = $("#model_type").val();
var toggleHide = function () {
if (toggle) {
$("#menu").css("display", "none");
$("#sidenav").css("width", width + "px");
$('#createSession,#joinSession,#addStyle,#addSurface,#addLabelRes,#centerModel,#savePng,#vrmlExport').css("display", "inline")
glviewer.translate(width / 2, 0, 400, false);
} else {
$("#sidenav").css("width", "0");
$('#createSession,#joinSession,#addStyle,#addSurface,#addLabelRes,#centerModel,#savePng,#header,#vrmlExport').css("display", "none")
$("#menu").css("display", "inline");
width = $("#sidenav").width();
glviewer.translate(-width / 2, 0, 400, false);
}
toggle = !toggle;
}
var glviewer = null;
// http://localhost/$3Dmol/viewer.html?pdb=1ycr&style=cartoon&addstyle=line&select=chain~A&colorbyelement=whiteCarbon&style=surface,opacity~.8&select=chain~B&addstyle=stick&select=chain~B,resn~TRP&style=sphere
// Process commands, in order, and run on viewer (array of strings split on '&')
var runcmds = function (cmds, viewer, renderSurface) {
//console.log("rendering")
renderSurface = renderSurface == undefined ? true : renderSurface;
if (renderSurface)
viewer.removeAllSurfaces();
viewer.removeAllLabels();
var currentsel = {};
for (var i = 0; i < cmds.length; i++) {
var kv = cmds[i].split('=');
var cmdname = kv[0];
var cmdobj = $3Dmol.specStringToObject(kv[1]);
if (cmdname == 'select')
currentsel = cmdobj;
else if (cmdname == 'surface' && renderSurface) {
viewer.addSurface($3Dmol.SurfaceType.VDW, cmdobj, currentsel,
currentsel);
} else if (cmdname == 'style') {
viewer.setStyle(currentsel, cmdobj);
} else if (cmdname == 'addstyle') {
viewer.addStyle(currentsel, cmdobj);
} else if (cmdname == 'labelres') {
viewer.addResLabels(currentsel, cmdobj);
} else if (cmdname == 'colorbyelement') {
if (typeof ($3Dmol.elementColors[cmdobj.colorscheme]) != "undefined")
viewer.setColorByElement(currentsel,
$3Dmol.elementColors[cmdobj.colorscheme]);
} else if (cmdname == 'colorbyproperty') {
if (typeof (cmdobj.prop) != "undefined"
&& typeof ($3Dmol.Gradient[cmdobj.scheme]) != "undefined") {
viewer.setColorByProperty(currentsel, cmdobj.prop,
new $3Dmol.Gradient[cmdobj.scheme]());
}
}
}
let getlabel = function(atom) {
let label = atom.elem;
if(atom.resn && atom.resi) {
label = atom.resn+atom.resi+":"+atom.atom
}
return label;
}
//hover labels
var hover_label = null;
viewer.setHoverable({},true,
function(atom){ //hover
if(atom._clicklabel == undefined) {
label = getlabel(atom);
hover_label = viewer.addLabel(label,
{position: atom, fontSize: 12, backgroundColor: "black", backgroundOpacity: 0.5, alignment: "bottomCenter"});
viewer.render();
}
},
function(){
if(hover_label) {
viewer.removeLabel(hover_label);
viewer.render();
}
} //unhover
);
var lastclicked = null;
viewer.setClickable({}, true, function(atom) {
if(hover_label) {
viewer.removeLabel(hover_label);
}
if(lastclicked == null) {
let label = getlabel(atom);
atom._clicklabel =
viewer.addLabel(label,
{position: atom, fontSize: 12, backgroundColor: "blue", backgroundOpacity: 0.75, alignment: "bottomCenter"});
lastclicked = atom;
} else {
viewer.removeLabel(lastclicked._clicklabel);
lastclicked._clicklabel = null;
if(lastclicked != atom) {
let start = new $3Dmol.Vector3(lastclicked.x, lastclicked.y, lastclicked.z);
let end = new $3Dmol.Vector3(atom.x, atom.y, atom.z);
let dlabel = null;
viewer.addCylinder({
dashed:true,
radius:.05,
dashLength:0.25,
gapLength:.15,
start:start,
end:end,
fromCap:2,
toCap:2,
color:"blue",
clickable: true,
callback: function(shape) {
viewer.removeShape(shape);
viewer.removeLabel(dlabel);
}
});
let dist = $3Dmol.GLShape.distance_from(start,end);
let mid = start.add(end).multiplyScalar(.5);
dlabel = viewer.addLabel(dist.toFixed(3),{position: mid, fontSize: 12, backgroundColor: "blue",
backgroundOpacity: 0.75, alignment: "bottomCenter"});
}
lastclicked = null;
}
viewer.render();
});
};
function run() {
try {
var url = window.location.search.substring(1);
url = decodeURIComponent(url)
var cmds = url.split("&");
var first = cmds.splice(0, 1)[0];
var pos = first.indexOf('=');
var src = first.substring(0, pos), data = first
.substring(pos + 1);
var type = "pdb";
if (glviewer === null) {
glviewer = $3Dmol.createViewer("gldiv", {
defaultcolors: $3Dmol.rasmolElementColors
});
glviewer.setBackgroundColor(0xffffff);
} else {
glviewer.clear();
}
if (src == 'session' || src == 'SESSION') {
// join a session
joinSession(data);
}
if (src == 'pdb') {
console.log(data)
data = data.toUpperCase();
if (!data.match(/^[1-9][A-Za-z0-9]{3}$/)) {
return;
}
data = "https://files.rcsb.org/view/" + data
+ ".pdb";
type = "pdb";
} if (src == 'cif') {
data = data.toUpperCase();
if (!data.match(/^[1-9][A-Za-z0-9]{3}$/)) {
return;
}
data = "https://files.rcsb.org/view/" + data
+ ".cif";
type = "cif";
} else if (src == 'cid') {
type = "sdf";
data = "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/"
+ data + "/SDF?record_type=3d";
} else if (src == 'mmtf') {
data = data.toUpperCase();
data = 'https://mmtf.rcsb.org/full/' + data ;
type = 'mmtf';
} else { // url
// try to extract extension
type = data.substring(data.lastIndexOf('.') + 1);
if (type == 'gz') {
var base = data.substring(0, data.lastIndexOf('.'));
type = base.substring(base.lastIndexOf('.')) + '.gz';
}
}
if (cmds[0] && cmds[0].indexOf('type=') == 0) {
type = cmds[0].split('=')[1];
}
var start = new Date();
if (/\.gz$/.test(data) || type == 'mmtf') { // binary
// data
$.ajax({
url: data,
type: "GET",
dataType: "binary",
responseType: "arraybuffer",
processData: false,
success: function (ret, txt, response) {
console.log("mtf fetch " + (+new Date() - start) + "ms");
var time = new Date();
glviewer.addModel(ret, type);
runcmds(cmds, glviewer);
glviewer.zoomTo();
glviewer.render();
console.log("mtf load " + (+new Date() - time) + "ms");
}
}).fail(function () {
// if couldn't get url natively, go through echo
// server
$.ajax({
url: "echo.cgi",
data: { 'url': data },
processData: false,
responseType: "arraybuffer",
dataType: "binary",
success: function (ret, txt, response) {
glviewer.addModel(ret, type);
runcmds(cmds, glviewer);
glviewer.zoomTo();
glviewer.render();
}
})
});
} else {
$.get(data, function (ret, txt, response) {
console.log("alt fetch " + (+new Date() - start) + "ms");
var time = new Date();
glviewer.addModel(ret, type);
runcmds(cmds, glviewer);
glviewer.zoomTo();
glviewer.render();
console.log("alt load " + (+new Date() - time) + "ms");
}).fail(function () {
// if couldn't get url natively, go through echo
// server
$.post("echo.cgi", {
'url': data
}, function (ret, txt, response) {
if (src == 'pdb' && (ret.search("We're sorry, but the requested") >= 0 || ret == "")) {
// really large files aren't available
// in pdb format
type = 'cif';
data = data.replace(/pdb$/, 'cif');
$.post("echo.cgi", {
'url': data
}, function (ret, txt, response) {
glviewer.addModel(ret, type);
runcmds(cmds, glviewer);
glviewer.zoomTo();
glviewer.render();
})
} else {
glviewer.addModel(ret, type);
runcmds(cmds, glviewer);
glviewer.zoomTo();
glviewer.render();
}
});
});
}
}
catch (e) {
console
.error("Could not instantiate viewer from supplied url: '"
+ e + "'");
window.location = "http://get.webgl.org";
}
}
$(document).ready(function () {
var url = window.location.href.substring(window.location.href.indexOf("?") + 1);
initSessions();
run();
var start_width;
$("#sidenav").resizable({
handles: 'e',
minWidth: 300,
maxWidth: 1000,
start: function (event, ui) {
start_width = $("#sidenav").width();
},
resize: function (event, ui) {
glviewer.center();
glviewer.translate(($("#sidenav").width() - start_width) / 2, 0, 0, false);
start_width = $("#sidenav").width();
}
});
$("#selection_list").sortable({
items: ".selection:not(#spacer)",
update: function (event, ui) {
render(true);
},
});// $("#selection_list").accordion();
$("#selection_list").disableSelection();
initSide(url);
});