From: Alexander Sadleir Date: Sat, 28 Jul 2012 13:08:44 +0000 Subject: Library updates X-Git-Url: https://maxious.lambdacomplex.org/git/?p=disclosr.git&a=commitdiff&h=f9e85a3ad37f2c71f3b1f86a3a2e0fc1078facbd --- Library updates Former-commit-id: 6bd29a4ce0df1531423533a3653d2086d21c3a55 --- --- a/.gitmodules +++ b/.gitmodules @@ -4,9 +4,6 @@ [submodule "couchdb/settee"] path = couchdb/settee url = https://github.com/inadarei/settee.git -[submodule "lib/springy"] - path = lib/springy - url = https://github.com/dhotson/springy.git [submodule "lib/php-diff"] path = lib/php-diff url = https://github.com/chrisboulton/php-diff.git @@ -19,4 +16,10 @@ [submodule "lib/phpquery"] path = lib/phpquery url = https://github.com/TobiaszCudnik/phpquery.git +[submodule "javascripts/sigma"] + path = javascripts/sigma + url = https://github.com/jacomyal/sigma.js.git +[submodule "javascripts/bubbletree"] + path = javascripts/bubbletree + url = https://github.com/okfn/bubbletree.git --- /dev/null +++ b/admin/conflicts.php @@ -1,1 +1,48 @@ +get_db('disclosr-agencies'); + +try { + $rows = $db->get_view("app", "getConflicts", null, true)->rows; + //print_r($rows); + foreach ($rows as $row) { +echo "

".$row->id."

"; +$request = Requests::get($serverAddr."disclosr-agencies/".$row->id); +$origSort = object_to_array(json_decode($request->body)); +ksort($origSort); + $origDoc = explode(",",json_encode($origSort)); + foreach($row->value as $conflictRev) { +$conflictURL = $serverAddr."disclosr-agencies/".$row->id."?rev=".$conflictRev; +$request = Requests::get($conflictURL); +$conflictSort = object_to_array(json_decode($request->body)); +ksort($conflictSort); + $conflictDoc = explode(",",json_encode($conflictSort)); +echo "curl -X DELETE ".$conflictURL."
".PHP_EOL; + // Options for generating the diff + $options = array( + //'ignoreWhitespace' => true, + //'ignoreCase' => true, + ); + + // Initialize the diff class + $diff = new Diff($conflictDoc, $origDoc, $options); + + // Generate a side by side diff + $renderer = new Diff_Renderer_Html_SideBySide; + echo $diff->Render($renderer); +} +die(); + + } +} catch (SetteeRestClientException $e) { + setteErrorHandler($e); +} + +include_footer(); +?> + --- /dev/null +++ b/admin/directory.gexf.php @@ -1,1 +1,59 @@ + "gov", "label" => "Federal Government")); +$edges = Array(); + +function addEdge($source, $target) { + global $edges; + $edges[] = Array("id" => md5($source . $target), "source" => $source, "target" => $target); +} + +function addNode($id, $label, $pid) { + global $nodes; + $nodes[] = Array("id" => $id, "label" => $label , "pid" => $pid); +} + +function addChildren($parentID, $parentXML) { + foreach ($parentXML as $childXML) { + + if ($childXML->getName() == "organization" || $childXML->getName() == "organizationalUnit" || $childXML->getName() == "person") { + $attr = $childXML->attributes(); + $id = $attr['UUID']; + if ($childXML->getName() == "organization" || $childXML->getName() == "organizationalUnit") { + + $label = $childXML->name; + } else if ($childXML->getName() == "person") { + $label = $childXML->fullName; + } + addNode($id, $label, $parentID); + addEdge($id, $parentID); + addChildren($id, $childXML); + } + } +} + +if (file_exists('directoryexport.xml')) { + $xml = simplexml_load_file('directoryexport.xml'); + + addChildren("gov", $xml); +} else { + exit('Failed to open directoryexport.xml'); +} + header('Content-Type: application/gexf+xml'); +echo ' + + + '; +foreach ($nodes as $node) { + echo ' '; +} +echo ' + '; +foreach ($edges as $edge) { + echo ' '; +} +echo ' + +'; +?> + --- /dev/null +++ b/admin/directoryexport.xml --- a/admin/exportEmployees.csv.php +++ b/admin/exportEmployees.csv.php @@ -5,7 +5,6 @@ $format = "csv"; //$format = "json"; if (isset($_REQUEST['format'])) $format = $_REQUEST['format']; - setlocale(LC_CTYPE, 'C'); if ($format == "csv") { $headers = Array("name"); --- a/admin/importAPSCEmployees.php +++ b/admin/importAPSCEmployees.php @@ -32,23 +32,35 @@ @$sums[$id][$timePeriod] += $data[1]; } else { echo "
ERROR NAME MISSING FROM ID LIST

