diff --git a/workbenches/gosling/Gosling.js b/workbenches/gosling/Gosling.js new file mode 100644 index 00000000..3cc378c4 --- /dev/null +++ b/workbenches/gosling/Gosling.js @@ -0,0 +1,164 @@ +//// +//// A little fun driving a view with YAS*. +//// + +// Let jshint pass over over our external globals (browserify takes +// care of it all). +/* global YASQE */ +/* global YASR */ +/* global jQuery */ +/* global global_barista_token */ +/* global global_barista_location */ +/* global global_sparql_templates_universal */ + +var us = require('underscore'); +var bbop = require('bbop-core'); +//var bbop = require('bbop').bbop; +//var bbopx = require('bbopx'); +var amigo_inst = require('amigo2-instance-data'); +var amigo = new amigo_inst(); +var bbop_legacy = require('bbop').bbop; + +// Help with strings and colors--configured separately. +var aid = new bbop_legacy.context(amigo.data.context); + +var widgetry = require('noctua-widgetry'); + +// Aliases +var each = us.each; +var uuid = bbop.uuid; + +// Code here will be ignored by JSHint, as we are technically +// "redefining" jQuery (although we are not). +/* jshint ignore:start */ +var jQuery = require('jquery'); +/* jshint ignore:end */ + +// +var json_response = require('bbop-rest-response'); +var jquery_engine = require('bbop-rest-manager').jquery; +var sparql_manager = require('bbop-manager-sparql'); + +/// +/// ... +/// + +var sparql_select_id = 'spqlsel'; +var yasqe_id = 'yasqe'; +var yasr_id = 'yasr'; + +// Make the default ours at least. +YASQE.defaults.sparql.endpoint = "http://rdf.geneontology.org/sparql"; + +/// +var GoslingInit = function(){ + + var logger = new bbop.logger('gosling'); + logger.DEBUG = true; + function ll(str){ logger.kvetch(str); } + + /// + /// Add option to dropdown. + /// + + // Attach to interface. + var app_map = {}; + var selopts_str = []; + us.each(global_sparql_templates_universal, function(tmpl){ + var uuid = bbop.uuid(); + + // Mental. + app_map[uuid] = tmpl; + + // Physical. + var str = ''; + selopts_str.push(str); + }); + jQuery('#'+sparql_select_id).append(selopts_str.join('')); + + /// + /// Setup and glue YAS*. + /// + + var yasqe = YASQE(document.getElementById("yasqe"), { + sparql: { + showQueryButton: true, + endpoint: 'http://rdf.geneontology.org/sparql' + } + }); + var yasr = YASR(document.getElementById("yasr"), { + //this way, the URLs in the results are prettified using the + //defined prefixes in the query + getUsedPrefixes: yasqe.getPrefixesFromQuery + }); + + //link both together + yasqe.options.sparql.callbacks.complete = yasr.setResponse; + + /// + /// ... + /// + + function _insertion(tmpl){ + + var prefixes = ''; + us.each(tmpl['prefixes'], function(prefix){ + prefixes += + 'PREFIX '+ prefix['prefix'] +':'+ prefix['expansion'] +'\n'; + }); + var qstr = prefixes + tmpl['query']; + + yasqe.setValue(qstr); + if( tmpl['endpoint'] ){ + var ep = tmpl['endpoint']; + //yasqe.endpoint(tmpl['endpoint']); + //YASQE.defaults.sparql.endpoint = ep; + //yasqe.defaults.sparql.endpoint = ep; + } + } + + // Make the interface live. + jQuery("#"+sparql_select_id).change(function(){ + var tmpl_id = jQuery(this).val(); + console.log(tmpl_id); + var tmpl = app_map[tmpl_id]; + console.log(tmpl); + console.log(app_map); + _insertion(tmpl); + }); + + // Insert first item. + var stu = global_sparql_templates_universal; + if( stu && stu[0] ){ + _insertion(stu[0]); + } +}; + +// Start the day the jQuery way. +jQuery(document).ready(function(){ + console.log('jQuery ready'); + + // Try to define token. + var start_token = null; + if( global_barista_token ){ + start_token = global_barista_token; + } + + // Next we need a manager to try and pull in the model. + if( typeof(global_minerva_definition_name) === 'undefined' || + typeof(global_barista_location) === 'undefined' ){ + alert('environment not ready'); + }else{ + // Only roll if the env is correct. + // Will use the above variables internally (sorry). + GoslingInit(); + + // When all is said and done, let's also fillout the user + // name just for niceness. This is also a test of CORS in + // express. + if( start_token ){ + widgetry.user_check(global_barista_location, + start_token, 'user_name_info', false); + } + } +}); diff --git a/workbenches/gosling/README.md b/workbenches/gosling/README.md new file mode 100644 index 00000000..086adc40 --- /dev/null +++ b/workbenches/gosling/README.md @@ -0,0 +1,4 @@ +``` +npm install +npm run build +``` diff --git a/workbenches/gosling/config.yaml b/workbenches/gosling/config.yaml new file mode 100644 index 00000000..a366923d --- /dev/null +++ b/workbenches/gosling/config.yaml @@ -0,0 +1,11 @@ +menu-name: "Gosling (Noctua's little GOOSE)" +page-name: "Gosling (Noctua's little GOOSE)" +type: "universal" +help-link: "http://github.com/geneontology/noctua/issues" +javascript: + - http://cdn.jsdelivr.net/yasqe/2.11.10/yasqe.bundled.min.js + - http://cdn.jsdelivr.net/yasr/2.10.8/yasr.bundled.min.js + - GoslingBundle.js +css: + - http://cdn.jsdelivr.net/yasqe/2.11.10/yasqe.min.css + - http://cdn.jsdelivr.net/yasr/2.10.8/yasr.min.css diff --git a/workbenches/gosling/package.json b/workbenches/gosling/package.json new file mode 100644 index 00000000..a75a37a7 --- /dev/null +++ b/workbenches/gosling/package.json @@ -0,0 +1,91 @@ +{ + "name": "workbench-go-gosling", + "version": "0.0.1", + "license": "BSD-3-Clause", + "description": "Experimental GOOOSE-like Noctua interface for the Gene Ontology.", + "keywords": [ + "workbench", + "gene ontology", + "GO", + "bbop", + "SPARQL", + "GOOSE", + "YASGUI", + "noctua" + ], + "author": "SJC (http://berkeleybop.org/)", + "homepage": "http://berkeleybop.org/", + "repository": { + "type": "git", + "url": "git+https://github.com/geneontology/noctua.git" + }, + "engines": { + "node": ">= 4.4", + "npm": ">= 2.15" + }, + "dependencies": { + "amigo2-instance-data": "2.5.7", + "amigo2": "2.4.3", + "bbop": "2.4.3", + "bbop-core": "0.0.5", + "bbop-graph-noctua": "0.0.32", + "bbop-manager-sparql": "0.0.5", + "bbop-rest-manager": "0.0.17", + "bbop-rest-response": "0.0.4", + "gulp": "^3.9.1", + "jquery": "^2.2.4", + "underscore": "1.8.3" + }, + "devDependencies": { + "browserify": "^13.0.1", + "browserify-shim": "^3.8.12", + "chai": "^3.5.0", + "eslint": "^2.13.1", + "gulp-bump": "^2.1.0", + "gulp-git": "^1.7.2", + "gulp-jsdoc": "^0.1.5", + "gulp-jshint": "^2.0.1", + "gulp-mocha": "^2.2.0", + "gulp-streamify": "^1.0.2", + "gulp-uglify": "^1.5.3", + "jsdoc": "^3.4.0", + "jshint": "^2.9.3", + "uglifyify": "^3.0.2", + "vinyl-source-stream": "1.1.0", + "wait-on": "^1.5.2", + "watchify": "^3.7.0" + }, + "browserify": { + "transform": [ + "browserify-shim" + ] + }, + "browser": { + "jquery": "./node_modules/jquery/dist/jquery.min.js", + "bootstrap": "./node_modules/bootstrap/dist/js/bootstrap.min.js", + "jquery-ui": "./external_js/jquery-ui-1.10.3.custom.min.js", + "noctua-widgetry": "../../js/lib/noctua-widgetry/widgetry.js" + }, + "browserify-shim": { + "jquery": { + "exports": "global:jQuery" + }, + "jquery-ui": { + "depends": [ + "jquery" + ] + } + }, + "bundleDependencies": [], + "private": false, + "main": null, + "bugs": { + "url": "https://github.com/geneontology/noctua/issues" + }, + "directories": { + }, + "scripts": { + "build": "./node_modules/.bin/browserify Gosling.js -o public/GoslingBundle.js --exclude ringo/httpclient", + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/workbenches/gosling/public/GoslingBundle.js b/workbenches/gosling/public/GoslingBundle.js new file mode 100644 index 00000000..20cbabd0 --- /dev/null +++ b/workbenches/gosling/public/GoslingBundle.js @@ -0,0 +1,68780 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;ocomment ' + + // ann.value() + ''; + // } + var okay_annotations = { + 'state' : true, + 'date' : true, + 'title' : true, + 'contributor' : true, + 'comment' : true + }; + if( okay_annotations[ann.key()] && ann.value() ){ + anns += '
' + '' + + ann.key() + ' ' + + ann.value() + '
'; + } + }); + if( anns === '' ){ + anns = '
none
'; + } + + // Try and get a title out of the model. + var mtitle = '??? (title)'; + var tanns = ecore.get_annotations_by_key('title'); + if( tanns && tanns.length === 1 ){ mtitle = tanns[0].value(); } + + var str_cache = [ + '
', + // '
', + // '
', + // '
', + '
ID
', + '
', + ecore.get_id(), + '
', + '
Name
', + '
', + mtitle, + '
', + '
Individuals
', + '
', + nds.length || 0, + '
', + '
Indv. Rels.
', + '
', + eds.length || 0, + '
', + '
Annotations
', + anns + ]; + + // Add to display. + jQuery(info_div).empty(); + jQuery(info_div).append(str_cache.join(' ')); +} + +/* + * Function: repaint_exp_table + * + * Add edit model node contents to a descriptive table. + */ +function repaint_exp_table(ecore, aid, table_div){ + + // First, lets get the headers that we'll need by poking the + // model and getting all of the possible categories. + var cat_list = []; + each(ecore.get_nodes(), function(enode, enode_id){ + each(enode.types(), function(in_type){ + cat_list.push(in_type.category()); + }); + }); + // Dedupe list. + var tmph = bbop_core.hashify(cat_list); + cat_list = us.keys(tmph); + + // If we actually got something, render the table. Otherwise, + // a message. + if( us.isEmpty(cat_list) ){ + + // Add to display. + jQuery(table_div).empty(); + jQuery(table_div).append('

