3dmol
Version:
Object oriented Javascript molecular visualization library
860 lines (759 loc) • 28.3 kB
JavaScript
//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=$3Dmol.GLModel.validAtomStyleSpecs[parent.type].validItems;
other=false;
}else if(parent.type.toLowerCase() == "surface"){
type = "surface";
validNames = $3Dmol.GLModel.validSurfaceSpecs;
other=true;
}else if(parent.type.toLowerCase() == "labelres"){
type = "labelres";
validNames =$3Dmol.GLModel.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:"✕",
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);
}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 = $3Dmol.GLModel.validColorschemeSpecs;
}else if(type == "color"){
validItemsValue = $3Dmol.GLModel.validColorSpecs;
}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=$3Dmol.GLModel.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:"✕",
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:"✕",
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();
glviewer.translate(width/2,0,0,false);
}
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"){
glviewer.clear();
render(true);
run();
var width = $("#sidenav").width();
glviewer.translate(width/2,0,0,false);
}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])
}
//this adds spinners to things with spinner as a class this is here because they need to ba a part of the dom before this is called
$('<li id = "spacer"><br><br><br></li>').appendTo(parent)
}
//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 == "")
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 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");
$('#addStyle,#addSurface,#addLabelRes,#centerModel,#savePng,#vrmlExport').css("display","inline")
$('#header').css("display","block");
glviewer.translate(width/2,0,400,false);
glviewer.render();
}else{
$("#sidenav").css("width","0");
$('#addStyle,#addSurface,#addLabelRes,#centerModel,#savePng,#header,#vrmlExport').css("display","none")
$("#menu").css("display","inline");
width = $("#sidenav").width();
glviewer.translate(-width/2,0,400,false);
glviewer.render();
}
toggle = !toggle;
}