From: Maxious Date: Tue, 21 Aug 2012 01:20:16 +0000 Subject: Add Google Analytics X-Git-Url: https://maxious.lambdacomplex.org/git/?p=disclosr.git&a=commitdiff&h=135380b4c6e29e3408a49e524f2a16e15ea24e94 --- Add Google Analytics Former-commit-id: c64f37b2545054d1482676e5f04dd933bcd024b9 --- --- 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 --- a/about.php +++ b/about.php @@ -7,10 +7,9 @@

Lorem ipsum.

What is this?

-Disclosr is a project to monitor Australian Federal Government agencies +Disclo.gs is a project to monitor Australian Federal Government agencies compliance with their "proactive disclosure requirements". -OGRE (Open Government Realization Evaluation) is a ranking of compliance with these requirements. -Prometheus is the agent which polls agency websites to assess compliance. +

Open everything

All documents released CC-BY 3 AU --- /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"); @@ -22,6 +21,7 @@ if (isset($row->value->statistics->employees)) { $headers = array_unique(array_merge($headers, array_keys(object_to_array($row->value->statistics->employees)))); + } } } catch (SetteeRestClientException $e) { --- /dev/null +++ b/admin/exportScore.csv.php @@ -1,1 +1,73 @@ +get_db('disclosr-agencies'); +$format = "csv"; +//$format = "json"; +if (isset($_REQUEST['format'])) $format = $_REQUEST['format']; + +setlocale(LC_CTYPE, 'C'); + + $headers = Array(); + +$fp = fopen('php://output', 'w'); +if ($fp && $db) { + if ($format == "csv") { + header('Content-Type: text/csv; charset=utf-8'); + header('Content-Disposition: attachment; filename="export.score.' . date("c") . '.csv"'); + } + header('Pragma: no-cache'); + header('Expires: 0'); + + try { + $agencies = $db->get_view("score", "score", null, true)->rows; + //print_r($agencies); + $first = true; + if ($format == "json") { + echo '"data" : ['.PHP_EOL; + + } + foreach ($agencies as $agency) { + $agencyArray = object_to_array($agency->value); + if ($first) { + $headers = array_keys($agencyArray); +if ($format == "csv") { + fputcsv($fp, $headers); + } else if ($format == "json") { + echo '{ + "labels" : ["' . implode('","', $headers) . '"],'.PHP_EOL; + } + } + $row = Array(); + + foreach ($headers as $i => $fieldName) { + if (isset($agencyArray[$fieldName])) { + $row[] = $agencyArray[$fieldName]; + } else { + $row[] = ''; + } + } + if ($format == "csv") { + fputcsv($fp, array_values($row)); + } else if ($format == "json") { + if (!$first) echo ","; + echo '{"data" : [' . implode(",", array_values($row)) . '], "label": "'.$agency->value->name.'", "lines" : { "show" : true }, "points" : { "show" : true }}'.PHP_EOL; + + } + $first = false; + } + + if ($format == "json") { + echo '] + }'.PHP_EOL; + + } + } catch (SetteeRestClientException $e) { + setteErrorHandler($e); + } + + die; +} +?> + --- 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/getAgency.php +++ b/getAgency.php @@ -86,9 +86,14 @@ } else if ($schemas['agency']['properties'][$defaultField]['type'] == "array") { if (is_array($row[$defaultField])) { $row[$defaultField][] = ""; + $row[$defaultField][] = ""; + $row[$defaultField][] = ""; } else { $value = $row[$defaultField]; $row[$defaultField] = Array($value); + $row[$defaultField][] = ""; + $row[$defaultField][] = ""; + } } } @@ -102,7 +107,7 @@ // by name = startkey="Ham"&endkey="Ham\ufff0" // edit? - $row = $db->get($_REQUEST['id']); + $obj = $db->get($_REQUEST['id']); //print_r($row); if (sizeof($_POST) > 0) { //print_r($_POST); @@ -126,17 +131,19 @@ echo "Edited version was latest version, continue saving"; $newdoc = $_POST; $newdoc['metadata']['lastModified'] = time(); - $row = $db->save($newdoc); + $obj = $db->save($newdoc); } else { echo "ALERT doc revised by someone else while editing. Document not saved."; } } - $mode = "edit"; + $mode = "view"; + $rowArray = object_to_array($obj); +ksort($rowArray); if ($mode == "edit") { - $row = addDefaultFields(object_to_array($row)); + $row = addDefaultFields($rowArray); } else { - $row = object_to_array($row); + $row = $rowArray; } if ($mode == "view") { @@ -188,14 +195,14 @@ (isset($row->value->name) && $row->value->name != "" ? $row->value->name : "NO NAME " . $row->value->abn) . ''; } */ - $rows = $db->get_view("app", "byName")->rows; + $rows = $db->get_view("app", "byCanonicalName")->rows; //print_r($rows); echo '"; --- /dev/null +++ b/google676a414ad086cefb.html @@ -1,1 +1,2 @@ +google-site-verification: google676a414ad086cefb.html --- 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/common.inc.php +++ b/include/common.inc.php @@ -51,6 +51,9 @@ function phrase_to_tag ($phrase) { return str_replace(" ","_",str_replace("'","",str_replace(",","",strtolower($phrase)))); } +function local_url() { + return "http://" . $_SERVER['HTTP_HOST'] . rtrim(dirname($_SERVER['PHP_SELF']), '/\\') . "/"; +} function GetDomain($url) { $nowww = ereg_replace('www\.','',$url); --- 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,27 +95,31 @@ } }"; // http://stackoverflow.com/questions/646628/javascript-startswith - $obj->views->scoreHas->map = 'if(!String.prototype.startsWith){ + $obj->views->score->map = 'if(!String.prototype.startsWith){ String.prototype.startsWith = function (str) { return !this.indexOf(str); } } -if(!String.prototype.endsWith){ - String.prototype.endsWith = function(suffix) { -     return this.indexOf(suffix, this.length - suffix.length) !== -1; - }; -} + function(doc) { -if (typeof(doc["status"]) == "undefined" || doc["status"] != "suspended") { -for(var propName in doc) { - if(typeof(doc[propName]) != "undefined" && (propName.startsWith("has") || propName.endsWith("URL"))) { - emit(propName, 1); - } -} - emit("total", 1); - } + count = 0; + if (doc["status"] != "suspended") { + for(var propName in doc) { + if(typeof(doc[propName]) != "undefined" && doc[propName] != "") { + count++; + } + } + portfolio = doc.parentOrg; + if (doc.orgType == "FMA-DepartmentOfState") { + portfolio = doc._id; + } + if (doc.orgType == "Court-Commonwealth" || doc.orgType == "FMA-DepartmentOfParliament") { + portfolio = doc.orgType; + } + 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); } @@ -135,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); @@ -150,8 +157,7 @@ } 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") { @@ -159,7 +165,9 @@ } else { $serverAddr = 'http://127.0.0.1:5984/'; } - $server = new SetteeServer($serverAddr); +$server = new SetteeServer($serverAddr); + function setteErrorHandler($e) { echo $e->getMessage() . "
" . PHP_EOL; } + --- a/include/template.inc.php +++ b/include/template.inc.php @@ -71,9 +71,27 @@ + + --- /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, + labe