no instances

'); + + }else{ + + // Sort header list according to known priorities. + cat_list = cat_list.sort(function(a, b){ + return aid.priority(b) - aid.priority(a); + }); + + // Convert the ids into readable headers. + var nav_tbl_headers = []; + each(cat_list, function(cat_id){ + var hdrc = [ + aid.readable(cat_id), + '↑↓' + ]; + nav_tbl_headers.push(hdrc.join(' ')); + }); + + var nav_tbl = + new bbop.html.table(nav_tbl_headers, [], + {'generate_id': true, + 'class': ['table', 'table-bordered', + 'table-hover', + 'table-condensed'].join(' ')}); + + //each(ecore.get_nodes(), + each(ecore.edit_node_order(), function(enode_id){ + var enode = ecore.get_node(enode_id); + + // Now that we have an enode, we want to mimic the order + // that we created for the header (cat_list). Start by + // binning the types. + var bin = {}; + each(enode.types(), function(in_type){ + var cat = in_type.category(); + if( ! bin[cat] ){ bin[cat] = []; } + bin[cat].push(in_type); + }); + + // Now unfold the binned types into the table row + // according to the sorted order. + var table_row = []; + each(cat_list, function(cat_id){ + var accumulated_types = bin[cat_id]; + var cell_cache = []; + each(accumulated_types, function(atype){ + var tt = type_to_span(atype, aid); + cell_cache.push(tt); + }); + table_row.push(cell_cache.join('
')); + }); + nav_tbl.add_to(table_row); + }); + + // Add to display. + jQuery(table_div).empty(); + jQuery(table_div).append(nav_tbl.to_string()); + + // Make it sortable using the plugin. + jQuery('#' + nav_tbl.get_id()).tablesorter(); + } +} + +/** + * Add edit model edge contents to a descriptive table. + */ +function repaint_edge_table(ecore, aid, table_div){ + + var edge_list = ecore.all_edges(); + + // If we actually got something, render the table. Otherwise, + // a message. + if( us.isEmpty(edge_list) ){ + + // Add to display. + jQuery(table_div).empty(); + jQuery(table_div).append('