" . PHP_EOL; - + die(); - } } fclose($handle); } } foreach ($sums as $id => $sum) { - echo $id. "
" . PHP_EOL; + echo $id . "
" . PHP_EOL; $doc = $db->get($id); - // print_r($doc); - if (isset($doc->statistics)) $doc->statistics = Array(); + echo $doc->name . "
" . PHP_EOL; + // print_r($doc); + $changed = false; + if (!isset($doc->statistics)) { + $changed = true; + $doc->statistics = Array(); + } foreach ($sum as $timePeriod => $value) { - $doc->statistics["employees"][$timePeriod] = Array("value"=>$value, "source"=>"http://apsc.gov.au/stateoftheservice/"); + if (!isset($doc->statistics->employees->$timePeriod->value) + || $doc->statistics->employees->$timePeriod->value != $value) { + $changed = true; + $doc->statistics["employees"][$timePeriod] = Array("value" => $value, "source" => "http://apsc.gov.au/stateoftheservice/"); + } } - $db->save($doc); + if ($changed) { + $db->save($doc); + } else { + echo "not changed" . "
" . PHP_EOL; + } } // employees: timeperiod, source = apsc state of service, value ?> --- a/admin/importGov2RegisterRSSFacebookTwitter.php +++ b/admin/importGov2RegisterRSSFacebookTwitter.php @@ -1,7 +1,7 @@ get_db('disclosr-agencies'); $rows = $db->get_view("app", "byName")->rows; @@ -10,41 +10,40 @@ foreach ($rows as $row) { $nametoid[trim($row->key)] = $row->value; } + function extractHTMLAccounts($url, $accountType) { global $accounts, $nametoid; $request = Requests::get($url); $doc = phpQuery::newDocumentHTML($request->body); phpQuery::selectDocument($doc); foreach (pq('tr')->elements as $tr) { - //echo $tr->nodeValue.PHP_EOL; - $agency = ""; - $url = ""; - foreach ($tr->childNodes as $td) { - $class = $td->getAttribute("class"); - //echo "cccc $class ".$td->nodeValue.PHP_EOL; - if ($class == "s11" || $class == "s10" || $class == "s7") { - $agency = $td->nodeValue; - } else if ($class == "s6" || $class == "s9"){ - $url = $td->nodeValue; - foreach($td->childNodes as $a) { - $href = $a->getAttribute("href"); - if ($href != "") { - $url = $href; - } - } - } - } - if ($agency != "" && $url != "") { - if (!in_array(trim($agency), array_keys($nametoid))) { - echo trim($agency)." missing" . PHP_EOL; - } else { - // echo $agency." = ".$url.PHP_EOL; - $accounts[$nametoid[trim($agency)]][$accountType][] = $url; + //echo $tr->nodeValue.PHP_EOL; + $agency = ""; + $url = ""; + foreach ($tr->childNodes as $td) { + $class = $td->getAttribute("class"); + //echo "cccc $class ".$td->nodeValue.PHP_EOL; + if ($class == "s11" || $class == "s10" || $class == "s7") { + $agency = $td->nodeValue; + } else if ($class == "s6" || $class == "s9") { + $url = $td->nodeValue; + foreach ($td->childNodes as $a) { + $href = $a->getAttribute("href"); + if ($href != "") { + $url = $href; } - - } + } + } + } + if ($agency != "" && $url != "") { + if (!in_array(trim($agency), array_keys($nametoid))) { + echo trim($agency) . " missing" . PHP_EOL; + } else { + // echo $agency." = ".$url.PHP_EOL; + $accounts[$nametoid[trim($agency)]][$accountType][] = $url; + } + } } - } function extractCSVAccounts($url, $accountType, $nameField, $accountField, $filter) { @@ -53,7 +52,7 @@ $Data = str_getcsv($request->body, "\n"); //parse the rows $headers = Array(); foreach ($Data as $num => $line) { - $Row = str_getcsv($line, ",",'"'); + $Row = str_getcsv($line, ",", '"'); if ($num == 0) { } else if ($num == 1) { @@ -64,7 +63,7 @@ $agencyName = $Row[array_search($nameField, $headers)]; if (!$filter || $Row[array_search("State", $headers)] == "NAT") { if (!in_array(trim($agencyName), array_keys($nametoid))) { - echo trim($agencyName)." missing" . PHP_EOL; + echo trim($agencyName) . " missing" . PHP_EOL; } else { // echo $Row[array_search($nameField, $headers)] . PHP_EOL; $accounts[$nametoid[trim($agencyName)]][$accountType][] = $Row[array_search($accountField, $headers)]; @@ -84,6 +83,18 @@ extractHTMLAccounts("https://docs.google.com/spreadsheet/pub?hl=en_GB&hl=en_GB&key=0Ah41IAK0HzSTdGJxandJREhLSGlWWUZfZ2xKOTNHZ0E&output=html", "RSS"); // facebook extractHTMLAccounts("https://docs.google.com/spreadsheet/pub?hl=en_GB&hl=en_GB&key=0Ah41IAK0HzSTdGtjcW9vOXdyZ3pOV21vQU51VmhzQnc&single=true&gid=0&output=html", "Facebook"); +foreach ($accounts as $id => $accountTypes) { + echo $id . "
" . PHP_EOL; + $doc = object_to_array($db->get($id)); + // print_r($doc); + foreach ($accountTypes as $accountType => $accounts) { + if (!isset($doc["has" . $accountType]) || !is_array($doc["has" . $accountType])) { + $doc["has" . $accountType] = Array(); + } + $doc["has" . $accountType] = array_unique(array_merge($doc["has" . $accountType], $accounts)); + } + $db->save($doc); +} ?> --- /dev/null +++ b/admin/metadata.py @@ -1,1 +1,22 @@ +#http://packages.python.org/CouchDB/client.html +import couchdb +from BeautifulSoup import BeautifulSoup +couch = couchdb.Server('http://127.0.0.1:5984/') + +# select database +docsdb = couch['disclosr-documents'] + +for row in docsdb.view('app/getMetadataExtractRequired'): + print row.id + html = docsdb.get_attachment(row.id,row.value.iterkeys().next()).read() + metadata = [] + # http://www.crummy.com/software/BeautifulSoup/documentation.html + soup = BeautifulSoup(html) +metatags = soup.meta + for metatag in metatags: + print metatag['name'] + doc = docsdb.get(row.id) + //doc['metadata'] = metadata + //docsdb.save(doc) + --- /dev/null +++ b/admin/validation.py @@ -1,1 +1,30 @@ +#http://packages.python.org/CouchDB/client.html +import couchdb +import json +import pprint +import re +from tidylib import tidy_document +couch = couchdb.Server('http://127.0.0.1:5984/') + +# select database +docsdb = couch['disclosr-documents'] + +def f(x): + invalid = re.compile(r"ensure|testing|flicker|updating|longdesc|Accessibility Checks|not recognized") + valid = re.compile(r"line") + return (not invalid.search(x)) and valid.search(x) and x != '' + +for row in docsdb.view('app/getValidationRequired'): + print row.id + html = docsdb.get_attachment(row.id,row.value.iterkeys().next()).read() + #print html + document, errors = tidy_document(html,options={'accessibility-check':1,'show-warnings':0,'markup':0},keep_doc=True) + #http://www.aprompt.ca/Tidy/accessibilitychecks.html + #print document + errors = '\n'.join(filter(f,errors.split('\n'))) + #print errors + doc = docsdb.get(row.id) + doc['validation'] = errors + docsdb.save(doc) + --- /dev/null +++ b/bubbletree.php @@ -1,1 +1,126 @@ + + + + + Minimal BubbleTree Demo + + + + + + + + + + + + + +
+
+
+ + + --- a/couchdb/settee +++ b/couchdb/settee --- a/graph.php +++ b/graph.php @@ -6,36 +6,46 @@ $format = $_REQUEST['format']; } -function add_node($id, $label) { +function add_node($id, $label, $parent="") { global $format; if ($format == "html") { - echo "nodes[\"$id\"] = graph.newNode({label: \"$label\"});" . PHP_EOL; + // echo "nodes[\"$id\"] = graph.newNode({label: \"$label\"});" . PHP_EOL; } if ($format == "dot" && $label != "") { echo "$id [label=\"$label\"];". PHP_EOL; } + if ($format == "gexf") { + echo "":">") + ."" + ."". PHP_EOL; + } } function add_edge($from, $to, $color) { global $format; if ($format == "html") { - echo "graph.newEdge(nodes[\"$from\"], nodes['$to'], {color: '$color'});" . PHP_EOL; + // echo "graph.newEdge(nodes[\"$from\"], nodes['$to'], {color: '$color'});" . PHP_EOL; } if ($format == "dot") { echo "$from -> $to ".($color != ""? "[color=$color]":"").";". PHP_EOL; } + if ($format == "gexf") { + echo "". PHP_EOL; + } +} +if ($format == "gexf") { + //header('Content-Type: text/xml'); + header('Content-Type: application/gexf+xml'); +echo ' + + + Gexf.net + A hello world! file + + + '. PHP_EOL; } -if ($format == "html") { - ?> - - - - +
+ + + + + +
+
'. PHP_EOL; +} //include_footer(); ?> --- a/include/couchdb.inc.php +++ b/include/couchdb.inc.php @@ -5,25 +5,28 @@ require ($basePath . 'couchdb/settee/src/settee.php'); function createDocumentsDesignDoc() { - /*"views": { - "web_server": { - "map": "function(doc) {\n emit(doc.web_server, 1);\n}", - "reduce": "function (key, values, rereduce) {\n return sum(values);\n}" - }, - "byAgency": { - "map": "function(doc) {\n emit(doc.agencyID, 1);\n}", - "reduce": "function (key, values, rereduce) {\n return sum(values);\n}" - }, - "byURL": { - "map": "function(doc) {\n emit(doc.url, doc);\n}" - }, - "agency": { - "map": "function(doc) {\n emit(doc.agencyID, doc);\n}" - }, - "byWebServer": { - "map": "function(doc) {\n emit(doc.web_server, doc);\n}" - } - }*/ + /* "views": { + "web_server": { + "map": "function(doc) {\n emit(doc.web_server, 1);\n}", + "reduce": "function (key, values, rereduce) {\n return sum(values);\n}" + }, + "byAgency": { + "map": "function(doc) {\n emit(doc.agencyID, 1);\n}", + "reduce": "function (key, values, rereduce) {\n return sum(values);\n}" + }, + "byURL": { + "map": "function(doc) {\n emit(doc.url, doc);\n}" + }, + "agency": { + "map": "function(doc) {\n emit(doc.agencyID, doc);\n}" + }, + "byWebServer": { + "map": "function(doc) {\n emit(doc.web_server, doc);\n}" + }, + "getValidationRequired": { + "map": "function(doc) {\nif (doc.mime_type == \"text/html\" \n&& typeof(doc.validation) == \"undefined\") {\n emit(doc._id, doc._attachments);\n}\n}" + } + } */ } function createAgencyDesignDoc() { @@ -92,7 +95,7 @@ } }"; // http://stackoverflow.com/questions/646628/javascript-startswith -$obj->views->score->map = 'if(!String.prototype.startsWith){ + $obj->views->score->map = 'if(!String.prototype.startsWith){ String.prototype.startsWith = function (str) { return !this.indexOf(str); } @@ -116,7 +119,7 @@ emit(count+doc._id, {id:doc._id, name: doc.name, score:count, orgType: doc.orgType, portfolio:portfolio}); } }'; - $obj->views->scoreHas->map = 'if(!String.prototype.startsWith){ + $obj->views->scoreHas->map = 'if(!String.prototype.startsWith){ String.prototype.startsWith = function (str) { return !this.indexOf(str); } @@ -139,7 +142,7 @@ $obj->views->scoreHas->reduce = 'function (key, values, rereduce) { return sum(values); }'; - $obj->views->fieldNames->map = ' + $obj->views->fieldNames->map = ' function(doc) { for(var propName in doc) { emit(propName, doc._id); @@ -154,16 +157,17 @@ } if (php_uname('n') == "vanille") { -$serverAddr = 'http://192.168.178.21:5984/'; - + $serverAddr = 'http://192.168.178.21:5984/'; } else if (php_uname('n') == "KYUUBEY") { - $serverAddr = 'http://192.168.1.148:5984/'; + $serverAddr = 'http://127.0.0.1:5984/'; } else { $serverAddr = 'http://127.0.0.1:5984/'; } - $server = new SetteeServer($serverAddr); +$server = new SetteeServer($serverAddr); + function setteErrorHandler($e) { echo $e->getMessage() . "
" . PHP_EOL; } + --- /dev/null +++ b/javascripts/bubbletree --- a/javascripts/flotr2 +++ b/javascripts/flotr2 --- /dev/null +++ b/javascripts/sigma --- /dev/null +++ b/javascripts/sigma.min.js @@ -1,1 +1,3552 @@ - +// Define packages: +var sigma = {}; +sigma.tools = {}; +sigma.classes = {}; +sigma.instances = {}; + +// Adding Array helpers, if not present yet: +(function() { + if (!Array.prototype.some) { + Array.prototype.some = function(fun /*, thisp*/) { + var len = this.length; + if (typeof fun != 'function') { + throw new TypeError(); + } + + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in this && + fun.call(thisp, this[i], i, this)) { + return true; + } + } + + return false; + }; + } + + if (!Array.prototype.forEach) { + Array.prototype.forEach = function(fun /*, thisp*/) { + var len = this.length; + if (typeof fun != 'function') { + throw new TypeError(); + } + + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in this) { + fun.call(thisp, this[i], i, this); + } + } + }; + } + + if (!Array.prototype.map) { + Array.prototype.map = function(fun /*, thisp*/) { + var len = this.length; + if (typeof fun != 'function') { + throw new TypeError(); + } + + var res = new Array(len); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in this) { + res[i] = fun.call(thisp, this[i], i, this); + } + } + + return res; + }; + } + + if (!Array.prototype.filter) { + Array.prototype.filter = function(fun /*, thisp*/) { + var len = this.length; + if (typeof fun != 'function') + throw new TypeError(); + + var res = new Array(); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in this) { + var val = this[i]; // in case fun mutates this + if (fun.call(thisp, val, i, this)) { + res.push(val); + } + } + } + + return res; + }; + } + + if (!Object.keys) { + Object.keys = (function() { + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; + + return function(obj) { + if (typeof obj !== 'object' && + typeof obj !== 'function' || + obj === null + ) { + throw new TypeError('Object.keys called on non-object'); + } + + var result = []; + + for (var prop in obj) { + if (hasOwnProperty.call(obj, prop)) result.push(prop); + } + + if (hasDontEnumBug) { + for (var i = 0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) { + result.push(dontEnums[i]); + } + } + } + return result; + } + })(); + } +})(); + +/** + * A jQuery like properties management class. It works like jQuery .css() + * method: You can call it with juste one string to get the corresponding + * property, with a string and anything else to set the corresponding property, + * or directly with an object, and then each pair string / object (or any type) + * will be set in the properties. + * @constructor + * @this {sigma.classes.Cascade} + */ +sigma.classes.Cascade = function() { + /** + * This instance properties. + * @protected + * @type {Object} + */ + this.p = {}; + + /** + * The method to use to set/get any property of this instance. + * @param {(string|Object)} a1 If it is a string and if a2 is undefined, + * then it will return the corresponding + * property. + * If it is a string and if a2 is set, then it + * will set a2 as the property corresponding to + * a1, and return this. + * If it is an object, then each pair string / + * object (or any other type) will be set as a + * property. + * @param {*?} a2 The new property corresponding to a1 if a1 is + * a string. + * @return {(*|sigma.classes.Cascade)} Returns itself or the corresponding + * property. + */ + this.config = function(a1, a2) { + if (typeof a1 == 'string' && a2 == undefined) { + return this.p[a1]; + } else { + var o = (typeof a1 == 'object' && a2 == undefined) ? a1 : {}; + if (typeof a1 == 'string') { + o[a1] = a2; + } + + for (var k in o) { + if (this.p[k] != undefined) { + this.p[k] = o[k]; + } + } + return this; + } + }; +}; + +/** + * sigma.js custom event dispatcher class. + * @constructor + * @this {sigma.classes.EventDispatcher} + */ +sigma.classes.EventDispatcher = function() { + /** + * An object containing all the different handlers bound to one or many + * events, indexed by these events. + * @private + * @type {Object.} + */ + var _h = {}; + + /** + * Represents "this", without the well-known scope issue. + * @private + * @type {sigma.classes.EventDispatcher} + */ + var _self = this; + + /** + * Will execute the handler the next (and only the next) time that the + * indicated event (or the indicated events) will be triggered. + * @param {string} events The name of the event (or the events + * separated by spaces). + * @param {function(Object)} handler The handler to bind. + * @return {sigma.classes.EventDispatcher} Returns itself. + */ + function one(events, handler) { + if (!handler || !events) { + return _self; + } + + var eArray = ((typeof events) == 'string') ? events.split(' ') : events; + + eArray.forEach(function(event) { + if (!_h[event]) { + _h[event] = []; + } + + _h[event].push({ + 'h': handler, + 'one': true + }); + }); + + return _self; + } + + /** + * Will execute the handler everytime that the indicated event (or the + * indicated events) will be triggered. + * @param {string} events The name of the event (or the events + * separated by spaces). + * @param {function(Object)} handler The handler to bind. + * @return {sigma.classes.EventDispatcher} Returns itself. + */ + function bind(events, handler) { + if (!handler || !events) { + return _self; + } + + var eArray = ((typeof events) == 'string') ? events.split(' ') : events; + + eArray.forEach(function(event) { + if (!_h[event]) { + _h[event] = []; + } + + _h[event].push({ + 'h': handler, + 'one': false + }); + }); + + return _self; + } + + /** + * Unbinds the handler from a specified event (or specified events). + * @param {?string} events The name of the event (or the events + * separated by spaces). If undefined, + * then all handlers are unbound. + * @param {?function(Object)} handler The handler to unbind. If undefined, + * each handler bound to the event or the + * events will be unbound. + * @return {sigma.classes.EventDispatcher} Returns itself. + */ + function unbind(events, handler) { + if (!events) { + _h = {}; + } + + var eArray = typeof events == 'string' ? events.split(' ') : events; + + if (handler) { + eArray.forEach(function(event) { + if (_h[event]) { + _h[event] = _h[event].filter(function(e) { + return e['h'] != handler; + }); + } + + if (_h[event] && _h[event].length == 0) { + delete _h[event]; + } + }); + }else { + eArray.forEach(function(event) { + delete _h[event]; + }); + } + + return _self; + } + + /** + * Executes each handler bound to the event + * @param {string} type The type of the event. + * @param {?Object} content The content of the event (optional). + * @return {sigma.classes.EventDispatcher} Returns itself. + */ + function dispatch(type, content) { + if (_h[type]) { + _h[type].forEach(function(e) { + e['h']({ + 'type': type, + 'content': content, + 'target': _self + }); + }); + + _h[type] = _h[type].filter(function(e) { + return !e['one']; + }); + } + + return _self; + } + + /* PUBLIC INTERFACE: */ + this.one = one; + this.bind = bind; + this.unbind = unbind; + this.dispatch = dispatch; +}; + +(function() { +// Define local shortcut: +var id = 0; + +// Define local package: +var local = {}; +local.plugins = []; + +sigma.init = function(dom) { + var inst = new Sigma(dom, (++id).toString()); + sigma.instances[id] = new SigmaPublic(inst); + return sigma.instances[id]; +}; + +/** + * This class listen to all the different mouse events, to normalize them and + * dispatch action events instead (from "startinterpolate" to "isdragging", + * etc). + * @constructor + * @extends sigma.classes.Cascade + * @extends sigma.classes.EventDispatcher + * @param {element} dom The DOM element to bind the handlers on. + * @this {MouseCaptor} + */ +function MouseCaptor(dom) { + sigma.classes.Cascade.call(this); + sigma.classes.EventDispatcher.call(this); + + /** + * Represents "this", without the well-known scope issue. + * @private + * @type {MouseCaptor} + */ + var self = this; + + /** + * The DOM element to bind the handlers on. + * @type {element} + */ + var dom = dom; + + /** + * The different parameters that define how this instance should work. + * @see sigma.classes.Cascade + * @type {Object} + */ + this.p = { + minRatio: 1, + maxRatio: 32, + marginRatio: 1, + zoomDelta: 0.1, + dragDelta: 0.3, + zoomMultiply: 2, + directZooming: false, + blockScroll: true, + inertia: 1.1, + mouseEnabled: true + }; + + var oldMouseX = 0; + var oldMouseY = 0; + var startX = 0; + var startY = 0; + + var oldStageX = 0; + var oldStageY = 0; + var oldRatio = 1; + + var targetRatio = 1; + var targetStageX = 0; + var targetStageY = 0; + + var lastStageX = 0; + var lastStageX2 = 0; + var lastStageY = 0; + var lastStageY2 = 0; + + var progress = 0; + var isZooming = false; + + this.stageX = 0; + this.stageY = 0; + this.ratio = 1; + + this.mouseX = 0; + this.mouseY = 0; + + this.isMouseDown = false; + + /** + * Extract the local X position from a mouse event. + * @private + * @param {event} e A mouse event. + * @return {number} The local X value of the mouse. + */ + function getX(e) { + return e.offsetX != undefined && e.offsetX || + e.layerX != undefined && e.layerX || + e.clientX != undefined && e.clientX; + }; + + /** + * Extract the local Y position from a mouse event. + * @private + * @param {event} e A mouse event. + * @return {number} The local Y value of the mouse. + */ + function getY(e) { + return e.offsetY != undefined && e.offsetY || + e.layerY != undefined && e.layerY || + e.clientY != undefined && e.clientY; + }; + + /** + * Extract the wheel delta from a mouse event. + * @private + * @param {event} e A mouse event. + * @return {number} The wheel delta of the mouse. + */ + function getDelta(e) { + return e.wheelDelta != undefined && e.wheelDelta || + e.detail != undefined && -e.detail; + }; + + /** + * The handler listening to the 'move' mouse event. It will set the mouseX + * and mouseY values as the mouse position values, prevent the default event, + * and dispatch a 'move' event. + * @private + * @param {event} event A 'move' mouse event. + */ + function moveHandler(event) { + oldMouseX = self.mouseX; + oldMouseY = self.mouseY; + + self.mouseX = getX(event); + self.mouseY = getY(event); + + self.isMouseDown && drag(event); + self.dispatch('move'); + + if (event.preventDefault) { + event.preventDefault(); + } else { + event.returnValue = false; + } + }; + + /** + * The handler listening to the 'up' mouse event. It will set the isMouseDown + * value as false, dispatch a 'mouseup' event, and trigger stopDrag(). + * @private + * @param {event} event A 'up' mouse event. + */ + function upHandler(event) { + if (self.p.mouseEnabled && self.isMouseDown) { + self.isMouseDown = false; + self.dispatch('mouseup'); + stopDrag(); + + if (event.preventDefault) { + event.preventDefault(); + } else { + event.returnValue = false; + } + } + }; + + /** + * The handler listening to the 'down' mouse event. It will set the + * isMouseDown value as true, dispatch a 'mousedown' event, and trigger + * startDrag(). + * @private + * @param {event} event A 'down' mouse event. + */ + function downHandler(event) { + if (self.p.mouseEnabled) { + self.isMouseDown = true; + oldMouseX = self.mouseX; + oldMouseY = self.mouseY; + + self.dispatch('mousedown'); + + startDrag(); + + if (event.preventDefault) { + event.preventDefault(); + } else { + event.returnValue = false; + } + } + }; + + /** + * The handler listening to the 'wheel' mouse event. It will trigger + * {@link startInterpolate} with the event delta as parameter. + * @private + * @param {event} event A 'wheel' mouse event. + */ + function wheelHandler(event) { + if (self.p.mouseEnabled) { + startInterpolate( + self.mouseX, + self.mouseY, + self.ratio * (getDelta(event) > 0 ? + self.p.zoomMultiply : + 1 / self.p.zoomMultiply) + ); + + if (self.p['blockScroll']) { + if (event.preventDefault) { + event.preventDefault(); + } else { + event.returnValue = false; + } + } + } + }; + + /** + * Will start computing the scene X and Y, until {@link stopDrag} is + * triggered. + */ + function startDrag() { + oldStageX = self.stageX; + oldStageY = self.stageY; + startX = self.mouseX; + startY = self.mouseY; + + lastStageX = self.stageX; + lastStageX2 = self.stageX; + lastStageY = self.stageY; + lastStageY2 = self.stageY; + + self.dispatch('startdrag'); + }; + + /** + * Stops computing the scene position. + */ + function stopDrag() { + if (oldStageX != self.stageX || oldStageY != self.stageY) { + startInterpolate( + self.stageX + self.p.inertia * (self.stageX - lastStageX2), + self.stageY + self.p.inertia * (self.stageY - lastStageY2) + ); + } + }; + + /** + * Computes the position of the scene, relatively to the mouse position, and + * dispatches a "drag" event. + */ + function drag() { + var newStageX = self.mouseX - startX + oldStageX; + var newStageY = self.mouseY - startY + oldStageY; + + if (newStageX != self.stageX || newStageY != self.stageY) { + lastStageX2 = lastStageX; + lastStageY2 = lastStageY; + + lastStageX = newStageX; + lastStageY = newStageY; + + self.stageX = newStageX; + self.stageY = newStageY; + self.dispatch('drag'); + } + }; + + /** + * Will start computing the scene zoom ratio, until {@link stopInterpolate} is + * triggered. + * @param {number} x The new stage X. + * @param {number} y The new stage Y. + * @param {number} ratio The new zoom ratio. + */ + function startInterpolate(x, y, ratio) { + if (self.isMouseDown) { + return; + } + + window.clearInterval(self.interpolationID); + isZooming = ratio != undefined; + + oldStageX = self.stageX; + targetStageX = x; + + oldStageY = self.stageY; + targetStageY = y; + + oldRatio = self.ratio; + targetRatio = ratio || self.ratio; + targetRatio = Math.min( + Math.max(targetRatio, self.p.minRatio), + self.p.maxRatio + ); + + progress = + self.p.directZooming ? + 1 - (isZooming ? self.p.zoomDelta : self.p.dragDelta) : + 0; + + if ( + self.ratio != targetRatio || + self.stageX != targetStageX || + self.stageY != targetStageY + ) { + interpolate(); + self.interpolationID = window.setInterval(interpolate, 50); + self.dispatch('startinterpolate'); + } + }; + + /** + * Stops the move interpolation. + */ + function stopInterpolate() { + var oldRatio = self.ratio; + + if (isZooming) { + self.ratio = targetRatio; + self.stageX = targetStageX + + (self.stageX - targetStageX) * + self.ratio / + oldRatio; + self.stageY = targetStageY + + (self.stageY - targetStageY) * + self.ratio / + oldRatio; + }else { + self.stageX = targetStageX; + self.stageY = targetStageY; + } + + self.dispatch('stopinterpolate'); + }; + + /** + * Computes the interpolate ratio and the position of the scene, relatively + * to the last mouse event delta received, and dispatches a "interpolate" + * event. + */ + function interpolate() { + progress += (isZooming ? self.p.zoomDelta : self.p.dragDelta); + progress = Math.min(progress, 1); + + var k = sigma.easing.quadratic.easeout(progress); + var oldRatio = self.ratio; + + self.ratio = oldRatio * (1 - k) + targetRatio * k; + + if (isZooming) { + self.stageX = targetStageX + + (self.stageX - targetStageX) * + self.ratio / + oldRatio; + + self.stageY = targetStageY + + (self.stageY - targetStageY) * + self.ratio / + oldRatio; + } else { + self.stageX = oldStageX * (1 - k) + targetStageX * k; + self.stageY = oldStageY * (1 - k) + targetStageY * k; + } + + self.dispatch('interpolate'); + if (progress >= 1) { + window.clearInterval(self.interpolationID); + stopInterpolate(); + } + }; + + /** + * Checks that there is always a part of the graph that is displayed, to + * avoid the user to drag the graph out of the stage. + * @param {Object} b An object containing the borders of the graph. + * @param {number} width The width of the stage. + * @param {number} height The height of the stage. + * @return {MouseCaptor} Returns itself. + */ + function checkBorders(b, width, height) { + // TODO : Find the good formula + /*if (!isNaN(b.minX) && !isNaN(b.maxX)) { + self.stageX = Math.min( + self.stageX = Math.max( + self.stageX, + (b.minX - width) * self.ratio + + self.p.marginRatio*(b.maxX - b.minX) + ), + (b.maxX - width) * self.ratio + + width - + self.p.marginRatio*(b.maxX - b.minX) + ); + } + + if (!isNaN(b.minY) && !isNaN(b.maxY)) { + self.stageY = Math.min( + self.stageY = Math.max( + self.stageY, + (b.minY - height) * self.ratio + + self.p.marginRatio*(b.maxY - b.minY) + ), + (b.maxY - height) * self.ratio + + height - + self.p.marginRatio*(b.maxY - b.minY) + ); + }*/ + + return self; + }; + + // ADD CALLBACKS + dom.addEventListener('DOMMouseScroll', wheelHandler, true); + dom.addEventListener('mousewheel', wheelHandler, true); + dom.addEventListener('mousemove', moveHandler, true); + dom.addEventListener('mousedown', downHandler, true); + document.addEventListener('mouseup', upHandler, true); + + this.checkBorders = checkBorders; + this.interpolate = startInterpolate; +} + +/** + * A class to monitor some local / global probes directly on an instance, + * inside a div DOM element. + * It executes different methods (called "probes") regularly, and displays + * the results on the element. + * @constructor + * @extends sigma.classes.Cascade + * @param {Sigma} instance The instance to monitor. + * @param {element} dom The div DOM element to draw write on. + * @this {Monitor} + */ +function Monitor(instance, dom) { + sigma.classes.Cascade.call(this); + + /** + * Represents "this", without the well-known scope issue. + * @private + * @type {Monitor} + */ + var self = this; + + /** + * {@link Sigma} instance owning this Monitor instance. + * @type {Sigma} + */ + this.instance = instance; + + /** + * Determines if the monitoring is activated or not. + * @type {Boolean} + */ + this.monitoring = false; + + /** + * The different parameters that define how this instance should work. It + * also contains the different probes. + * @see sigma.classes.Cascade + * @type {Object} + */ + this.p = { + fps: 40, + dom: dom, + globalProbes: { + 'Time (ms)': sigma.chronos.getExecutionTime, + 'Queue': sigma.chronos.getQueuedTasksCount, + 'Tasks': sigma.chronos.getTasksCount, + 'FPS': sigma.chronos.getFPS + }, + localProbes: { + 'Nodes count': function() { return self.instance.graph.nodes.length; }, + 'Edges count': function() { return self.instance.graph.edges.length; } + } + }; + + /** + * Activates the monitoring: Some texts describing some values about sigma.js + * or the owning {@link Sigma} instance will appear over the graph, but + * beneath the mouse sensible DOM element. + * @return {Monitor} Returns itself. + */ + function activate() { + if (!self.monitoring) { + self.monitoring = window.setInterval(routine, 1000 / self.p.fps); + } + + return self; + } + + /** + * Desactivates the monitoring: Will disappear, and stop computing the + * different probes. + * @return {Monitor} Returns itself. + */ + function desactivate() { + if (self.monitoring) { + window.clearInterval(self.monitoring); + self.monitoring = null; + + self.p.dom.innerHTML = ''; + } + + return self; + } + + /** + * The private method dedicated to compute the different values to observe. + * @private + * @return {Monitor} Returns itself. + */ + function routine() { + var s = ''; + + s += '

GLOBAL :

'; + for (var k in self.p.globalProbes) { + s += '

' + k + ' : ' + self.p.globalProbes[k]() + '

'; + } + + s += '

LOCAL :

'; + for (var k in self.p.localProbes) { + s += '

' + k + ' : ' + self.p.localProbes[k]() + '

'; + } + + self.p.dom.innerHTML = s; + + return self; + } + + this.activate = activate; + this.desactivate = desactivate; +} + +/** + * Sigma is the main class. It represents the core of any instance id sigma.js. + * It is private and can be initialized only from inside sigma.js. To see its + * public interface, see {@link SigmaPublic}. + * It owns its own {@link Graph}, {@link MouseCaptor}, {@link Plotter} + * and {@link Monitor}. + * @constructor + * @extends sigma.classes.Cascade + * @extends sigma.classes.EventDispatcher + * @param {element} root The DOM root of this instance (a div, for example). + * @param {string} id The ID of this instance. + * @this {Sigma} + */ +function Sigma(root, id) { + sigma.classes.Cascade.call(this); + sigma.classes.EventDispatcher.call(this); + + /** + * Represents "this", without the well-known scope issue. + * @private + * @type {Sigma} + */ + var self = this; + + /** + * The ID of the instance. + * @type {string} + */ + this.id = id.toString(); + + /** + * The different parameters that define how this instance should work. + * @see sigma.classes.Cascade + * @type {Object} + */ + this.p = { + auto: true, + drawNodes: 2, + drawEdges: 1, + drawLabels: 2, + lastNodes: 2, + lastEdges: 0, + lastLabels: 2, + drawHoverNodes: true, + drawActiveNodes: true + }; + + /** + * The root DOM element of this instance, containing every other elements. + * @type {element} + */ + this.domRoot = root; + + /** + * The width of this instance - initially, the root's width. + * @type {number} + */ + this.width = this.domRoot.offsetWidth; + + /** + * The height of this instance - initially, the root's height. + * @type {number} + */ + this.height = this.domRoot.offsetHeight; + + /** + * The graph of this instance - initiallyempty. + * @type {Graph} + */ + this.graph = new Graph(); + + /** + * An object referencing every DOM elements used by this instance. + * @type {Object} + */ + this.domElements = {}; + + initDOM('edges', 'canvas'); + initDOM('nodes', 'canvas'); + initDOM('labels', 'canvas'); + initDOM('hover', 'canvas'); + initDOM('monitor', 'div'); + initDOM('mouse', 'canvas'); + + /** + * The class dedicated to manage the drawing process of the graph of the + * different canvas. + * @type {Plotter} + */ + this.plotter = new Plotter( + this.domElements.nodes.getContext('2d'), + this.domElements.edges.getContext('2d'), + this.domElements.labels.getContext('2d'), + this.domElements.hover.getContext('2d'), + this.graph, + this.width, + this.height + ); + + /** + * The class dedicated to monitor different probes about the running + * processes or the data, such as the number of nodes or edges, or how + * many times the graph is drawn per second. + * @type {Monitor} + */ + this.monitor = new Monitor( + this, + this.domElements.monitor + ); + + /** + * The class dedicated to manage the different mouse events. + * @type {MouseCaptor} + */ + this.mousecaptor = new MouseCaptor( + this.domElements.mouse, + this.id + ); + + // Interaction listeners: + this.mousecaptor.bind('drag interpolate', function(e) { + self.draw( + self.p.auto ? 2 : self.p.drawNodes, + self.p.auto ? 0 : self.p.drawEdges, + self.p.auto ? 2 : self.p.drawLabels, + true + ); + }).bind('stopdrag stopinterpolate', function(e) { + self.draw( + self.p.auto ? 2 : self.p.drawNodes, + self.p.auto ? 1 : self.p.drawEdges, + self.p.auto ? 2 : self.p.drawLabels, + true + ); + }).bind('mousedown mouseup', function(e) { + var targeted = self.graph.nodes.filter(function(n) { + return !!n['hover']; + }).map(function(n) { + return n.id; + }); + + self.dispatch( + e['type'] == 'mousedown' ? + 'downgraph' : + 'upgraph' + ); + + if (targeted.length) { + self.dispatch( + e['type'] == 'mousedown' ? + 'downnodes' : + 'upnodes', + targeted + ); + } + }).bind('move', function() { + self.domElements.hover.getContext('2d').clearRect( + 0, + 0, + self.domElements.hover.width, + self.domElements.hover.height + ); + + drawHover(); + drawActive(); + }); + + sigma.chronos.bind('startgenerators', function() { + if (sigma.chronos.getGeneratorsIDs().some(function(id) { + return !!id.match(new RegExp('_ext_' + self.id + '$', '')); + })) { + self.draw( + self.p.auto ? 2 : self.p.drawNodes, + self.p.auto ? 0 : self.p.drawEdges, + self.p.auto ? 2 : self.p.drawLabels + ); + } + }).bind('stopgenerators', function() { + self.draw(); + }); + + /** + * Resizes the element, and redraws the graph with the last settings. + * @param {?number} w The new width (if undefined, it will use the root + * width). + * @param {?number} h The new height (if undefined, it will use the root + * height). + * @return {Sigma} Returns itself. + */ + function resize(w, h) { + var oldW = self.width, oldH = self.height; + + if (w != undefined && h != undefined) { + self.width = w; + self.height = h; + }else { + self.width = self.domRoot.offsetWidth; + self.height = self.domRoot.offsetHeight; + } + + if (oldW != self.width || oldH != self.height) { + for (var k in self.domElements) { + self.domElements[k].setAttribute('width', self.width + 'px'); + self.domElements[k].setAttribute('height', self.height + 'px'); + } + + self.plotter.resize(self.width, self.height); + + self.draw( + self.p.lastNodes, + self.p.lastEdges, + self.p.lastLabels, + true + ); + } + return self; + }; + + /** + * Kills every drawing task currently running. Basically, it stops this + * instance's drawing process. + * @return {Sigma} Returns itself. + */ + function clearSchedule() { + sigma.chronos.removeTask( + 'node_' + self.id, 2 + ).removeTask( + 'edge_' + self.id, 2 + ).removeTask( + 'label_' + self.id, 2 + ).stopTasks(); + return self; + }; + + /** + * Initialize a DOM element, that will be stores by this instance, to make + * automatic these elements resizing. + * @private + * @param {string} id The element's ID. + * @param {string} type The element's nodeName (Example : canvas, div, ...). + * @return {Sigma} Returns itself. + */ + function initDOM(id, type) { + self.domElements[id] = document.createElement(type); + self.domElements[id].style.position = 'absolute'; + self.domElements[id].setAttribute('id', 'sigma_' + id + '_' + self.id); + self.domElements[id].setAttribute('class', 'sigma_' + id + '_' + type); + self.domElements[id].setAttribute('width', self.width + 'px'); + self.domElements[id].setAttribute('height', self.height + 'px'); + + self.domRoot.appendChild(self.domElements[id]); + return self; + }; + + /** + * Starts the graph drawing process. The three first parameters indicate + * how the different layers have to be drawn: + * . -1: The layer is not drawn, but it is not erased. + * . 0: The layer is not drawn. + * . 1: The layer is drawn progressively. + * . 2: The layer is drawn directly. + * @param {?number} nodes Determines if and how the nodes must be drawn. + * @param {?number} edges Determines if and how the edges must be drawn. + * @param {?number} labels Determines if and how the labels must be drawn. + * @param {?boolean} safe If true, nothing will happen if any generator + * affiliated to this instance is currently running + * (an iterative layout, for example). + * @return {Sigma} Returns itself. + */ + function draw(nodes, edges, labels, safe) { + if (safe && sigma.chronos.getGeneratorsIDs().some(function(id) { + return !!id.match(new RegExp('_ext_' + self.id + '$', '')); + })) { + return self; + } + + var n = (nodes == undefined) ? self.p.drawNodes : nodes; + var e = (edges == undefined) ? self.p.drawEdges : edges; + var l = (labels == undefined) ? self.p.drawLabels : labels; + + var params = { + nodes: n, + edges: e, + labels: l + }; + + self.p.lastNodes = n; + self.p.lastEdges = e; + self.p.lastLabels = l; + + // Remove tasks: + clearSchedule(); + + // Rescale graph: + self.graph.rescale( + self.width, + self.height, + n > 0, + e > 0 + ).setBorders(); + + self.mousecaptor.checkBorders( + self.graph.borders, + self.width, + self.height + ); + + self.graph.translate( + self.mousecaptor.stageX, + self.mousecaptor.stageY, + self.mousecaptor.ratio, + n > 0, + e > 0 + ); + + self.dispatch( + 'graphscaled' + ); + + // Clear scene: + for (var k in self.domElements) { + if ( + self.domElements[k].nodeName.toLowerCase() == 'canvas' && + (params[k] == undefined || params[k] >= 0) + ) { + self.domElements[k].getContext('2d').clearRect( + 0, + 0, + self.domElements[k].width, + self.domElements[k].height + ); + } + } + + self.plotter.currentEdgeIndex = 0; + self.plotter.currentNodeIndex = 0; + self.plotter.currentLabelIndex = 0; + + var previous = null; + var start = false; + + if (n) { + if (n > 1) { + while (self.plotter.task_drawNode()) {} + }else { + sigma.chronos.addTask( + self.plotter.task_drawNode, + 'node_' + self.id, + false + ); + + start = true; + previous = 'node_' + self.id; + } + } + + if (l) { + if (l > 1) { + while (self.plotter.task_drawLabel()) {} + } else { + if (previous) { + sigma.chronos.queueTask( + self.plotter.task_drawLabel, + 'label_' + self.id, + previous + ); + } else { + sigma.chronos.addTask( + self.plotter.task_drawLabel, + 'label_' + self.id, + false + ); + } + + start = true; + previous = 'label_' + self.id; + } + } + + if (e) { + if (e > 1) { + while (self.plotter.task_drawEdge()) {} + }else { + if (previous) { + sigma.chronos.queueTask( + self.plotter.task_drawEdge, + 'edge_' + self.id, + previous + ); + }else { + sigma.chronos.addTask( + self.plotter.task_drawEdge, + 'edge_' + self.id, + false + ); + } + + start = true; + previous = 'edge_' + self.id; + } + } + + self.dispatch( + 'draw' + ); + + self.refresh(); + + start && sigma.chronos.runTasks(); + return self; + }; + + /** + * Draws the hover and active nodes labels. + * @return {Sigma} Returns itself. + */ + function refresh() { + self.domElements.hover.getContext('2d').clearRect( + 0, + 0, + self.domElements.hover.width, + self.domElements.hover.height + ); + + drawHover(); + drawActive(); + + return self; + } + + /** + * Draws the hover nodes labels. This method is applied directly, and does + * not use the pseudo-asynchronous tasks process. + * @return {Sigma} Returns itself. + */ + function drawHover() { + if (self.p.drawHoverNodes) { + self.graph.checkHover( + self.mousecaptor.mouseX, + self.mousecaptor.mouseY + ); + + self.graph.nodes.forEach(function(node) { + if (node.hover && !node.active) { + self.plotter.drawHoverNode(node); + } + }); + } + + return self; + } + + /** + * Draws the active nodes labels. This method is applied directly, and does + * not use the pseudo-asynchronous tasks process. + * @return {Sigma} Returns itself. + */ + function drawActive() { + if (self.p.drawActiveNodes) { + self.graph.nodes.forEach(function(node) { + if (node.active) { + self.plotter.drawActiveNode(node); + } + }); + } + + return self; + } + + // Apply plugins: + for (var i = 0; i < local.plugins.length; i++) { + local.plugins[i](this); + } + + this.draw = draw; + this.resize = resize; + this.refresh = refresh; + this.drawHover = drawHover; + this.drawActive = drawActive; + this.clearSchedule = clearSchedule; + + window.addEventListener('resize', function() { + self.resize(); + }); +} + +/** + * This class draws the graph on the different canvas DOM elements. It just + * contains all the different methods to draw the graph, synchronously or + * pseudo-asynchronously. + * @constructor + * @param {CanvasRenderingContext2D} nodesCtx Context dedicated to draw nodes. + * @param {CanvasRenderingContext2D} edgesCtx Context dedicated to draw edges. + * @param {CanvasRenderingContext2D} labelsCtx Context dedicated to draw + * labels. + * @param {CanvasRenderingContext2D} hoverCtx Context dedicated to draw hover + * nodes labels. + * @param {Graph} graph A reference to the graph to + * draw. + * @param {number} w The width of the DOM root + * element. + * @param {number} h The width of the DOM root + * element. + * @extends sigma.classes.Cascade + * @this {Plotter} + */ +function Plotter(nodesCtx, edgesCtx, labelsCtx, hoverCtx, graph, w, h) { + sigma.classes.Cascade.call(this); + + /** + * Represents "this", without the well-known scope issue. + * @private + * @type {Plotter} + */ + var self = this; + + /** + * The different parameters that define how this instance should work. + * @see sigma.classes.Cascade + * @type {Object} + */ + this.p = { + // ------- + // LABELS: + // ------- + // Label color: + // - 'node' + // - default (then defaultLabelColor + // will be used instead) + labelColor: 'default', + defaultLabelColor: '#000', + // Label hover background color: + // - 'node' + // - default (then defaultHoverLabelBGColor + // will be used instead) + labelHoverBGColor: 'default', + defaultHoverLabelBGColor: '#fff', + // Label hover shadow: + labelHoverShadow: true, + labelHoverShadowColor: '#000', + // Label hover color: + // - 'node' + // - default (then defaultLabelHoverColor + // will be used instead) + labelHoverColor: 'default', + defaultLabelHoverColor: '#000', + // Label active background color: + // - 'node' + // - default (then defaultActiveLabelBGColor + // will be used instead) + labelActiveBGColor: 'default', + defaultActiveLabelBGColor: '#fff', + // Label active shadow: + labelActiveShadow: true, + labelActiveShadowColor: '#000', + // Label active color: + // - 'node' + // - default (then defaultLabelActiveColor + // will be used instead) + labelActiveColor: 'default', + defaultLabelActiveColor: '#000', + // Label size: + // - 'fixed' + // - 'proportional' + // Label size: + // - 'fixed' + // - 'proportional' + labelSize: 'fixed', + defaultLabelSize: 12, // for fixed display only + labelSizeRatio: 2, // for proportional display only + labelThreshold: 6, + font: 'Arial', + hoverFont: '', + activeFont: '', + fontStyle: '', + hoverFontStyle: '', + activeFontStyle: '', + // ------ + // EDGES: + // ------ + // Edge color: + // - 'source' + // - 'target' + // - default (then defaultEdgeColor or edge['color'] + // will be used instead) + edgeColor: 'source', + defaultEdgeColor: '#aaa', + defaultEdgeType: 'line', + // ------ + // NODES: + // ------ + defaultNodeColor: '#aaa', + // HOVER: + // Node hover color: + // - 'node' + // - default (then defaultNodeHoverColor + // will be used instead) + nodeHoverColor: 'node', + defaultNodeHoverColor: '#fff', + // ACTIVE: + // Node active color: + // - 'node' + // - default (then defaultNodeActiveColor + // will be used instead) + nodeActiveColor: 'node', + defaultNodeActiveColor: '#fff', + // Node border color: + // - 'node' + // - default (then defaultNodeBorderColor + // will be used instead) + borderSize: 0, + nodeBorderColor: 'node', + defaultNodeBorderColor: '#fff', + // -------- + // PROCESS: + // -------- + edgesSpeed: 200, + nodesSpeed: 200, + labelsSpeed: 200 + }; + + /** + * The canvas context dedicated to draw the nodes. + * @type {CanvasRenderingContext2D} + */ + var nodesCtx = nodesCtx; + + /** + * The canvas context dedicated to draw the edges. + * @type {CanvasRenderingContext2D} + */ + var edgesCtx = edgesCtx; + + /** + * The canvas context dedicated to draw the labels. + * @type {CanvasRenderingContext2D} + */ + var labelsCtx = labelsCtx; + + /** + * The canvas context dedicated to draw the hover nodes. + * @type {CanvasRenderingContext2D} + */ + var hoverCtx = hoverCtx; + + /** + * A reference to the graph to draw. + * @type {Graph} + */ + var graph = graph; + + /** + * The width of the stage to draw on. + * @type {number} + */ + var width = w; + + /** + * The height of the stage to draw on. + * @type {number} + */ + var height = h; + + /** + * The index of the next edge to draw. + * @type {number} + */ + this.currentEdgeIndex = 0; + + /** + * The index of the next node to draw. + * @type {number} + */ + this.currentNodeIndex = 0; + + /** + * The index of the next label to draw. + * @type {number} + */ + this.currentLabelIndex = 0; + + /** + * An atomic function to drawn the N next edges, with N as edgesSpeed. + * The counter is {@link this.currentEdgeIndex}. + * This function has been designed to work with {@link sigma.chronos}, that + * will insert frames at the middle of the calls, to make the edges drawing + * process fluid for the user. + * @see sigma.chronos + * @return {boolean} Returns true if all the edges are drawn and false else. + */ + function task_drawEdge() { + var c = graph.edges.length; + var s, t, i = 0; + + while (i++< self.p.edgesSpeed && self.currentEdgeIndex < c) { + e = graph.edges[self.currentEdgeIndex]; + s = e['source']; + t = e['target']; + if (e['hidden'] || + s['hidden'] || + t['hidden'] || + (!self.isOnScreen(s) && !self.isOnScreen(t))) { + self.currentEdgeIndex++; + }else { + drawEdge(graph.edges[self.currentEdgeIndex++]); + } + } + + return self.currentEdgeIndex < c; + }; + + /** + * An atomic function to drawn the N next nodes, with N as nodesSpeed. + * The counter is {@link this.currentEdgeIndex}. + * This function has been designed to work with {@link sigma.chronos}, that + * will insert frames at the middle of the calls, to make the nodes drawing + * process fluid for the user. + * @see sigma.chronos + * @return {boolean} Returns true if all the nodes are drawn and false else. + */ + function task_drawNode() { + var c = graph.nodes.length; + var i = 0; + + while (i++< self.p.nodesSpeed && self.currentNodeIndex < c) { + if (!self.isOnScreen(graph.nodes[self.currentNodeIndex])) { + self.currentNodeIndex++; + }else { + drawNode(graph.nodes[self.currentNodeIndex++]); + } + } + + return self.currentNodeIndex < c; + }; + + /** + * An atomic function to drawn the N next labels, with N as labelsSpeed. + * The counter is {@link this.currentEdgeIndex}. + * This function has been designed to work with {@link sigma.chronos}, that + * will insert frames at the middle of the calls, to make the labels drawing + * process fluid for the user. + * @see sigma.chronos + * @return {boolean} Returns true if all the labels are drawn and false else. + */ + function task_drawLabel() { + var c = graph.nodes.length; + var i = 0; + + while (i++< self.p.labelsSpeed && self.currentLabelIndex < c) { + if (!self.isOnScreen(graph.nodes[self.currentLabelIndex])) { + self.currentLabelIndex++; + }else { + drawLabel(graph.nodes[self.currentLabelIndex++]); + } + } + + return self.currentLabelIndex < c; + }; + + /** + * Draws one node to the corresponding canvas. + * @param {Object} node The node to draw. + * @return {Plotter} Returns itself. + */ + function drawNode(node) { + var size = Math.round(node['displaySize'] * 10) / 10; + var ctx = nodesCtx; + + ctx.fillStyle = node['color']; + ctx.beginPath(); + ctx.arc(node['displayX'], + node['displayY'], + size, + 0, + Math.PI * 2, + true); + + ctx.closePath(); + ctx.fill(); + + node['hover'] && drawHoverNode(node); + return self; + }; + + /** + * Draws one edge to the corresponding canvas. + * @param {Object} edge The edge to draw. + * @return {Plotter} Returns itself. + */ + function drawEdge(edge) { + var x1 = edge['source']['displayX']; + var y1 = edge['source']['displayY']; + var x2 = edge['target']['displayX']; + var y2 = edge['target']['displayY']; + var color = edge['color']; + + if (!color) { + switch (self.p.edgeColor) { + case 'source': + color = edge['source']['color'] || + self.p.defaultNodeColor; + break; + case 'target': + color = edge['target']['color'] || + self.p.defaultNodeColor; + break; + default: + color = self.p.defaultEdgeColor; + break; + } + } + + var ctx = edgesCtx; + + switch (edge['type'] || self.p.defaultEdgeType) { + case 'curve': + ctx.strokeStyle = color; + ctx.lineWidth = edge['displaySize'] / 3; + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.quadraticCurveTo((x1 + x2) / 2 + (y2 - y1) / 4, + (y1 + y2) / 2 + (x1 - x2) / 4, + x2, + y2); + ctx.stroke(); + break; + case 'line': + default: + ctx.strokeStyle = color; + ctx.lineWidth = edge['displaySize'] / 3; + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + + ctx.stroke(); + break; + } + + return self; + }; + + /** + * Draws one label to the corresponding canvas. + * @param {Object} node The label to draw. + * @return {Plotter} Returns itself. + */ + function drawLabel(node) { + var ctx = labelsCtx; + + if (node['displaySize'] >= self.p.labelThreshold || node['forceLabel']) { + var fontSize = self.p.labelSize == 'fixed' ? + self.p.defaultLabelSize : + self.p.labelSizeRatio * node['displaySize']; + + ctx.font = self.p.fontStyle + fontSize + 'px ' + self.p.font; + + ctx.fillStyle = self.p.labelColor == 'node' ? + (node['color'] || self.p.defaultNodeColor) : + self.p.defaultLabelColor; + ctx.fillText( + node['label'], + Math.round(node['displayX'] + node['displaySize'] * 1.5), + Math.round(node['displayY'] + fontSize / 2 - 3) + ); + } + + return self; + }; + + /** + * Draws one hover node to the corresponding canvas. + * @param {Object} node The hover node to draw. + * @return {Plotter} Returns itself. + */ + function drawHoverNode(node) { + var ctx = hoverCtx; + + var fontSize = self.p.labelSize == 'fixed' ? + self.p.defaultLabelSize : + self.p.labelSizeRatio * node['displaySize']; + + ctx.font = (self.p.hoverFontStyle || self.p.fontStyle || '') + ' ' + + fontSize + 'px ' + + (self.p.hoverFont || self.p.font || ''); + + ctx.fillStyle = self.p.labelHoverBGColor == 'node' ? + (node['color'] || self.p.defaultNodeColor) : + self.p.defaultHoverLabelBGColor; + + // Label background: + ctx.beginPath(); + + if (self.p.labelHoverShadow) { + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + ctx.shadowBlur = 4; + ctx.shadowColor = self.p.labelHoverShadowColor; + } + + sigma.tools.drawRoundRect( + ctx, + Math.round(node['displayX'] - fontSize / 2 - 2), + Math.round(node['displayY'] - fontSize / 2 - 2), + Math.round(ctx.measureText(node['label']).width + + node['displaySize'] * 1.5 + + fontSize / 2 + 4), + Math.round(fontSize + 4), + Math.round(fontSize / 2 + 2), + 'left' + ); + ctx.closePath(); + ctx.fill(); + + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + ctx.shadowBlur = 0; + + // Node border: + ctx.beginPath(); + ctx.fillStyle = self.p.nodeBorderColor == 'node' ? + (node['color'] || self.p.defaultNodeColor) : + self.p.defaultNodeBorderColor; + ctx.arc(Math.round(node['displayX']), + Math.round(node['displayY']), + node['displaySize'] + self.p.borderSize, + 0, + Math.PI * 2, + true); + ctx.closePath(); + ctx.fill(); + + // Node: + ctx.beginPath(); + ctx.fillStyle = self.p.nodeHoverColor == 'node' ? + (node['color'] || self.p.defaultNodeColor) : + self.p.defaultNodeHoverColor; + ctx.arc(Math.round(node['displayX']), + Math.round(node['displayY']), + node['displaySize'], + 0, + Math.PI * 2, + true); + + ctx.closePath(); + ctx.fill(); + + // Label: + ctx.fillStyle = self.p.labelHoverColor == 'node' ? + (node['color'] || self.p.defaultNodeColor) : + self.p.defaultLabelHoverColor; + ctx.fillText( + node['label'], + Math.round(node['displayX'] + node['displaySize'] * 1.5), + Math.round(node['displayY'] + fontSize / 2 - 3) + ); + + return self; + }; + + /** + * Draws one active node to the corresponding canvas. + * @param {Object} node The active node to draw. + * @return {Plotter} Returns itself. + */ + function drawActiveNode(node) { + var ctx = hoverCtx; + + if (!isOnScreen(node)) { + return self; + } + + var fontSize = self.p.labelSize == 'fixed' ? + self.p.defaultLabelSize : + self.p.labelSizeRatio * node['displaySize']; + + ctx.font = (self.p.activeFontStyle || self.p.fontStyle || '') + ' ' + + fontSize + 'px ' + + (self.p.activeFont || self.p.font || ''); + + ctx.fillStyle = self.p.labelHoverBGColor == 'node' ? + (node['color'] || self.p.defaultNodeColor) : + self.p.defaultActiveLabelBGColor; + + // Label background: + ctx.beginPath(); + + if (self.p.labelActiveShadow) { + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + ctx.shadowBlur = 4; + ctx.shadowColor = self.p.labelActiveShadowColor; + } + + sigma.tools.drawRoundRect( + ctx, + Math.round(node['displayX'] - fontSize / 2 - 2), + Math.round(node['displayY'] - fontSize / 2 - 2), + Math.round(ctx.measureText(node['label']).width + + node['displaySize'] * 1.5 + + fontSize / 2 + 4), + Math.round(fontSize + 4), + Math.round(fontSize / 2 + 2), + 'left' + ); + ctx.closePath(); + ctx.fill(); + + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + ctx.shadowBlur = 0; + + // Node border: + ctx.beginPath(); + ctx.fillStyle = self.p.nodeBorderColor == 'node' ? + (node['color'] || self.p.defaultNodeColor) : + self.p.defaultNodeBorderColor; + ctx.arc(Math.round(node['displayX']), + Math.round(node['displayY']), + node['displaySize'] + self.p.borderSize, + 0, + Math.PI * 2, + true); + ctx.closePath(); + ctx.fill(); + + // Node: + ctx.beginPath(); + ctx.fillStyle = self.p.nodeActiveColor == 'node' ? + (node['color'] || self.p.defaultNodeColor) : + self.p.defaultNodeActiveColor; + ctx.arc(Math.round(node['displayX']), + Math.round(node['displayY']), + node['displaySize'], + 0, + Math.PI * 2, + true); + + ctx.closePath(); + ctx.fill(); + + // Label: + ctx.fillStyle = self.p.labelActiveColor == 'node' ? + (node['color'] || self.p.defaultNodeColor) : + self.p.defaultLabelActiveColor; + ctx.fillText( + node['label'], + Math.round(node['displayX'] + node['displaySize'] * 1.5), + Math.round(node['displayY'] + fontSize / 2 - 3) + ); + + return self; + }; + + /** + * Determines if a node is on the screen or not. The limits here are + * bigger than the actual screen, to avoid seeing labels disappear during + * the graph manipulation. + * @param {Object} node The node to check if it is on or out the screen. + * @return {boolean} Returns false if the node is hidden or not on the screen + * or true else. + */ + function isOnScreen(node) { + if (isNaN(node['x']) || isNaN(node['y'])) { + throw (new Error('A node\'s coordinate is not a ' + + 'number (id: ' + node['id'] + ')') + ); + } + + return !node['hidden'] && + (node['displayX'] + node['displaySize'] > -width / 3) && + (node['displayX'] - node['displaySize'] < width * 4 / 3) && + (node['displayY'] + node['displaySize'] > -height / 3) && + (node['displayY'] - node['displaySize'] < height * 4 / 3); + }; + + /** + * Resizes this instance. + * @param {number} w The new width. + * @param {number} h The new height. + * @return {Plotter} Returns itself. + */ + function resize(w, h) { + width = w; + height = h; + + return self; + } + + this.task_drawLabel = task_drawLabel; + this.task_drawEdge = task_drawEdge; + this.task_drawNode = task_drawNode; + this.drawActiveNode = drawActiveNode; + this.drawHoverNode = drawHoverNode; + this.isOnScreen = isOnScreen; + this.resize = resize; +} + +function SigmaPublic(sigmaInstance) { + var s = sigmaInstance; + var self = this; + sigma.classes.EventDispatcher.call(this); + + this._core = sigmaInstance; + + this.kill = function() { + // TODO + }; + + this.getID = function() { + return s.id; + }; + + // Config: + this.configProperties = function(a1, a2) { + var res = s.config(a1, a2); + return res == s ? self : res; + }; + + this.drawingProperties = function(a1, a2) { + var res = s.plotter.config(a1, a2); + return res == s.plotter ? self : res; + }; + + this.mouseProperties = function(a1, a2) { + var res = s.mousecaptor.config(a1, a2); + return res == s.mousecaptor ? self : res; + }; + + this.graphProperties = function(a1, a2) { + var res = s.graph.config(a1, a2); + return res == s.graph ? self : res; + }; + + this.getMouse = function() { + return { + mouseX: s.mousecaptor.mouseX, + mouseY: s.mousecaptor.mouseY, + down: s.mousecaptor.isMouseDown + }; + }; + + // Actions: + this.position = function(stageX, stageY, ratio) { + if (arguments.length == 0) { + return { + stageX: s.mousecaptor.stageX, + stageY: s.mousecaptor.stageY, + ratio: s.mousecaptor.ratio + }; + }else { + s.mousecaptor.stageX = stageX != undefined ? + stageX : + s.mousecaptor.stageX; + s.mousecaptor.stageY = stageY != undefined ? + stageY : + s.mousecaptor.stageY; + s.mousecaptor.ratio = ratio != undefined ? + ratio : + s.mousecaptor.ratio; + + return self; + } + }; + + this.goTo = function(stageX, stageY, ratio) { + s.mousecaptor.interpolate(stageX, stageY, ratio); + return self; + }; + + this.zoomTo = function(x, y, ratio) { + ratio = Math.min( + Math.max(s.mousecaptor.config('minRatio'), ratio), + s.mousecaptor.config('maxRatio') + ); + if (ratio == s.mousecaptor.ratio) { + s.mousecaptor.interpolate( + x - s.width / 2 + s.mousecaptor.stageX, + y - s.height / 2 + s.mousecaptor.stageY + ); + }else { + s.mousecaptor.interpolate( + (ratio * x - s.mousecaptor.ratio * s.width/2) / + (ratio - s.mousecaptor.ratio), + (ratio * y - s.mousecaptor.ratio * s.height/2) / + (ratio - s.mousecaptor.ratio), + ratio + ); + } + return self; + }; + + this.resize = function(w, h) { + s.resize(w, h); + return self; + }; + + this.draw = function(nodes, edges, labels, safe) { + s.draw(nodes, edges, labels, safe); + return self; + }; + + this.refresh = function() { + s.refresh(); + return self; + }; + + // Tasks methods: + this.addGenerator = function(id, task, condition) { + sigma.chronos.addGenerator(id + '_ext_' + s.id, task, condition); + return self; + }; + + this.removeGenerator = function(id) { + sigma.chronos.removeGenerator(id + '_ext_' + s.id); + return self; + }; + + // Graph methods: + this.addNode = function(id, params) { + s.graph.addNode(id, params); + return self; + }; + + this.addEdge = function(id, source, target, params) { + s.graph.addEdge(id, source, target, params); + return self; + } + + this.dropNode = function(v) { + s.graph.dropNode(v); + return self; + }; + + this.dropEdge = function(v) { + s.graph.dropEdge(v); + return self; + }; + + this.pushGraph = function(object, safe) { + object.nodes && object.nodes.forEach(function(node) { + node['id'] && (!safe || !s.graph.nodesIndex[node['id']]) && + self.addNode(node['id'], node); + }); + + var isEdgeValid; + object.edges && object.edges.forEach(function(edge) { + validID = edge['source'] && edge['target'] && edge['id']; + validID && + (!safe || !s.graph.edgesIndex[edge['id']]) && + self.addNode( + edge['id'], + edge['source'], + edge['target'], + edge + ); + }); + + return self; + }; + + this.emptyGraph = function() { + s.graph.empty(); + return self; + }; + + this.getNodesCount = function() { + return s.graph.nodes.length; + }; + + this.getEdgesCount = function() { + return s.graph.edges.length; + }; + + this.iterNodes = function(fun, ids) { + s.graph.iterNodes(fun, ids); + return self; + }; + + this.iterEdges = function(fun, ids) { + s.graph.iterEdges(fun, ids); + return self; + }; + + this.getNodes = function(ids) { + return s.graph.getNodes(ids); + }; + + this.getEdges = function(ids) { + return s.graph.getEdges(ids); + }; + + // Monitoring + this.activateMonitoring = function() { + return s.monitor.activate(); + }; + + this.desactivateMonitoring = function() { + return s.monitor.desactivate(); + }; + + // Events + s.bind('downnodes upnodes downgraph upgraph', function(e) { + self.dispatch(e.type, e.content); + }); + + s.graph.bind('overnodes outnodes', function(e) { + self.dispatch(e.type, e.content); + }); +} + +/** + * The graph data model used in sigma.js. + * @constructor + * @extends sigma.classes.Cascade + * @extends sigma.classes.EventDispatcher + * @this {Graph} + */ +function Graph() { + sigma.classes.Cascade.call(this); + sigma.classes.EventDispatcher.call(this); + + /** + * Represents "this", without the well-known scope issue. + * @private + * @type {Graph} + */ + var self = this; + + /** + * The different parameters that determine how the nodes and edges should be + * translated and rescaled. + * @type {Object} + */ + this.p = { + minNodeSize: 0, + maxNodeSize: 0, + minEdgeSize: 0, + maxEdgeSize: 0, + // Scaling mode: + // - 'inside' (default) + // - 'outside' + scalingMode: 'inside', + nodesPowRatio: 0.5, + edgesPowRatio: 0 + }; + + /** + * Contains the borders of the graph. These are useful to avoid the user to + * drag the graph out of the canvas. + * @type {Object} + */ + this.borders = {}; + + /** + * Inserts a node in the graph. + * @param {string} id The node's ID. + * @param {object} params An object containing the different parameters + * of the node. + * @return {Graph} Returns itself. + */ + function addNode(id, params) { + if (self.nodesIndex[id]) { + throw new Error('Node "' + id + '" already exists.'); + } + + params = params || {}; + var n = { + // Numbers : + 'x': 0, + 'y': 0, + 'size': 1, + 'degree': 0, + 'inDegree': 0, + 'outDegree': 0, + // Flags : + 'fixed': false, + 'active': false, + 'hidden': false, + 'forceLabel': false, + // Strings : + 'label': id.toString(), + 'id': id.toString(), + // Custom attributes : + 'attr': {} + }; + + for (var k in params) { + switch (k) { + case 'id': + break; + case 'x': + case 'y': + case 'size': + n[k] = +params[k]; + break; + case 'fixed': + case 'active': + case 'hidden': + case 'forceLabel': + n[k] = !!params[k]; + break; + case 'color': + case 'label': + n[k] = params[k]; + break; + default: + n['attr'][k] = params[k]; + } + } + + self.nodes.push(n); + self.nodesIndex[id.toString()] = n; + + return self; + }; + + /** + * Generates the clone of a node, to make it easier to be exported. + * @private + * @param {Object} node The node to clone. + * @return {Object} The clone of the node. + */ + function cloneNode(node) { + return { + 'x': node['x'], + 'y': node['y'], + 'size': node['size'], + 'degree': node['degree'], + 'inDegree': node['inDegree'], + 'outDegree': node['outDegree'], + 'displayX': node['displayX'], + 'displayY': node['displayY'], + 'displaySize': node['displaySize'], + 'label': node['label'], + 'id': node['id'], + 'color': node['color'], + 'fixed': node['fixed'], + 'active': node['active'], + 'hidden': node['hidden'], + 'forceLabel': node['forceLabel'], + 'attr': node['attr'] + }; + }; + + /** + * Checks the clone of a node, and inserts its values when possible. For + * example, it is possible to modify the size or the color of a node, but it + * is not possible to modify its display values or its id. + * @private + * @param {Object} node The original node. + * @param {Object} copy The clone. + * @return {Graph} Returns itself. + */ + function checkNode(node, copy) { + for (var k in copy) { + switch (k) { + case 'id': + case 'attr': + case 'degree': + case 'inDegree': + case 'outDegree': + case 'displayX': + case 'displayY': + case 'displaySize': + break; + case 'x': + case 'y': + case 'size': + node[k] = +copy[k]; + break; + case 'fixed': + case 'active': + case 'hidden': + case 'forceLabel': + node[k] = !!copy[k]; + break; + case 'color': + case 'label': + node[k] = (copy[k] || '').toString(); + break; + default: + node['attr'][k] = copy[k]; + } + } + + return self; + }; + + /** + * Deletes one or several nodes from the graph, and the related edges. + * @param {(string|Array.)} v A string ID, or an Array of several + * IDs. + * @return {Graph} Returns itself. + */ + function dropNode(v) { + var a = (v instanceof Array ? v : [v]) || []; + + a.forEach(function(id) { + if (self.nodesIndex[id]) { + var index = null; + self.nodes.some(function(n, i) { + if (n['id'] == id) { + index = i; + return true; + } + return false; + }); + + index != null && self.nodes.splice(index, 1); + delete self.nodesIndex[id]; + + var edgesToRemove = []; + self.edges = self.edges.filter(function(e) { + if (e['source']['id'] == id) { + delete self.edgesIndex[e['id']]; + e['target']['degree']--; + e['target']['inDegree']--; + return false; + }else if (e['target']['id'] == id) { + delete self.edgesIndex[e['id']]; + e['source']['degree']--; + e['source']['outDegree']--; + return false; + } + return true; + }); + }else { + sigma.log('Node "' + id + '" does not exist.'); + } + }); + + return self; + }; + + /** + * Inserts an edge in the graph. + * @param {string} id The edge ID. + * @param {string} source The ID of the edge source. + * @param {string} target The ID of the edge target. + * @param {object} params An object containing the different parameters + * of the edge. + * @return {Graph} Returns itself. + */ + function addEdge(id, source, target, params) { + if (self.edgesIndex[id]) { + throw new Error('Edge "' + id + '" already exists.'); + } + + if (!self.nodesIndex[source]) { + var s = 'Edge\'s source "' + source + '" does not exist yet.'; + throw new Error(s); + } + + if (!self.nodesIndex[target]) { + var s = 'Edge\'s target "' + target + '" does not exist yet.'; + throw new Error(s); + } + + params = params || {}; + var e = { + 'source': self.nodesIndex[source], + 'target': self.nodesIndex[target], + 'size': 1, + 'weight': 1, + 'displaySize': 0.5, + 'label': id.toString(), + 'id': id.toString(), + 'hidden': false, + 'attr': {} + }; + + e['source']['degree']++; + e['source']['outDegree']++; + e['target']['degree']++; + e['target']['inDegree']++; + + for (var k in params) { + switch (k) { + case 'id': + case 'source': + case 'target': + break; + case 'hidden': + e[k] = !!params[k]; + break; + case 'size': + case 'weight': + e[k] = +params[k]; + break; + case 'color': + e[k] = params[k].toString(); + break; + case 'type': + e[k] = params[k].toString(); + break; + case 'label': + e[k] = params[k]; + break; + default: + e['attr'][k] = params[k]; + } + } + + self.edges.push(e); + self.edgesIndex[id.toString()] = e; + + return self; + }; + + /** + * Generates the clone of a edge, to make it easier to be exported. + * @private + * @param {Object} edge The edge to clone. + * @return {Object} The clone of the edge. + */ + function cloneEdge(edge) { + return { + 'source': edge['source']['id'], + 'target': edge['target']['id'], + 'size': edge['size'], + 'type': edge['type'], + 'weight': edge['weight'], + 'displaySize': edge['displaySize'], + 'label': edge['label'], + 'hidden': edge['hidden'], + 'id': edge['id'], + 'attr': edge['attr'], + 'color': edge['color'] + }; + }; + + /** + * Checks the clone of an edge, and inserts its values when possible. For + * example, it is possible to modify the label or the type of an edge, but it + * is not possible to modify its display values or its id. + * @private + * @param {Object} edge The original edge. + * @param {Object} copy The clone. + * @return {Graph} Returns itself. + */ + function checkEdge(edge, copy) { + for (var k in copy) { + switch (k) { + case 'id': + case 'displaySize': + break; + case 'weight': + case 'size': + edge[k] = +copy[k]; + break; + case 'source': + case 'target': + edge[k] = self.nodesIndex[k] || edge[k]; + break; + case 'hidden': + edge[k] = !!copy[k]; + break; + case 'color': + case 'label': + case 'type': + edge[k] = (copy[k] || '').toString(); + break; + default: + edge['attr'][k] = copy[k]; + } + } + + return self; + }; + + /** + * Deletes one or several edges from the graph. + * @param {(string|Array.)} v A string ID, or an Array of several + * IDs. + * @return {Graph} Returns itself. + */ + function dropEdge(v) { + var a = (v instanceof Array ? v : [v]) || []; + + a.forEach(function(id) { + if (self.edgesIndex[id]) { + self.edgesIndex[id]['source']['degree']--; + self.edgesIndex[id]['source']['outDegree']--; + self.edgesIndex[id]['target']['degree']--; + self.edgesIndex[id]['target']['inDegree']--; + + var index = null; + self.edges.some(function(n, i) { + if (n['id'] == id) { + index = i; + return true; + } + return false; + }); + + index != null && self.edges.splice(index, 1); + delete self.edgesIndex[id]; + }else { + sigma.log('Edge "' + id + '" does not exist.'); + } + }); + + return self; + }; + + /** + * Deletes every nodes and edges from the graph. + * @return {Graph} Returns itself. + */ + function empty() { + self.nodes = []; + self.nodesIndex = {}; + self.edges = []; + self.edgesIndex = {}; + + return self; + }; + + /** + * Computes the display x, y and size of each node, relatively to the + * original values and the borders determined in the parameters, such as + * each node is in the described area. + * @param {number} w The area width (actually the width of the DOM + * root). + * @param {number} h The area height (actually the height of the + * DOM root). + * @param {boolean} parseNodes Indicates if the nodes have to be parsed. + * @param {boolean} parseEdges Indicates if the edges have to be parsed. + * @return {Graph} Returns itself. + */ + function rescale(w, h, parseNodes, parseEdges) { + var weightMax = 0, sizeMax = 0; + + parseNodes && self.nodes.forEach(function(node) { + sizeMax = Math.max(node['size'], sizeMax); + }); + + parseEdges && self.edges.forEach(function(edge) { + weightMax = Math.max(edge['size'], weightMax); + }); + + sizeMax = sizeMax || 1; + weightMax = weightMax || 1; + + // Recenter the nodes: + var xMin, xMax, yMin, yMax; + parseNodes && self.nodes.forEach(function(node) { + xMax = Math.max(node['x'], xMax || node['x']); + xMin = Math.min(node['x'], xMin || node['x']); + yMax = Math.max(node['y'], yMax || node['y']); + yMin = Math.min(node['y'], yMin || node['y']); + }); + + // First, we compute the scaling ratio, without considering the sizes + // of the nodes : Each node will have its center in the canvas, but might + // be partially out of it. + var scale = self.p.scalingMode == 'outside' ? + Math.max(w / Math.max(xMax - xMin, 1), + h / Math.max(yMax - yMin, 1)) : + Math.min(w / Math.max(xMax - xMin, 1), + h / Math.max(yMax - yMin, 1)); + + // Then, we correct that scaling ratio considering a margin, which is + // basically the size of the biggest node. + // This has to be done as a correction since to compare the size of the + // biggest node to the X and Y values, we have to first get an + // approximation of the scaling ratio. + var margin = (self.p.maxNodeSize || sizeMax) / scale; + xMax += margin; + xMin -= margin; + yMax += margin; + yMin -= margin; + + scale = self.p.scalingMode == 'outside' ? + Math.max(w / Math.max(xMax - xMin, 1), + h / Math.max(yMax - yMin, 1)) : + Math.min(w / Math.max(xMax - xMin, 1), + h / Math.max(yMax - yMin, 1)); + + // Size homothetic parameters: + var a, b; + if (!self.p.maxNodeSize && !self.p.minNodeSize) { + a = 1; + b = 0; + }else if (self.p.maxNodeSize == self.p.minNodeSize) { + a = 0; + b = self.p.maxNodeSize; + }else { + a = (self.p.maxNodeSize - self.p.minNodeSize) / sizeMax; + b = self.p.minNodeSize; + } + + var c, d; + if (!self.p.maxEdgeSize && !self.p.minEdgeSize) { + c = 1; + d = 0; + }else if (self.p.maxEdgeSize == self.p.minEdgeSize) { + c = 0; + d = self.p.minEdgeSize; + }else { + c = (self.p.maxEdgeSize - self.p.minEdgeSize) / weightMax; + d = self.p.minEdgeSize; + } + + // Rescale the nodes: + parseNodes && self.nodes.forEach(function(node) { + node['displaySize'] = node['size'] * a + b; + + if (!node['fixed']) { + node['displayX'] = (node['x'] - (xMax + xMin) / 2) * scale + w / 2; + node['displayY'] = (node['y'] - (yMax + yMin) / 2) * scale + h / 2; + } + }); + + parseEdges && self.edges.forEach(function(edge) { + edge['displaySize'] = edge['size'] * c + d; + }); + + return self; + }; + + /** + * Translates the display values of the nodes and edges relatively to the + * scene position and zoom ratio. + * @param {number} sceneX The x position of the scene. + * @param {number} sceneY The y position of the scene. + * @param {number} ratio The zoom ratio of the scene. + * @param {boolean} parseNodes Indicates if the nodes have to be parsed. + * @param {boolean} parseEdges Indicates if the edges have to be parsed. + * @return {Graph} Returns itself. + */ + function translate(sceneX, sceneY, ratio, parseNodes, parseEdges) { + var sizeRatio = Math.pow(ratio, self.p.nodesPowRatio); + parseNodes && self.nodes.forEach(function(node) { + if (!node['fixed']) { + node['displayX'] = node['displayX'] * ratio + sceneX; + node['displayY'] = node['displayY'] * ratio + sceneY; + } + + node['displaySize'] = node['displaySize'] * sizeRatio; + }); + + sizeRatio = Math.pow(ratio, self.p.edgesPowRatio); + parseEdges && self.edges.forEach(function(edge) { + edge['displaySize'] = edge['displaySize'] * sizeRatio; + }); + + return self; + }; + + /** + * Determines the borders of the graph as it will be drawn. It is used to + * avoid the user to drag the graph out of the canvas. + */ + function setBorders() { + self.borders = {}; + + self.nodes.forEach(function(node) { + self.borders.minX = Math.min( + self.borders.minX == undefined ? + node['displayX'] - node['displaySize'] : + self.borders.minX, + node['displayX'] - node['displaySize'] + ); + + self.borders.maxX = Math.max( + self.borders.maxX == undefined ? + node['displayX'] + node['displaySize'] : + self.borders.maxX, + node['displayX'] + node['displaySize'] + ); + + self.borders.minY = Math.min( + self.borders.minY == undefined ? + node['displayY'] - node['displaySize'] : + self.borders.minY, + node['displayY'] - node['displaySize'] + ); + + self.borders.maxY = Math.max( + self.borders.maxY == undefined ? + node['displayY'] - node['displaySize'] : + self.borders.maxY, + node['displayY'] - node['displaySize'] + ); + }); + } + + /** + * Checks which nodes are under the (mX, mY) points, representing the mouse + * position. + * @param {number} mX The mouse X position. + * @param {number} mY The mouse Y position. + * @return {Graph} Returns itself. + */ + function checkHover(mX, mY) { + var dX, dY, s, over = [], out = []; + self.nodes.forEach(function(node) { + if (node['hidden']) { + node['hover'] = false; + return; + } + + dX = Math.abs(node['displayX'] - mX); + dY = Math.abs(node['displayY'] - mY); + s = node['displaySize']; + + var oldH = node['hover']; + var newH = dX < s && dY < s && Math.sqrt(dX * dX + dY * dY) < s; + + if (oldH && !newH) { + node['hover'] = false; + out.push(node.id); + } else if (newH && !oldH) { + node['hover'] = true; + over.push(node.id); + } + }); + + over.length && self.dispatch('overnodes', over); + out.length && self.dispatch('outnodes', out); + + return self; + }; + + /** + * Applies a function to a clone of each node (or indicated nodes), and then + * tries to apply the modifications made on the clones to the original nodes. + * @param {function(Object)} fun The function to execute. + * @param {?Array.} ids An Array of node IDs (optional). + * @return {Graph} Returns itself. + */ + function iterNodes(fun, ids) { + var a = ids ? ids.map(function(id) { + return self.nodesIndex[id]; + }) : self.nodes; + + var aCopies = a.map(cloneNode); + aCopies.forEach(fun); + + a.forEach(function(n, i) { + checkNode(n, aCopies[i]); + }); + + return self; + }; + + /** + * Applies a function to a clone of each edge (or indicated edges), and then + * tries to apply the modifications made on the clones to the original edges. + * @param {function(Object)} fun The function to execute. + * @param {?Array.} ids An Array of edge IDs (optional). + * @return {Graph} Returns itself. + */ + function iterEdges(fun, ids) { + var a = ids ? ids.map(function(id) { + return self.edgesIndex[id]; + }) : self.edges; + + var aCopies = a.map(cloneEdge); + aCopies.forEach(fun); + + a.forEach(function(e, i) { + checkEdge(e, aCopies[i]); + }); + + return self; + }; + + /** + * Returns a specific node clone or an array of specified node clones. + * @param {(string|Array.)} ids The ID or an array of node IDs. + * @return {(Object|Array.)} The clone or the array of clones. + */ + function getNodes(ids) { + var a = ((ids instanceof Array ? ids : [ids]) || []).map(function(id) { + return cloneNode(self.nodesIndex[id]); + }); + + return (ids instanceof Array ? a : a[0]); + }; + + /** + * Returns a specific edge clone or an array of specified edge clones. + * @param {(string|Array.)} ids The ID or an array of edge IDs. + * @return {(Object|Array.)} The clone or the array of clones. + */ + function getEdges(ids) { + var a = ((ids instanceof Array ? ids : [ids]) || []).map(function(id) { + return cloneEdge(self.edgesIndex[id]); + }); + + return (ids instanceof Array ? a : a[0]); + }; + + empty(); + + this.addNode = addNode; + this.addEdge = addEdge; + this.dropNode = dropNode; + this.dropEdge = dropEdge; + + this.iterEdges = iterEdges; + this.iterNodes = iterNodes; + + this.getEdges = getEdges; + this.getNodes = getNodes; + + this.empty = empty; + this.rescale = rescale; + this.translate = translate; + this.setBorders = setBorders; + this.checkHover = checkHover; +} + +sigma.easing = { + linear: {}, + quadratic: {} +}; + +sigma.easing.linear.easenone = function(k) { + return k; +}; + +sigma.easing.quadratic.easein = function(k) { + return k * k; +}; + +sigma.easing.quadratic.easeout = function(k) { + return - k * (k - 2); +}; + +sigma.easing.quadratic.easeinout = function(k) { + if ((k *= 2) < 1) return 0.5 * k * k; + return - 0.5 * (--k * (k - 2) - 1); +}; + +sigma.debugMode = 0; + +sigma.log = function() { + if (sigma.debugMode == 1) { + for (var k in arguments) { + console.log(arguments[k]); + } + }else if (sigma.debugMode > 1) { + for (var k in arguments) { + throw new Error(arguments[k]); + } + } + + return sigma; +}; + +/** + * Add a function to the prototype of SigmaPublic, but with access to the + * Sigma class properties. + * @param {string} pluginName [description]. + * @param {function} caller [description]. + * @param {function(Sigma)} launcher [description]. + */ +sigma.addPlugin = function(pluginName, caller, launcher) { + SigmaPublic.prototype[pluginName] = caller; + local.plugins.push(launcher); +}; +sigma.tools.drawRoundRect = function(ctx, x, y, w, h, ellipse, corners) { + var e = ellipse ? ellipse : 0; + var c = corners ? corners : []; + c = ((typeof c) == 'string') ? c.split(' ') : c; + + var tl = e && (c.indexOf('topleft') >= 0 || + c.indexOf('top') >= 0 || + c.indexOf('left') >= 0); + var tr = e && (c.indexOf('topright') >= 0 || + c.indexOf('top') >= 0 || + c.indexOf('right') >= 0); + var bl = e && (c.indexOf('bottomleft') >= 0 || + c.indexOf('bottom') >= 0 || + c.indexOf('left') >= 0); + var br = e && (c.indexOf('bottomright') >= 0 || + c.indexOf('bottom') >= 0 || + c.indexOf('right') >= 0); + + ctx.moveTo(x, y + e); + + if (tl) { + ctx.arcTo(x, y, x + e, y, e); + }else { + ctx.lineTo(x, y); + } + + if (tr) { + ctx.lineTo(x + w - e, y); + ctx.arcTo(x + w, y, x + w, y + e, e); + }else { + ctx.lineTo(x + w, y); + } + + if (br) { + ctx.lineTo(x + w, y + h - e); + ctx.arcTo(x + w, y + h, x + w - e, y + h, e); + }else { + ctx.lineTo(x + w, y + h); + } + + if (bl) { + ctx.lineTo(x + e, y + h); + ctx.arcTo(x, y + h, x, y + h - e, e); + }else { + ctx.lineTo(x, y + h); + } + + ctx.lineTo(x, y + e); +}; + +sigma.tools.getRGB = function(s, asArray) { + s = s.toString(); + var res = { + 'r': 0, + 'g': 0, + 'b': 0 + }; + + if (s.length >= 3) { + if (s.charAt(0) == '#') { + var l = s.length - 1; + if (l == 6) { + res = { + 'r': parseInt(s.charAt(1) + s.charAt(2), 16), + 'g': parseInt(s.charAt(3) + s.charAt(4), 16), + 'b': parseInt(s.charAt(5) + s.charAt(5), 16) + }; + }else if (l == 3) { + res = { + 'r': parseInt(s.charAt(1) + s.charAt(1), 16), + 'g': parseInt(s.charAt(2) + s.charAt(2), 16), + 'b': parseInt(s.charAt(3) + s.charAt(3), 16) + }; + } + } + } + + if (asArray) { + res = [ + res['r'], + res['g'], + res['b'] + ]; + } + + return res; +}; + +sigma.tools.rgbToHex = function(R, G, B) { + return sigma.tools.toHex(R) + sigma.tools.toHex(G) + sigma.tools.toHex(B); +}; + +sigma.tools.toHex = function(n) { + n = parseInt(n, 10); + + if (isNaN(n)) { + return '00'; + } + n = Math.max(0, Math.min(n, 255)); + return '0123456789ABCDEF'.charAt((n - n % 16) / 16) + + '0123456789ABCDEF'.charAt(n % 16); +}; + +/** + * sigma.chronos manages frames insertion to simulate asynchronous computing. + * It has been designed to make possible to execute heavy computing tasks + * for the browser, without freezing it. + * @constructor + * @extends sigma.classes.Cascade + * @extends sigma.classes.EventDispatcher + * @this {sigma.chronos} + */ +sigma.chronos = new (function() { + sigma.classes.EventDispatcher.call(this); + + /** + * Represents "this", without the well-known scope issue. + * @private + * @type {sigma.chronos} + */ + var self = this; + + /** + * Indicates whether any task is actively running or not. + * @private + * @type {boolean} + */ + var isRunning = false; + + /** + * Indicates the FPS "goal", that will define the theoretical + * frame length. + * @private + * @type {number} + */ + var fpsReq = 80; + + /** + * Stores the last computed FPS value (FPS is computed only when any + * task is running). + * @private + * @type {number} + */ + var lastFPS = 0; + + /** + * The number of frames inserted since the last start. + * @private + * @type {number} + */ + var framesCount = 0; + + /** + * The theoretical frame time. + * @private + * @type {number} + */ + var frameTime = 1000 / fpsReq; + + /** + * The theoretical frame length, minus the last measured delay. + * @private + * @type {number} + */ + var correctedFrameTime = frameTime; + + /** + * The measured length of the last frame. + * @private + * @type {number} + */ + var effectiveTime = 0; + + /** + * The time passed since the last runTasks action. + * @private + * @type {number} + */ + var currentTime = 0; + + /** + * The time when the last frame was inserted. + * @private + * @type {number} + */ + var startTime = 0; + + /** + * The difference between the theoretical frame length and the + * last measured frame length. + * @private + * @type {number} + */ + var delay = 0; + + /** + * The container of all active generators. + * @private + * @type {Object.} + */ + var generators = {}; + + /** + * The array of all the referenced and active tasks. + * @private + * @type {Array.} + */ + var tasks = []; + + /** + * The array of all the referenced and queued tasks. + * @private + * @type {Array.} + */ + var queuedTasks = []; + + /** + * The index of the next task to execute. + * @private + * @type {number} + */ + var taskIndex = 0; + + + /** + * Inserts a frame before executing the callback. + * @param {function()} callback The callback to execute after having + * inserted the frame. + * @return {sigma.chronos} Returns itself. + */ + function insertFrame(callback) { + window.setTimeout(callback, 0); + return self; + } + + /** + * The local method that executes routine, and inserts frames when needed. + * It dispatches a "frameinserted" event after having inserted any frame, + * and an "insertframe" event before. + * @private + */ + function frameInserter() { + self.dispatch('frameinserted'); + while (isRunning && tasks.length && routine()) {} + + if (!isRunning || !tasks.length) { + stopTasks(); + } else { + startTime = (new Date()).getTime(); + framesCount++; + delay = effectiveTime - frameTime; + correctedFrameTime = frameTime - delay; + + self.dispatch('insertframe'); + insertFrame(frameInserter); + } + }; + + /** + * The local method that executes the tasks, and compares the current frame + * length to the ideal frame length. + * @private + * @return {boolean} Returns false if the current frame should be ended, + * and true else. + */ + function routine() { + taskIndex = taskIndex % tasks.length; + + if (!tasks[taskIndex].task()) { + var n = tasks[taskIndex].taskName; + + queuedTasks = queuedTasks.filter(function(e) { + (e.taskParent == n) && tasks.push({ + taskName: e.taskName, + task: e.task + }); + return e.taskParent != n; + }); + + self.dispatch('killed', tasks.splice(taskIndex--, 1)[0]); + } + + taskIndex++; + effectiveTime = (new Date()).getTime() - startTime; + return effectiveTime <= correctedFrameTime; + }; + + /** + * Starts tasks execution. + * @return {sigma.chronos} Returns itself. + */ + function runTasks() { + isRunning = true; + taskIndex = 0; + framesCount = 0; + + startTime = (new Date()).getTime(); + currentTime = startTime; + + self.dispatch('start'); + self.dispatch('insertframe'); + insertFrame(frameInserter); + return self; + }; + + /** + * Stops tasks execution, and dispatch a "stop" event. + * @return {sigma.chronos} Returns itself. + */ + function stopTasks() { + self.dispatch('stop'); + isRunning = false; + return self; + }; + + /** + * A task is a function that will be executed continuously while it returns + * true. As soon as it return false, the task will be removed. + * If several tasks are present, they will be executed in parallele. + * This method will add the task to this execution process. + * @param {function(): boolean} task The task to add. + * @param {string} name The name of the worker, used for + * managing the different tasks. + * @param {boolean} autostart If true, sigma.chronos will start + * automatically if it is not working + * yet. + * @return {sigma.chronos} Returns itself. + */ + function addTask(task, name, autostart) { + if (typeof task != 'function') { + throw new Error('Task "' + name + '" is not a function'); + } + + tasks.push({ + taskName: name, + task: task + }); + + isRunning = !!(isRunning || (autostart && runTasks()) || true); + return self; + }; + + /** + * Will add a task that will be start to be executed as soon as a task + * named as the parent will be removed. + * @param {function(): boolean} task The task to add. + * @param {string} name The name of the worker, used for + * managing the different tasks. + * @param {string} parent The name of the parent task. + * @return {sigma.chronos} Returns itself. + */ + function queueTask(task, name, parent) { + if (typeof task != 'function') { + throw new Error('Task "' + name + '" is not a function'); + } + + if (!tasks.concat(queuedTasks).some(function(e) { + return e.taskName == parent; + })) { + throw new Error( + 'Parent task "' + parent + '" of "' + name + '" is not attached.' + ); + } + + queuedTasks.push({ + taskParent: parent, + taskName: name, + task: task + }); + + return self; + }; + + /** + * Removes a task. + * @param {string} v If v is undefined, then every tasks will + * be removed. If not, each task named v will + * be removed. + * @param {number} queueStatus Determines the queued tasks behaviour. If 0, + * then nothing will happen. If 1, the tasks + * queued to any removed task will be triggered. + * If 2, the tasks queued to any removed task + * will be removed as well. + * @return {sigma.chronos} Returns itself. + */ + function removeTask(v, queueStatus) { + if (v == undefined) { + tasks = []; + if (queueStatus == 1) { + queuedTasks = []; + }else if (queueStatus == 2) { + tasks = queuedTasks; + queuedTasks = []; + } + stopTasks(); + } else { + var n = (typeof v == 'string') ? v : ''; + tasks = tasks.filter(function(e) { + if ((typeof v == 'string') ? e.taskName == v : e.task == v) { + n = e.taskName; + return false; + } + return true; + }); + + if (queueStatus > 0) { + queuedTasks = queuedTasks.filter(function(e) { + if (queueStatus == 1 && e.taskParent == n) { + tasks.push(e); + } + return e.taskParent != n; + }); + } + } + + isRunning = !!(!tasks.length || (stopTasks() && false)); + return self; + }; + + /** + * A generator is a pair task/condition. The task will be executed + * while it returns true. + * When it returns false, the condition will be tested. If + * the condition returns true, the task will be executed + * again at the next process iteration. If not, the generator + * is removed. + * If several generators are present, they will be executed one + * by one: When the first stops, the second will start, etc. When + * they are all ended, then the conditions will be tested to know + * which generators have to be started again. + * @param {string} id The generators ID. + * @param {function(): boolean} task The generator's task. + * @param {function(): boolean} condition The generator's condition. + * @return {sigma.chronos} Returns itself. + */ + function addGenerator(id, task, condition) { + if (generators[id] != undefined) { + return self; + } + + generators[id] = { + task: task, + condition: condition + }; + + getGeneratorsCount(true) == 0 && startGenerators(); + return self; + }; + + /** + * Removes a generator. It means that the task will continue being eecuted + * until it returns false, but then the + * condition will not be tested. + * @param {string} id The generator's ID. + * @return {sigma.chronos} Returns itself. + */ + function removeGenerator(id) { + if (generators[id]) { + generators[id].on = false; + generators[id].del = true; + } + return self; + }; + + /** + * Returns the number of generators. + * @private + * @param {boolean} running If true, returns the number of active + * generators instead. + * @return {sigma.chronos} Returns itself. + */ + function getGeneratorsCount(running) { + return running ? + Object.keys(generators).filter(function(id) { + return !!generators[id].on; + }).length : + Object.keys(generators).length; + }; + + /** + * Returns the array of the generators IDs. + * @return {array.} The array of IDs. + */ + function getGeneratorsIDs() { + return Object.keys(generators); + } + + /** + * startGenerators is the method that manages which generator + * is the next to start when another one stops. It will dispatch + * a "stopgenerators" event if there is no more generator to start, + * and a "startgenerators" event else. + * @return {sigma.chronos} Returns itself. + */ + function startGenerators() { + if (!Object.keys(generators).length) { + self.dispatch('stopgenerators'); + }else { + self.dispatch('startgenerators'); + + self.unbind('killed', onTaskEnded); + insertFrame(function() { + for (var k in generators) { + generators[k].on = true; + addTask( + generators[k].task, + k, + false + ); + } + }); + + self.bind('killed', onTaskEnded).runTasks(); + } + + return self; + }; + + /** + * A callback triggered everytime the task of a generator stops, that will + * test the related generator's condition, and see if there is still any + * generator to start. + * @private + * @param {Object} e The sigma.chronos "killed" event. + */ + function onTaskEnded(e) { + if (generators[e['content'].taskName] != undefined) { + if (generators[e['content'].taskName].del || + !generators[e['content'].taskName].condition()) { + delete generators[e['content'].taskName]; + }else { + generators[e['content'].taskName].on = false; + } + + if (getGeneratorsCount(true) == 0) { + startGenerators(); + } + } + }; + + /** + * Either set or returns the fpsReq property. This property determines + * the number of frames that should be inserted per second. + * @param {?number} v The frequency asked. + * @return {(Chronos|number)} Returns the frequency if v is undefined, and + * itself else. + */ + function frequency(v) { + if (v != undefined) { + fpsReq = Math.abs(1 * v); + frameTime = 1000 / fpsReq; + framesCount = 0; + return self; + } else { + return fpsReq; + } + }; + + /** + * Returns the actual average number of frames that are inserted per + * second. + * @return {number} The actual average FPS. + */ + function getFPS() { + if (isRunning) { + lastFPS = + Math.round( + framesCount / + ((new Date()).getTime() - currentTime) * + 10000 + ) / 10; + } + + return lastFPS; + }; + + /** + * Returns the number of tasks. + * @return {number} The number of tasks. + */ + function getTasksCount() { + return tasks.length; + } + + /** + * Returns the number of queued tasks. + * @return {number} The number of queued tasks. + */ + function getQueuedTasksCount() { + return queuedTasks.length; + } + + /** + * Returns how long sigma.chronos has active tasks running + * without interuption for, in ms. + * @return {number} The time chronos is running without interuption for. + */ + function getExecutionTime() { + return startTime - currentTime; + } + + this.frequency = frequency; + + this.runTasks = runTasks; + this.stopTasks = stopTasks; + this.insertFrame = insertFrame; + + this.addTask = addTask; + this.queueTask = queueTask; + this.removeTask = removeTask; + + this.addGenerator = addGenerator; + this.removeGenerator = removeGenerator; + this.startGenerators = startGenerators; + this.getGeneratorsIDs = getGeneratorsIDs; + + this.getFPS = getFPS; + this.getTasksCount = getTasksCount; + this.getQueuedTasksCount = getQueuedTasksCount; + this.getExecutionTime = getExecutionTime; + + return this; +})(); + +sigma.publicPrototype = SigmaPublic.prototype; +})(); + + --- /dev/null +++ b/lib/Color.php @@ -1,1 +1,502 @@ - + + * + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * + * @version $Id$ + * + */ +class Lux_Color +{ + /** + * + * Converts hexadecimal colors to RGB. + * + * @param string $hex Hexadecimal value. Accepts values with 3 or 6 numbers, + * with or without #, e.g., CCC, #CCC, CCCCCC or #CCCCCC. + * + * @return array RGB values: 0 => R, 1 => G, 2 => B + * + */ + public function hex2rgb($hex) + { + // Remove #. + if (strpos($hex, '#') === 0) { + $hex = substr($hex, 1); + } + + if (strlen($hex) == 3) { + $hex .= $hex; + } + + if (strlen($hex) != 6) { + return false; + } + + // Convert each tuple to decimal. + $r = hexdec(substr($hex, 0, 2)); + $g = hexdec(substr($hex, 2, 2)); + $b = hexdec(substr($hex, 4, 2)); + + return array($r, $g, $b); + } + + /** + * + * Converts hexadecimal colors to HSV. + * + * @param string $hex Hexadecimal value. Accepts values with 3 or 6 numbers, + * with or without #, e.g., CCC, #CCC, CCCCCC or #CCCCCC. + * + * @return array HSV values: 0 => H, 1 => S, 2 => V + * + */ + public function hex2hsv($hex) + { + return $this->rgb2hsv($this->hex2rgb($hex)); + } + + /** + * + * Converts hexadecimal colors to HSL. + * + * @param string $hex Hexadecimal value. Accepts values with 3 or 6 numbers, + * with or without #, e.g., CCC, #CCC, CCCCCC or #CCCCCC. + * + * @return array HSL values: 0 => H, 1 => S, 2 => L + * + */ + public function hex2hsl($hex) + { + return $this->rgb2hsl($this->hex2rgb($hex)); + } + + /** + * + * Converts RGB colors to hexadecimal. + * + * @param array $rgb RGB values: 0 => R, 1 => G, 2 => B + * + * @return string Hexadecimal value with six digits, e.g., CCCCCC. + * + */ + public function rgb2hex($rgb) + { + if(count($rgb) < 3) { + return false; + } + + list($r, $g, $b) = $rgb; + + // From php.net. + $r = 0x10000 * max(0, min(255, $r)); + $g = 0x100 * max(0, min(255, $g)); + $b = max(0, min(255, $b)); + + return strtoupper(str_pad(dechex($r + $g + $b), 6, 0, STR_PAD_LEFT)); + } + + /** + * + * Converts RGB to HSV. + * + * @param array $rgb RGB values: 0 => R, 1 => G, 2 => B + * + * @return array HSV values: 0 => H, 1 => S, 2 => V + * + */ + public function rgb2hsv($rgb) + { + // RGB values = 0 ÷ 255 + $var_R = ($rgb[0] / 255); + $var_G = ($rgb[1] / 255); + $var_B = ($rgb[2] / 255); + + // Min. value of RGB + $var_Min = min($var_R, $var_G, $var_B); + + // Max. value of RGB + $var_Max = max($var_R, $var_G, $var_B); + + // Delta RGB value + $del_Max = $var_Max - $var_Min; + + $V = $var_Max; + + // This is a gray, no chroma... + if ( $del_Max == 0 ) { + // HSV results = 0 ÷ 1 + $H = 0; + $S = 0; + } else { + // Chromatic data... + $S = $del_Max / $var_Max; + + $del_R = ((($var_Max - $var_R) / 6) + ($del_Max / 2)) / $del_Max; + $del_G = ((($var_Max - $var_G) / 6) + ($del_Max / 2)) / $del_Max; + $del_B = ((($var_Max - $var_B) / 6) + ($del_Max / 2)) / $del_Max; + + if ($var_R == $var_Max) { + $H = $del_B - $del_G; + } else if ($var_G == $var_Max) { + $H = (1 / 3) + $del_R - $del_B; + } else if ($var_B == $var_Max) { + $H = (2 / 3) + $del_G - $del_R; + } + + if ($H < 0) { + $H += 1; + } + if ($H > 1) { + $H -= 1; + } + } + + // Returns agnostic values. + // Range will depend on the application: e.g. $H*360, $S*100, $V*100. + return array($H, $S, $V); + } + + /** + * + * Converts RGB to HSL. + * + * @param array $rgb RGB values: 0 => R, 1 => G, 2 => B + * + * @return array HSL values: 0 => H, 1 => S, 2 => L + * + */ + public function rgb2hsl($rgb) + { + // Where RGB values = 0 ÷ 255. + $var_R = $rgb[0] / 255; + $var_G = $rgb[1] / 255; + $var_B = $rgb[2] / 255; + + // Min. value of RGB + $var_Min = min($var_R, $var_G, $var_B); + // Max. value of RGB + $var_Max = max($var_R, $var_G, $var_B); + // Delta RGB value + $del_Max = $var_Max - $var_Min; + + $L = ($var_Max + $var_Min) / 2; + + if ( $del_Max == 0 ) { + // This is a gray, no chroma... + // HSL results = 0 ÷ 1 + $H = 0; + $S = 0; + } else { + // Chromatic data... + if ($L < 0.5) { + $S = $del_Max / ($var_Max + $var_Min); + } else { + $S = $del_Max / ( 2 - $var_Max - $var_Min ); + } + + $del_R = ((($var_Max - $var_R) / 6) + ($del_Max / 2)) / $del_Max; + $del_G = ((($var_Max - $var_G) / 6) + ($del_Max / 2)) / $del_Max; + $del_B = ((($var_Max - $var_B) / 6) + ($del_Max / 2)) / $del_Max; + + if ($var_R == $var_Max) { + $H = $del_B - $del_G; + } else if ($var_G == $var_Max) { + $H = ( 1 / 3 ) + $del_R - $del_B; + } else if ($var_B == $var_Max) { + $H = ( 2 / 3 ) + $del_G - $del_R; + } + + if ($H < 0) { + $H += 1; + } + if ($H > 1) { + $H -= 1; + } + } + + return array($H, $S, $L); + } + + /** + * + * Converts HSV colors to hexadecimal. + * + * @param array $hsv HSV values: 0 => H, 1 => S, 2 => V + * + * @return string Hexadecimal value with six digits, e.g., CCCCCC. + * + */ + public function hsv2hex($hsv) + { + return $this->rgb2hex($this->hsv2rgb($hsv)); + } + + /** + * + * Converts HSV to RGB. + * + * @param array $hsv HSV values: 0 => H, 1 => S, 2 => V + * + * @return array RGB values: 0 => R, 1 => G, 2 => B + * + */ + public function hsv2rgb($hsv) + { + $H = $hsv[0]; + $S = $hsv[1]; + $V = $hsv[2]; + + // HSV values = 0 ÷ 1 + if ($S == 0) { + $R = $V * 255; + $G = $V * 255; + $B = $V * 255; + } else { + $var_h = $H * 6; + // H must be < 1 + if ( $var_h == 6 ) { + $var_h = 0; + } + // Or ... $var_i = floor( $var_h ) + $var_i = floor( $var_h ); + $var_1 = $V * ( 1 - $S ); + $var_2 = $V * ( 1 - $S * ( $var_h - $var_i ) ); + $var_3 = $V * ( 1 - $S * ( 1 - ( $var_h - $var_i ) ) ); + + switch($var_i) { + case 0: + $var_r = $V; + $var_g = $var_3; + $var_b = $var_1; + break; + case 1: + $var_r = $var_2; + $var_g = $V; + $var_b = $var_1; + break; + case 2: + $var_r = $var_1; + $var_g = $V; + $var_b = $var_3; + break; + case 3: + $var_r = $var_1; + $var_g = $var_2; + $var_b = $V; + break; + case 4: + $var_r = $var_3; + $var_g = $var_1; + $var_b = $V; + break; + default: + $var_r = $V; + $var_g = $var_1; + $var_b = $var_2; + } + + //RGB results = 0 ÷ 255 + $R = $var_r * 255; + $G = $var_g * 255; + $B = $var_b * 255; + } + + return array($R, $G, $B); + } + + /** + * + * Converts HSV colors to HSL. + * + * @param array $hsv HSV values: 0 => H, 1 => S, 2 => V + * + * @return array HSL values: 0 => H, 1 => S, 2 => L + * + */ + public function hsv2hsl($hsv) + { + return $this->rgb2hsl($this->hsv2rgb($hsv)); + } + + /** + * + * Converts hexadecimal colors to HSL. + * + * @param array $hsl HSL values: 0 => H, 1 => S, 2 => L + * + * @return string Hexadecimal value. Accepts values with 3 or 6 numbers, + * with or without #, e.g., CCC, #CCC, CCCCCC or #CCCCCC. + * + */ + public function hsl2hex($hsl) + { + return $this->rgb2hex($this->hsl2rgb($hsl)); + } + + /** + * + * Converts HSL to RGB. + * + * @param array $hsv HSL values: 0 => H, 1 => S, 2 => L + * + * @return array RGB values: 0 => R, 1 => G, 2 => B + * + */ + public function hsl2rgb($hsl) + { + list($H, $S, $L) = $hsl; + + if ($S == 0) { + // HSL values = 0 ÷ 1 + // RGB results = 0 ÷ 255 + $R = $L * 255; + $G = $L * 255; + $B = $L * 255; + } else { + if ($L < 0.5) { + $var_2 = $L * (1 + $S); + } else { + $var_2 = ($L + $S) - ($S * $L); + } + + $var_1 = 2 * $L - $var_2; + + $R = 255 * $this->_hue2rgb($var_1, $var_2, $H + (1 / 3)); + $G = 255 * $this->_hue2rgb($var_1, $var_2, $H); + $B = 255 * $this->_hue2rgb($var_1, $var_2, $H - (1 / 3)); + } + + return array($R, $G, $B); + } + + /** + * + * Support method for hsl2rgb(): converts hue ro RGB. + * + * @param + * + * @param + * + * @param + * + * @return int + * + */ + protected function _hue2rgb($v1, $v2, $vH) + { + if ($vH < 0) { + $vH += 1; + } + + if ($vH > 1) { + $vH -= 1; + } + + if ((6 * $vH) < 1) { + return ($v1 + ($v2 - $v1) * 6 * $vH); + } + + if ((2 * $vH) < 1) { + return $v2; + } + + if ((3 * $vH) < 2) { + return ($v1 + ($v2 - $v1) * (( 2 / 3) - $vH) * 6); + } + + return $v1; + } + + /** + * + * Converts hexadecimal colors to HSL. + * + * @param array $hsl HSL values: 0 => H, 1 => S, 2 => L + * + * @return array HSV values: 0 => H, 1 => S, 2 => V + * + */ + public function hsl2hsv($hsl) + { + return $this->rgb2hsv($this->hsl2rgb($hsl)); + } + + /** + * + * Updates HSV values. + * + * @param array $hsv HSV values: 0 => H, 1 => S, 2 => V + * + * @param array $values Values to update: 0 => value to add to H (0 to 360), + * 1 and 2 => values to multiply S and V (0 to 100). Example: + * + * {{{code:php + * // Update saturation to 80% in the provided HSV. + * $hsv = array(120, 0.75, 0.75); + * $new_hsv = $color->updateHsv($hsv, array(null, 80, null)); + * }}} + * + */ + public function updateHsv($hsv, $values) + { + if (isset($values[0])) { + $hsv[0] = max(0, min(360, ($hsv[0] + $values[0]))); + } + + if (isset($values[1])) { + $hsv[1] = max(0, min(1, ($hsv[1] * ($values[1] / 100)))); + } + + if (isset($values[2])) { + $hsv[2] = max(0, min(1, ($hsv[2] * ($values[2] / 100)))); + } + + return $hsv; + } + + /** + * + * Updates HSL values. + * + * @param array $hsl HSL values: 0 => H, 1 => S, 2 => L + * + * @param array $values Values to update: 0 => value to add to H (0 to 360), + * 1 and 2 => values to multiply S and V (0 to 100). Example: + * + * {{{code:php + * // Update saturation to 80% in the provided HSL. + * $hsl = array(120, 0.75, 0.75); + * $new_hsl = $color->updateHsl($hsl, array(null, 80, null)); + * }}} + * + */ + public function updateHsl($hsl, $values) + { + if (isset($values[0])) { + $hsl[0] = max(0, min(360, ($hsl[0] + $values[0]))); + } + + if (isset($values[1])) { + $hsl[1] = max(0, min(1, ($hsl[1] * ($values[1] / 100)))); + } + + if (isset($values[2])) { + $hsl[2] = max(0, min(1, ($hsl[2] * ($values[2] / 100)))); + } + + return $hsl; + } +} --- a/lib/springy +++ /dev/null --- a/unimplemented/foundation.html +++ /dev/null @@ -1,137 +1,1 @@ - - - - - - - - - - - - - Welcome to Foundation - - - - - - - - - - - - - - - -
- -
-
-

Welcome to Foundation

-

This is version 2.1.4 released on December 19, 2011

-
-
-
- -
-
-

The Grid

- - -
-
-
-

This is a twelve column section in a row. Each of these includes a div.panel element so you can see where the columns are - it's not required at all for the grid.

-
-
-
-
-
-
-

Six columns

-
-
-
-
-

Six columns

-
-
-
-
-
-
-

Four columns

-
-
-
-
-

Four columns

-
-
-
-
-

Four columns

-
-
-
- -

Tabs

-
-
Simple Tab 1
-
Simple Tab 2
-
Simple Tab 3
-
- -
    -
  • This is simple tab 1's content. Pretty neat, huh?
  • -
  • This is simple tab 2's content. Now you see it!
  • -
  • This is simple tab 3's content. It's, you know...okay.
  • -
- -

Buttons

- -

Small Blue Button

-

Medium Blue Button

-

Large Blue Button

- -

Nice Blue Button

-

Nice Blue Button

-

Nice Blue Button

- -
- -
-

Getting Started

-

We're stoked you want to try Foundation! To get going, this file (index.html) includes some basic styles you can modify, play around with, or totally destroy to get going.

- -

Other Resources

-

Once you've exhausted the fun in this document, you should check out:

-
    -
  • Foundation Documentation
    Everything you need to know about using the framework.
  • -
  • Foundation on Github
    Latest code, issue reports, feature requests and more.
  • -
  • @foundationzurb
    Ping us on Twitter if you have questions. If you build something with this we'd love to see it (and send you a totally boss sticker).
  • -
-
-
- -
- - - - - - - - - - - - --- a/unimplemented/humans.txt +++ /dev/null @@ -1,8 +1,1 @@ -/* Foundation was made by ZURB, an interaction design and design strategy firm in Campbell, CA */ -/* zurb.com */ -/* humanstxt.org */ -/* SITE */ - Standards: HTML5, CSS3 - Components: jQuery, Orbit, Reveal - Software: Coda, Textmate, Git --- a/unimplemented/lastUpdated.php +++ /dev/null @@ -1,2 +1,1 @@ -for each agency, record when last changed (number of days too) and show a couple of URLs that were in that change --- a/unimplemented/validation.php +++ /dev/null --- a/unimplemented/webservers.php +++ /dev/null @@ -1,1 +1,1 @@ -for each agency, find a scrapped document and read the webserver off it + --- /dev/null +++ b/webserver.php @@ -1,1 +1,43 @@ + + namewebserveraccessiblity errors"; +$agenciesdb = $server->get_db('disclosr-agencies'); +$docsdb = $server->get_db('disclosr-documents'); +try { + $rows = $agenciesdb->get_view("app", "all", null, true)->rows; + + + if ($rows) { + foreach ($rows as $row) { + + echo "" . $row->value->name . ""; + if (isset($row->value->website)) { + try { + $website = $docsdb->get(md5($row->value->website)); + $serverParts = explode(" ",$website->web_server); + echo "" . $serverParts[0] . ""; + if (!isset($website->validation)) { + echo "?"; + } else { + if ($website->validation == "") { + echo "No error"; + } else { + echo "
" . str_replace("<", "<", $website->validation) . "
"; + } + } + } catch (SetteeRestClientException $e) { + // setteErrorHandler($e); + } + } + echo ""; + } + } +} catch (SetteeRestClientException $e) { + setteErrorHandler($e); +} +include_footer(); +?>