no relations

'); + + }else{ + + // Make the (obvjously known) headers pretty. + var nav_tbl_headers = []; + each(['subject', 'relation', 'object'], function(hdr){ + var hdrc = [ + hdr, + '↑↓' + ]; + nav_tbl_headers.push(hdrc.join(' ')); + }); + + var nav_tbl = + new bbop.html.table(nav_tbl_headers, [], + {'generate_id': true, + 'class': ['table', 'table-bordered', + 'table-hover', + 'table-condensed'].join(' ')}); + + each(edge_list, function(edge){ + var s = edge.source(); + var r = edge.relation(); + var t = edge.target(); + + // according to the sorted order. + var table_row = [ + aid.readable(s), + aid.readable(r), + aid.readable(t) + ]; + + nav_tbl.add_to(table_row); + }); + + // Add to display. + jQuery(table_div).empty(); + jQuery(table_div).append(nav_tbl.to_string()); + + // Make it sortable using the plugin. + jQuery('#' + nav_tbl.get_id()).tablesorter(); + } +} + +/** + * Wipe out the contents of a jQuery-identified div. + */ +function wipe(div){ + jQuery(div).empty(); +} + +/** + * Takes a core edit node types as the argument, categorize the, order + * them. + */ +function enode_types_to_ordered_stack(enode_types, aid){ + + // Sort the types within the stack according to the known + // type priorities. + function _sorter(a, b){ + // Use aid property priority. + var bpri = aid.priority(b.property_id()); + var apri = aid.priority(a.property_id()); + return apri - bpri; + } + + // + var out_stack = enode_types.sort(_sorter); + return out_stack; +} + +/** + * This is a silly little object that represents a node stack. It can + * render the stack as a string (the original non-object purpose of + * this little bit of code) and manager to relay the relation between + * random DOM IDs and underlying edges (the reason it was turned into + * an object). + * + * This change was made to make it possible to allow the evidence to + * be clicked on in the display and the edge annotation dialog (with + * the accompanying evidence) to come up for editing. + * + * This whole bit will change a lot with new evidence coming down the + * pipe. + */ +function node_stack_object(enode, aid){ + + var hook_list = []; + + // Create a colorful label stack into an individual table. + var enode_stack_table = new bbop.html.tag('table', + {'class':'bbop-mme-stack-table'}); + + // General function for adding type information to stack. + function _add_table_row(item, color, prefix, suffix){ + //var rep_color = aid.color(item.category()); + var out_rep = type_to_span(item, color); + if( prefix ){ out_rep = prefix + out_rep; } + if( suffix ){ out_rep = out_rep + suffix; } + var trstr = null; + if( color ){ + trstr = '' + out_rep + ''; + }else{ + trstr = '' + + '' + out_rep + ''; + } + enode_stack_table.add_to(trstr); + } + + // Inferred types first. + var inf_types = enode.get_unique_inferred_types(); + each(inf_types, function(item){ _add_table_row(item, null, '[', ']'); }); + // Editable types next. + var std_types = enode.types(); + each(std_types, function(item){ _add_table_row(item); }); + + // Now we trick our way through to adding the types^H^H^H^H^H + // absorbed subgraph nodes of the subgraphs. + var subgraph = enode.subgraph(); + if( subgraph ){ + + // Gather the stack to display, abstractly do go up or down + // the subgraph. + var _folded_stack_gather = function(direction){ + + // First, get the parent/child sub-nodes. + var x_edges = []; + if( direction === 'standard' ){ + x_edges = subgraph.get_parent_edges(enode.id()); + }else{ + x_edges = subgraph.get_child_edges(enode.id()); + } + // Put an order on the edges. + x_edges.sort(function(e1, e2){ + return aid.priority(e1.relation()) - aid.priority(e2.relation()); + }); + each(x_edges, function(x_edge){ + // Edge info. + var rel = x_edge.relation() || 'n/a'; + var rel_color = aid.color(rel); + var rel_readable = aid.readable(rel); + // If context aid doesn't work, see if it comes with a label. + if( rel_readable === rel && typeof(x_edge.label) === 'function'){ + var label_rn = x_edge.label(); + if( label_rn !== rel ){ + rel = label_rn; // use label + } + }else{ + rel = rel_readable; // use context + } + + // Try and extract proof of evidence. + var ev_edge_anns = x_edge.get_annotations_by_key('evidence'); + // Get node. + var x_ent_id = null; + if( direction === 'standard' ){ + x_ent_id = x_edge.object_id(); + }else{ + x_ent_id = x_edge.subject_id(); + } + var x_node = subgraph.get_node(x_ent_id); + // Try and extract proof of evidence. + if( x_node ){ + var ev_node_anns = x_node.get_annotations_by_key('evidence'); + + // Add the edge/node combos to the table. + each(x_node.types(), function(x_type){ + + // + var elt_id = bbop_core.uuid(); + var edge_id = x_edge.id(); + hook_list.push([edge_id, elt_id]); + if( ev_edge_anns.length > 0 ){ + // In this case (which should be the only possible + // case), we'll capture the ID and pair it with an + // ID. + _add_table_row(x_type, rel_color, rel + '(', + ')E'); + }else{ + _add_table_row(x_type, rel_color, rel + '(', + ') '); + } + }); + } + }); + }; + + // Do it both ways--upstream and downstream. + _folded_stack_gather('standard'); + _folded_stack_gather('reverse'); + + } + + // Inject meta-information if extant. + var anns = enode.annotations(); + if( anns.length !== 0 ){ + + // Meta counts. + var n_ev = 0; + var n_other = 0; + each(anns, function(ann){ + if( ann.key() === 'evidence' ){ + n_ev++; + }else{ + if( ann.key() !== 'hint-layout-x' && + ann.key() !== 'hint-layout-y' ){ + n_other++; + } + } + }); + + // Add to top. No longer need evidence count on individuals. + var trstr = '' + + '' + + //'evidence: ' + n_ev + '; other: ' + n_other + + 'annotations: ' + n_other + + ''; + enode_stack_table.add_to(trstr); + } + + // Add external visual cue if there were inferred types. + if( inf_types.length > 0 ){ + var itcstr = '' + + '' + + 'inferred types: ' + inf_types.length + ''; + enode_stack_table.add_to(itcstr); + } + + // return enode_stack_table; + this.to_string = function(){ + return enode_stack_table.to_string(); + }; + + // + this.hooks = function(){ + return hook_list; + }; +} + +/** + * Add a new enode, need a lot of extra junk to pass on to make + * annotation editor work, by plugging into the node stack creation + * object. + */ +function add_enode(annotation_config, ecore, manager, enode, aid, graph_div, left, top, gserv, gconf){ + + // See whether or not we need to place the nodes with style. + var style_str = ''; + if( left !== null && top !== null ){ + style_str = 'top: ' + top + 'px; ' + 'left: ' + left + 'px;'; + } + //ll('style: ' + style_str); + + // Node as table nested into bbop.html div. + var div_id = ecore.get_node_elt_id(enode.id()); + var w = new bbop.html.tag('div', + {'id': div_id, + 'class': 'demo-window', + 'style': style_str}); + + var enode_stack_table = new node_stack_object(enode, aid); + w.add_to(enode_stack_table.to_string()); + + // Box to drag new connections from. + var konn = new bbop.html.tag('div', {'class': 'konn'}); + w.add_to(konn); + + // Box to click for edit dialog. + var opend = new bbop.html.tag('button', + {'class': 'open-dialog btn btn-default', + 'title': 'Open edit annoton dialog'}); + w.add_to(opend); + + // Box to open annotation dialog. + var openann = new bbop.html.tag('button', + {'class': + 'open-annotation-dialog btn btn-default', + 'title': 'Open annotation dialog'}); + w.add_to(openann); + + // Add to display. + jQuery(graph_div).append(w.to_string()); + + // + each(enode_stack_table.hooks(), function(hook_pair){ + var edge_id = hook_pair[0]; + var element_id = hook_pair[1]; + jQuery('#'+element_id).click(function(evt){ + evt.stopPropagation(); + + var eam = edit_annotations_modal(annotation_config, ecore, manager, + edge_id, gserv, gconf); + eam.show(); + }); + }); +} + +/** + * Update the displayed contents of an enode. + */ +function update_enode(ecore, enode, aid){ + + // Node as table nested into bbop.html div. + var uelt = ecore.get_node_elt_id(enode.id()); + jQuery('#' + uelt).empty(); + + var enode_stack_table = new node_stack_object(enode, aid); + jQuery('#' + uelt).append(enode_stack_table.to_string()); + + // Box to drag new connections from. + var konn = new bbop.html.tag('div', {'class': 'konn'}); + jQuery('#' + uelt).append(konn.to_string()); + + // Box to open the edit dialog. + var opend = new bbop.html.tag('button', + {'class': 'open-dialog btn btn-default', + 'title': 'Open edit annoton dialog'}); + jQuery('#' + uelt).append(opend.to_string()); + + // Box to open annotation dialog. + var openann = new bbop.html.tag('button', + {'class': + 'open-annotation-dialog btn btn-default', + 'title': 'Open annotation dialog'}); + jQuery('#' + uelt).append(openann.to_string()); +} + +/** + * Object. + * + * The contained_modal is a simple modal dialog + * Node modal: invisible until it's not modal dialog. + * + * NOTE: We're skipping some of the bbop.html stuff since we + * specifically want BS3 stuff and not the jQuery-UI stuff that is + * sometimes haning around in there. + * + * arg_title may be null, string, or bbop.html + * arg_body may be null, string, or bbop.html + * + * @constructor + */ +function contained_modal(type, arg_title, arg_body){ + + var shield_p = false; + if( type && type === 'shield' ){ + shield_p = true; + }else{ + // ??? + } + + // Define buttons first. + var x_btn_args = { + 'type': 'button', + 'class': 'close', + 'data-dismiss': 'modal', + 'aria-hidden': 'true' + }; + var x_btn = new bbop.html.tag('button', x_btn_args, '×'); + var close_btn_args = { + 'type': 'button', + 'class': 'btn btn-default', + 'data-dismiss': 'modal' + }; + var close_btn = new bbop.html.tag('button', close_btn_args, 'Close'); + + // Then the title. + var title_args = { + 'generate_id': true, + 'class': 'modal-title' + }; + var title = new bbop.html.tag('div', title_args, arg_title); + + // One button and the title are in the header. + var header_args = { + 'class': 'modal-header' + }; + var header = null; + if( shield_p ){ + header = new bbop.html.tag('div', header_args, title); + }else{ + header = new bbop.html.tag('div', header_args, [x_btn, title]); + } + + // The footer has the other button. + var footer_args = { + 'generate_id': true, + 'class': 'modal-footer' + }; + var footer = new bbop.html.tag('div', footer_args, close_btn); + + // Ready the body. + var body_args = { + 'generate_id': true, + 'class': 'modal-body' + }; + var body = new bbop.html.tag('div', body_args, arg_body); + + // Content has header, body, and footer. + var content_args = { + 'class': 'modal-content' + }; + var content = null; + if( shield_p ){ + content = new bbop.html.tag('div', content_args, [header,body]); + }else{ + content = new bbop.html.tag('div', content_args, [header,body,footer]); + } + + // Dialog contains content. + var dialog_args = { + 'class': 'modal-dialog' + }; + var dialog = new bbop.html.tag('div', dialog_args, content); + + // And the container contains it all. + var container_args = { + 'generate_id': true, + 'class': 'modal fade', + 'tabindex': '-1', + 'role': 'dialog', + 'aria-labelledby': body.get_id(), + 'aria-hidden': 'true' + }; + var container = new bbop.html.tag('div', container_args, dialog); + + // Attach the assembly to the DOM. + var modal_elt = '#' + container.get_id(); + jQuery('body').append(container.to_string()); + var modal_opts = { + }; + if( shield_p ){ + modal_opts['backdrop'] = 'static'; + modal_opts['keyboard'] = false; + } + + // Add destructor to hidden listener--clicking on the close with + // eliminate this dialog from the DOM completely. + jQuery(modal_elt).on('hidden.bs.modal', + function(){ jQuery(this).remove(); }); + + // Add activities. + // TODO + + /// + /// Add external controls, etc. + /// + + // To be used before show--add elements (as a string) to the main + // modal DOM (which can have events attached). + this.add_to_body = function(str){ + var add_to_elt = '#' + body.get_id(); + jQuery(add_to_elt).append(str); + }; + + // // To be used before show--add elements (as a string) to the main + // // modal DOM (which can have events attached). + // this.reset_footer = function(){ + // var add_to_elt = '#' + footer.get_id(); + // jQuery(add_to_elt).append(str); + // }; + + // To be used before show--add elements (as a string) to the main + // modal DOM (which can have events attached). + this.add_to_footer = function(str){ + var add_to_elt = '#' + footer.get_id(); + jQuery(add_to_elt).append(str); + }; + + // + this.show = function(){ + jQuery(modal_elt).modal(modal_opts); + }; + + // + // Will end up destorying it since we are listening for the + // "hidden" event above. + this.destroy = function(){ + jQuery(modal_elt).modal('hide'); + }; +} + +/** + * Contained blocking shield for general compute activity. + * + * Function that returns object. + * + * TODO: make subclass? + * + * @constructor + */ +function compute_shield(){ + + // Text. + var p = + new bbop.html.tag('p', {}, + 'Doing remote processing. This may take a minute...'); + + // Progress bar. + var pb_args = { + 'class': 'progress-bar', + 'role': 'progressbar', + 'aria-valuenow': '100', + 'aria-valuemin': '0', + 'aria-valuemax': '100', + 'style': 'width: 100%' + }; + var pb = new bbop.html.tag('div', pb_args, + 'Working...'); + var pb_container_args = { + 'class': 'progress progress-striped active' + }; + var pb_container = new bbop.html.tag('div', pb_container_args, pb); + + var mdl = new contained_modal('shield', 'Relax', [p, pb_container]); + return mdl; +} + +/** + * Function that returns a sorted relation list of the form [[id, label], ...] + * + * Optional boost when we don't care using the boolean "relevant" field. + * The boost is 10. + * + * TODO: make subclass? + */ +function sorted_relation_list(relations, aid){ + + var boost = 10; + + // Get a sorted list of known rels. + //var rels = aid.all_entities(); + var rels = relations.sort(function(a,b){ + var id_a = a['id']; + var id_b = b['id']; + + var pr_a = aid.priority(id_a); + var pr_b = aid.priority(id_b); + + // Looking at the optional boolean "relevant" field, if we + // showed no preference in our context, give these a + // boost. + if( pr_a === 0 && a['relevant'] ){ pr_a = boost; } + if( pr_b === 0 && b['relevant'] ){ pr_b = boost; } + + return pr_b - pr_a; + }); + var rellist = []; + each(rels, function(rel){ + // We have the id. + var r = [rel['id']]; + if( rel['label'] ){ // use their label + r.push(rel['label']); + }else{ // otherwise, try readable + r.push(aid.readable(rel['id'])); + } + rellist.push(r); + }); + + return rellist; +} + +/** + * Contained shield for creating new edges between nodes. + * + * Function that returns object. + * + * TODO: make subclass? + * + * @constructor + */ +function add_edge_modal(ecore, manager, relations, aid, source_id, target_id){ + + // Get a sorted list of known rels. + var rellist = sorted_relation_list(relations, aid); + + // Preamble. + var mebe = [ + // '

Relation selection

', + 'Edge source:', + source_id, + '
', + 'Edge target:', + target_id + ]; + + // Randomized radio. + var radio_name = bbop_core.uuid(); + + // Hard-code tree from + // https://github.com/geneontology/noctua/issues/165 as temporary + // relief. + function _fuse(rel_name, rel_id, radio_name, lvl, first_p){ + + // Main. + var str = '