Commit e9f27795 authored by Romain Loth's avatar Romain Loth

politoscope data and fix gexf cats parsing

various experiments around gexf parsing: take into account declared atts, trace comments for parseCustom, declarable @cluster_index equivalent, and separate function in sigma_tools for using stock gexf parser (unconnected now)
parent a0aa844c
<?php <?php
# £TODO WTF ??? => move params to settings_explorerjs ???
$gexf_db = array(); $gexf_db = array();
# $gexf_db["data/terrorism/terrorism_bi.gexf"] = "data/terrorism/data.db"; # $gexf_db["data/terrorism/terrorism_bi.gexf"] = "data/terrorism/data.db";
...@@ -11,6 +13,10 @@ $gexf_db["data/AXA/RiskV2PageRank1000.gexf"] = "data/AXA/data.db"; ...@@ -11,6 +13,10 @@ $gexf_db["data/AXA/RiskV2PageRank1000.gexf"] = "data/AXA/data.db";
$gexf_db["data/AXA/RiskV2PageRank2500.gexf"] = "data/AXA/data.db"; $gexf_db["data/AXA/RiskV2PageRank2500.gexf"] = "data/AXA/data.db";
$gexf_db["data/AXA/RiskV2PageRank5000.gexf"] = "data/AXA/data.db"; $gexf_db["data/AXA/RiskV2PageRank5000.gexf"] = "data/AXA/data.db";
// TESTS
$gexf_db["data/ProgrammeDesCandidats.gexf"] = "foobar";
$gexf= str_replace('"','',$_GET["gexf"]); $gexf= str_replace('"','',$_GET["gexf"]);
$mainpath=dirname(getcwd())."/"; $mainpath=dirname(getcwd())."/";
......
{ {
"data/politoscope": {
"dbname":null,
"title":"Politoscope",
"date":"2017",
"abstract":"hello woerld",
"first" : "ProgrammeDesCandidats.gexf",
"gexfs": {
"ProgrammeDesCandidats.gexf": {
"social": {},
"semantic": {}
}
}
},
"data/AXA": { "data/AXA": {
"dbname":"data.db", "dbname":"data.db",
"title":"ISITITLE", "title":"ISITITLE",
"date":"ISIpubdate", "date":"ISIpubdate",
"abstract":"ISIABSTRACT", "abstract":"ISIABSTRACT",
"first" : "RiskV2PageRank1000.gexf", "first" : "RiskV2PageRank1000.gexf",
"gexfs": { "gexfs": {
"RiskV2PageRank1000.gexf": { "RiskV2PageRank1000.gexf": {
"social": { "table":"ISIAUTHOR" , "textCol":"data","forkeyCol":"id"}, "social": { "table":"ISIAUTHOR" , "textCol":"data","forkeyCol":"id"},
......
...@@ -587,10 +587,15 @@ ...@@ -587,10 +587,15 @@
<!-- new sigma 1.2 imports --> <!-- new sigma 1.2 imports -->
<script src="tinawebJS/sigma_v1.2/sigma.min.js" type="text/javascript" language="javascript"></script> <script src="tinawebJS/sigma_v1.2/sigma.min.js" type="text/javascript" language="javascript"></script>
<!-- <script src="tinawebJS/sigma_v1.2/sigma.js" type="text/javascript" language="javascript"></script> -->
<script src="tinawebJS/sigma_v1.2/plugins/sigma.layout.forceAtlas2/supervisor.js"></script> <script src="tinawebJS/sigma_v1.2/plugins/sigma.layout.forceAtlas2/supervisor.js"></script>
<script src="tinawebJS/sigma_v1.2/plugins/sigma.layout.forceAtlas2/worker.js"></script> <script src="tinawebJS/sigma_v1.2/plugins/sigma.layout.forceAtlas2/worker.js"></script>
<script src="tinawebJS/sigma_v1.2/plugins/sigma.renderers.snapshot/sigma.renderers.snapshot.js"></script> <script src="tinawebJS/sigma_v1.2/plugins/sigma.renderers.snapshot/sigma.renderers.snapshot.js"></script>
<!-- Tested as replacement for parseCustom -->
<!-- <script src="tinawebJS/sigma_v1.2/plugins/sigma.parsers.gexf/gexf-parser.js"></script> -->
<!-- <script src="tinawebJS/sigma_v1.2/plugins/sigma.parsers.gexf/sigma.parsers.gexf.js"></script> -->
<!-- testing sigma 1.5 imports from linkurious src --> <!-- testing sigma 1.5 imports from linkurious src -->
<!-- <script src="tinawebJS/sigma_v1.5/sigma.js" type="text/javascript" language="javascript"></script> <!-- <script src="tinawebJS/sigma_v1.5/sigma.js" type="text/javascript" language="javascript"></script>
<script src="tinawebJS/sigma_v1.5/plugins.js" type="text/javascript" language="javascript"></script> --> <script src="tinawebJS/sigma_v1.5/plugins.js" type="text/javascript" language="javascript"></script> -->
......
...@@ -21,14 +21,12 @@ function ChangeGraphAppearanceByAtt( manualflag ) { ...@@ -21,14 +21,12 @@ function ChangeGraphAppearanceByAtt( manualflag ) {
// Seeing all the possible attributes! // Seeing all the possible attributes!
var AttsDict = {} var AttsDict = {}
var Atts_2_Exclude = {} var Atts_2_Exclude = {}
var v_nodes = getVisibleNodes(); for (var j in TW.nodeIds) {
for (var i in v_nodes) { let nid = TW.nodeIds[j]
if(!v_nodes[i].hidden) { let n = TW.partialGraph.graph.nodes(nid)
if(!n.hidden) {
var id = v_nodes[i].id; for(var a in TW.Nodes[nid].attributes) {
var someatt = TW.Nodes[nid].attributes[a]
for(var a in TW.Nodes[id].attributes) {
var someatt = TW.Nodes[id].attributes[a]
// Identifying the attribute datatype: exclude strings and objects // Identifying the attribute datatype: exclude strings and objects
if ( ( typeof(someatt)=="string" && isNaN(Number(someatt)) ) || typeof(someatt)=="object" ) { if ( ( typeof(someatt)=="string" && isNaN(Number(someatt)) ) || typeof(someatt)=="object" ) {
...@@ -38,10 +36,10 @@ function ChangeGraphAppearanceByAtt( manualflag ) { ...@@ -38,10 +36,10 @@ function ChangeGraphAppearanceByAtt( manualflag ) {
} }
var possible_atts = []; var possible_atts = [];
if (!isUndef(TW.Nodes[id].attributes)) if (!isUndef(TW.Nodes[nid].attributes))
possible_atts = Object.keys(TW.Nodes[id].attributes) possible_atts = Object.keys(TW.Nodes[nid].attributes)
if(!isUndef(v_nodes[i].degree)) if(!isUndef(n.degree))
possible_atts.push("degree") possible_atts.push("degree")
possible_atts.push("clust_louvain") possible_atts.push("clust_louvain")
...@@ -240,10 +238,15 @@ function set_ClustersLegend ( daclass ) { ...@@ -240,10 +238,15 @@ function set_ClustersLegend ( daclass ) {
var Type = raw[0] var Type = raw[0]
var ClustType = raw[1] var ClustType = raw[1]
var ClustID = raw[2] var ClustID = raw[2]
var legTxt = "N/A"
if (TW.Clusters && TW.Clusters[Type] && TW.Clusters[Type][ClustType] && TW.Clusters[Type][ClustType][ClustID]) {
legTxt = TW.Clusters[Type][ClustType][ClustID]
}
var Color = ClustNB_CurrentColor[IDx] var Color = ClustNB_CurrentColor[IDx]
pr ( Color+" : "+ TW.Clusters[Type][ClustType][ClustID] ) // console.log ( Color+" : ", Type, ClustType, ClustID )
pr ( Color+" : "+ legTxt )
var ColorDiv = '<span style="background:'+Color+';"></span>' var ColorDiv = '<span style="background:'+Color+';"></span>'
LegendDiv += '<li onclick=\'SomeEffect("'+IDx+'")\'>'+ColorDiv+ TW.Clusters[Type][ClustType][ClustID]+"</li>"+"\n" LegendDiv += '<li onclick=\'SomeEffect("'+IDx+'")\'>'+ColorDiv+ legTxt+"</li>"+"\n"
} }
} else { } else {
for(var i in OrderedClustDicts) { for(var i in OrderedClustDicts) {
......
...@@ -101,7 +101,8 @@ var semanticConverged=false; ...@@ -101,7 +101,8 @@ var semanticConverged=false;
// ============ < / DEVELOPER OPTIONS > ============ // ============ < / DEVELOPER OPTIONS > ============
var showLabelsIfZoom=1.0; TW.nodeClusAtt = "modularity_class"
TW.edgeDefaultOpacity = 0.5 // opacity when true_color TW.edgeDefaultOpacity = 0.5 // opacity when true_color
TW.edgeGreyColor = "rgba(150, 150, 150, 0.2)"; TW.edgeGreyColor = "rgba(150, 150, 150, 0.2)";
TW.nodesGreyBorderColor = "rgba(100, 100, 100, 0.5)"; TW.nodesGreyBorderColor = "rgba(100, 100, 100, 0.5)";
...@@ -111,6 +112,7 @@ TW.selectedColor = "node" // "node" for a background like the node's color, ...@@ -111,6 +112,7 @@ TW.selectedColor = "node" // "node" for a background like the node's color,
TW.overSampling = true // costly hi-def rendering (true => pixelRatio x 2) TW.overSampling = true // costly hi-def rendering (true => pixelRatio x 2)
TW.deselectOnclickStage = false // will a click on the background remove selection ? (except when dragging) TW.deselectOnclickStage = false // will a click on the background remove selection ? (except when dragging)
var showLabelsIfZoom=1.0;
// ============ < / DEVELOPER OPTIONS > ============ // ============ < / DEVELOPER OPTIONS > ============
...@@ -164,7 +166,7 @@ var twjs="tinawebJS/"; ...@@ -164,7 +166,7 @@ var twjs="tinawebJS/";
TW.categories = {}; TW.categories = {};
TW.categoriesIndex = []; TW.categoriesIndex = [];
var gexf; var gexfFile;
//var zoom=0; //var zoom=0;
var checkBox=false; var checkBox=false;
......
...@@ -3,6 +3,18 @@ ...@@ -3,6 +3,18 @@
// always useful // always useful
var theHtml = document.getElementsByTagName('html')[0] var theHtml = document.getElementsByTagName('html')[0]
// overriding pixelRatio is possible if we need very high definition
// var realRatio = sigma.utils.getPixelRatio
// if (TW.overSampling) {
// sigma.utils.getPixelRatio = function() {
// return 2 * realRatio()
// }
// }
TW.anynodegoes = true
// TW.anynodegoes = true
//============================ < NEW BUTTONS > =============================// //============================ < NEW BUTTONS > =============================//
...@@ -449,6 +461,11 @@ function changeLevel() { ...@@ -449,6 +461,11 @@ function changeLevel() {
// NB new sigma js: dropEdge is quite slow so we add a waiting cursor // NB new sigma js: dropEdge is quite slow so we add a waiting cursor
function EdgeWeightFilter(sliderDivID , type_attrb , type , criteria) { function EdgeWeightFilter(sliderDivID , type_attrb , type , criteria) {
console.log("EdgeWeightFilter")
console.log("sliderDivID", sliderDivID)
console.log("type_attrb", type_attrb)
console.log("type", type)
console.log("criteria", criteria)
// if ($(sliderDivID).html()!="") { // if ($(sliderDivID).html()!="") {
// console.log("\t\t\t\t\t\t[[ algorithm not applied "+sliderDivID+" ]]") // console.log("\t\t\t\t\t\t[[ algorithm not applied "+sliderDivID+" ]]")
// return; // return;
...@@ -480,10 +497,16 @@ function EdgeWeightFilter(sliderDivID , type_attrb , type , criteria) { ...@@ -480,10 +497,16 @@ function EdgeWeightFilter(sliderDivID , type_attrb , type , criteria) {
var filterparams = AlgorithmForSliders ( TW.Edges , type_attrb , type , criteria) //OK var filterparams = AlgorithmForSliders ( TW.Edges , type_attrb , type , criteria) //OK
// TODO make an index // TODO make an index
// console.log("EdgeWeightFilter: "+type) console.log("EdgeWeightFilter: "+type)
// console.log(filterparams) console.log(filterparams)
var steps = filterparams["steps"] var steps = filterparams["steps"]
// TODO polito filterparams comes back like this {steps:0, finalarray:[]}
console.warn("overriding steps")
steps = 2
var finalarray = filterparams["finalarray"] var finalarray = filterparams["finalarray"]
// if(steps<3) { // if(steps<3) {
// $(sliderDivID).freshslider({ // $(sliderDivID).freshslider({
......
...@@ -34,7 +34,7 @@ var AjaxSync = (function(TYPE, URL, DATA, CT , DT) { ...@@ -34,7 +34,7 @@ var AjaxSync = (function(TYPE, URL, DATA, CT , DT) {
var Result = [] var Result = []
TYPE = (!TYPE)?"GET":"POST" TYPE = (!TYPE)?"GET":"POST"
if(DT && (DT=="jsonp" || DT=="json")) CT="application/json"; if(DT && (DT=="jsonp" || DT=="json")) CT="application/json";
// console.log(TYPE, URL, DATA, CT , DT) console.log("---AjaxSync---\n", TYPE, URL, DATA, CT , DT, "\n--------------")
$.ajax({ $.ajax({
type: TYPE, type: TYPE,
url: URL, url: URL,
...@@ -56,6 +56,7 @@ var AjaxSync = (function(TYPE, URL, DATA, CT , DT) { ...@@ -56,6 +56,7 @@ var AjaxSync = (function(TYPE, URL, DATA, CT , DT) {
Result = { "OK":true , "format":format , "data":data }; Result = { "OK":true , "format":format , "data":data };
}, },
error: function(exception) { error: function(exception) {
console.log('now error')
Result = { "OK":false , "format":false , "data":exception.status }; Result = { "OK":false , "format":false , "data":exception.status };
} }
}); });
...@@ -93,20 +94,27 @@ if(!isUndef(getUrlParam.mode)) { // if {db|api}.json ...@@ -93,20 +94,27 @@ if(!isUndef(getUrlParam.mode)) { // if {db|api}.json
// RES == { OK: true, format: "json", data: Object } // RES == { OK: true, format: "json", data: Object }
var RES = AjaxSync({ URL: file }); var RES = AjaxSync({ URL: file });
console.log('RES', RES)
if(RES["OK"]) { if(RES["OK"]) {
var fileparam;// = { db|api.json , somefile.json|gexf } var fileparam;// = { db|api.json , somefile.json|gexf }
var the_data = RES["data"]; var the_data = RES["data"];
// console.log('initial AjaxSync result RES', RES) console.log('initial AjaxSync result RES', RES)
// ===================
var the_file = ""; var the_file = "";
// ===================
if ( !isUndef(getUrlParam.mode) && getUrlParam.mode=="db.json") { if ( !isUndef(getUrlParam.mode) && getUrlParam.mode=="db.json") {
var first_file = "" , first_path = "" var first_file = "" , first_path = ""
for( var path in the_data ) { for( var path in the_data ) {
console.log("db.json path", path)
first_file = the_data[path]["first"] first_file = the_data[path]["first"]
first_path = path first_path = path
console.log("db.json first_file", first_path, first_file)
break; break;
} }
...@@ -116,24 +124,23 @@ if(RES["OK"]) { ...@@ -116,24 +124,23 @@ if(RES["OK"]) {
the_file = first_path+"/"+getUrlParam.file the_file = first_path+"/"+getUrlParam.file
} }
fileparam = the_file;
var files_selector = '<select onchange="jsActionOnGexfSelector(this.value , true);">' var files_selector = '<select onchange="jsActionOnGexfSelector(this.value , true);">'
for( var path in the_data ) { for( var path in the_data ) {
var the_gexfs = the_data[path]["gexfs"] var the_gexfs = the_data[path]["gexfs"]
console.log("\t\tThese are the available Gexfs:") console.log("\t\tThese are the available Gexfs:")
for(var gexf in the_gexfs) { console.log(the_gexfs)
var gexfBasename = gexf.replace(/\.gexf$/, "") // more human-readable in the menu for(var aGexf in the_gexfs) {
console.log("\t\t\t"+gexf+ " -> table:" +the_gexfs[gexf]["semantic"]["table"] ) var gexfBasename = aGexf.replace(/\.gexf$/, "") // more human-readable in the menu
console.log("\t\t\t"+gexfBasename+ " -> table:" +the_gexfs[aGexf]["semantic"]["table"] )
TW.field[path+"/"+gexf] = the_gexfs[gexf]["semantic"]["table"] TW.field[path+"/"+aGexf] = the_gexfs[aGexf]["semantic"]["table"]
// ex : data/AXA/RiskV2PageRank5000.gexf:"ISItermsAxa_2015" // ex : data/AXA/RiskV2PageRank5000.gexf:"ISItermsAxa_2015"
TW.gexfDict[path+"/"+gexf] = gexf TW.gexfDict[path+"/"+aGexf] = aGexf
// ex : data/AXA/RiskV2PageRank1000.gexf:"RiskV2PageRank1000.gexf" // ex : data/AXA/RiskV2PageRank1000.gexf:"RiskV2PageRank1000.gexf"
var selected = (the_file==(path+"/"+gexf))?"selected":"" var selected = (the_file==(path+"/"+aGexf))?"selected":""
files_selector += '<option '+selected+'>'+gexfBasename+'</option>' files_selector += '<option '+selected+'>'+gexfBasename+'</option>'
} }
// console.log( files_selector ) // console.log( files_selector )
...@@ -147,12 +154,15 @@ if(RES["OK"]) { ...@@ -147,12 +154,15 @@ if(RES["OK"]) {
// console.log("\n============================\n") // console.log("\n============================\n")
// console.log(TW.field) // console.log(TW.field)
// console.log(TW.gexfDict) // console.log(TW.gexfDict)
var sub_RES = AjaxSync({ URL: fileparam }); var sub_RES = AjaxSync({ URL: the_file });
the_data = sub_RES["data"] the_data = sub_RES["data"]
fileparam = sub_RES["format"] fileparam = sub_RES["format"]
// console.log(the_data.length) // console.log(the_data.length)
// console.log(fileparam) // console.log(fileparam)
console.warn('@the_file', sub_RES["OK"], the_file)
// why now ???
getUrlParam.file=the_file; getUrlParam.file=the_file;
console.log(" . .. . -. - .- . - -.") console.log(" . .. . -. - .- . - -.")
console.log(getUrlParam.file) console.log(getUrlParam.file)
...@@ -171,8 +181,8 @@ if(RES["OK"]) { ...@@ -171,8 +181,8 @@ if(RES["OK"]) {
console.log("parsing the data") console.log("parsing the data")
var start = new ParseCustom( fileparam , the_data ); var start = new ParseCustom( fileparam , the_data );
var categories = start.scanFile(); //user should choose the order of categories var categories = start.scanFile(); //user should choose the order of categories
// console.log("Categories: ") console.error("Categories: ")
// console.log(categories) console.log(categories)
if (! categories) { if (! categories) {
console.warn ('ParseCustom scanFile found no categories!!') console.warn ('ParseCustom scanFile found no categories!!')
...@@ -181,7 +191,11 @@ if(RES["OK"]) { ...@@ -181,7 +191,11 @@ if(RES["OK"]) {
var possibleStates = makeSystemStates( categories ) var possibleStates = makeSystemStates( categories )
var initialState = buildInitialState( categories ) //[true,false]// var initialState = buildInitialState( categories ) //[true,false]//
var dicts = start.makeDicts(categories); // XML parsing from ParseCustom
var dicts = start.makeDicts(categories); // > parseGexf, dictfyGexf
console.warn("parsing result:", dicts)
TW.Nodes = dicts.nodes; TW.Nodes = dicts.nodes;
TW.Edges = dicts.edges; TW.Edges = dicts.edges;
TW.nodeIds = Object.keys(dicts.nodes) // useful for loops TW.nodeIds = Object.keys(dicts.nodes) // useful for loops
...@@ -195,10 +209,12 @@ if(RES["OK"]) { ...@@ -195,10 +209,12 @@ if(RES["OK"]) {
if (the_data.clusters) TW.Clusters = the_data.clusters if (the_data.clusters) TW.Clusters = the_data.clusters
// relations already copied in TW.Relations at this point
// TW.nodes1 = dicts.n1;//not used // TW.nodes1 = dicts.n1;//not used
var catDict = dicts.catDict var catDict = dicts.catDict
// console.log("CategoriesDict: ") console.log("CategoriesDict: ")
// console.log(catDict) console.log(catDict)
TW.categoriesIndex = categories;//to_remove TW.categoriesIndex = categories;//to_remove
TW.catSoc = categories[0];//to_remove TW.catSoc = categories[0];//to_remove
...@@ -231,7 +247,40 @@ if(RES["OK"]) { ...@@ -231,7 +247,40 @@ if(RES["OK"]) {
// preparing the data and settings // preparing the data and settings
TW.graphData = {nodes: [], edges: []} TW.graphData = {nodes: [], edges: []}
TW.graphData = sigma_utils.FillGraph( initialState , catDict , dicts.nodes , dicts.edges , TW.graphData ); TW.graphData = sigma_utils.FillGraph( initialState , catDict , TW.Nodes , TW.Edges , TW.graphData );
// // ----------- TEST stock parse gexf and use nodes to replace TW's ---------
// var gexfData = gexf.fetch('data/politoscope/ProgrammeDesCandidats.gexf')
//
// TW.graphData = sigmaTools.myGexfParserReplacement(
// gexfData.nodes,
// gexfData.edges
// )
// console.log ('ex in TW.graphData.nodes[0]', TW.graphData.nodes[0])
//
// // our holey id-indexed arrays
// TW.Nodes = {}
// TW.Edges = {}
// TW.nodeIds = []
// TW.edgeIds = []
// for (var j in TW.graphData.nodes) {
// var nid = TW.graphData.nodes[j].id
// TW.Nodes[nid] = TW.graphData.nodes[j]
// TW.nodeIds.push(nid)
// }
// for (var i in TW.graphData.edges) {
// var eid = TW.graphData.edges[i].id
// TW.Edges[eid] = TW.graphData.edges[i]
// TW.edgeIds.push(eid)
// }
//
//
// // -------------------------------------------------------------------------
if (TW.graphData.nodes.length == 0) console.error("empty graph")
if (TW.graphData.edges.length == 0) console.error("no edges in graph")
// cf github.com/jacomyal/sigma.js/wiki/Settings // cf github.com/jacomyal/sigma.js/wiki/Settings
var customSettings = Object.assign( var customSettings = Object.assign(
...@@ -324,10 +373,17 @@ if(RES["OK"]) { ...@@ -324,10 +373,17 @@ if(RES["OK"]) {
TW.partialGraph.states[1] = TW.SystemStates; TW.partialGraph.states[1] = TW.SystemStates;
TW.partialGraph.states[1].categories = categories TW.partialGraph.states[1].categories = categories
TW.partialGraph.states[1].categoriesDict = catDict; TW.partialGraph.states[1].categoriesDict = catDict;
console.log("!? initialState => states[1].type")
TW.partialGraph.states[1].type = initialState; TW.partialGraph.states[1].type = initialState;
TW.partialGraph.states[1].LouvainFait = false; TW.partialGraph.states[1].LouvainFait = false;
// [ / Poblating the Sigma-Graph ] // [ / Poblating the Sigma-Graph ]
// ex called for new selections with args like:
// (undefined, undefined, [268], undefined)
// ^^^^
// why type not used (monopart? deprecated?)
TW.partialGraph.states[1].setState = (function( type , level , sels , oppos ) { TW.partialGraph.states[1].setState = (function( type , level , sels , oppos ) {
var bistate=false, typestring=false; var bistate=false, typestring=false;
console.log("IN THE SET STATE METHOD:") console.log("IN THE SET STATE METHOD:")
...@@ -342,7 +398,7 @@ if(RES["OK"]) { ...@@ -342,7 +398,7 @@ if(RES["OK"]) {
this.LouvainFait = false; this.LouvainFait = false;
console.log("") console.log("")
console.log(" % % % % % % % % % % ") console.log(" % % % % % % % % % % ")
// console.log("type: "+thetype.map(Number)); console.log("setState type: ", type);
console.log("bistate: "+bistate) console.log("bistate: "+bistate)
console.log("level: "+level); console.log("level: "+level);
console.log("selections: "); console.log("selections: ");
...@@ -476,7 +532,12 @@ if(RES["OK"]) { ...@@ -476,7 +532,12 @@ if(RES["OK"]) {
// } // }
} }
ChangeGraphAppearanceByAtt(true) try {
ChangeGraphAppearanceByAtt(true)
}
catch (e) {
console.error(e)
}
set_ClustersLegend ( "clust_default" ) set_ClustersLegend ( "clust_default" )
......
...@@ -9,8 +9,8 @@ ParseCustom = function ( format , data ) { ...@@ -9,8 +9,8 @@ ParseCustom = function ( format , data ) {
this.nbCats = 0; this.nbCats = 0;
// input = GEXFstring // input = GEXFstring
this.getGEXFCategories = function(gexf) { this.getGEXFCategories = function(aGexfFile) {
this.data = $.parseXML(gexf) this.data = $.parseXML(aGexfFile) // <===================== (XML parse)
return scanGexf( this.data ); return scanGexf( this.data );
}// output = [ "cat1" , "cat2" , ...] }// output = [ "cat1" , "cat2" , ...]
...@@ -86,26 +86,125 @@ ParseCustom.prototype.makeDicts = function(categories) { ...@@ -86,26 +86,125 @@ ParseCustom.prototype.makeDicts = function(categories) {
}; };
function gexfCheckAttributesMap (someXMLContent) {
// excerpt from targeted XML:
// <graph defaultedgetype="undirected" mode="static">
// | <attributes class="node" mode="static">
// | <attribute id="0" title="category" type="string"></attribute>
// | <attribute id="1" title="country" type="float"></attribute>
// | </attributes>
// (...)
// THIS SEGMENT USED TO BE IN dictifyGexf
// Census of the conversions between attr and some attr name
var i, j, k;
var nodesAttributes = []; // The list of attributes of the nodes of the graph that we build in json
var edgesAttributes = []; // The list of attributes of the edges of the graph that we build in json
// In the gexf (that is an xml), the list of xml nodes 'attributes' (note the plural 's')
var attributesNodes = someXMLContent.getElementsByTagName('attributes');
for(i = 0; i<attributesNodes.length; i++){
var attributesNode = attributesNodes[i]; // attributesNode is each xml node 'attributes' (plural)
if(attributesNode.getAttribute('class') == 'node'){
var attributeNodes = attributesNode.getElementsByTagName('attribute'); // The list of xml nodes 'attribute' (no 's')
for(j = 0; j<attributeNodes.length; j++){
var attributeNode = attributeNodes[j]; // Each xml node 'attribute'
var id = attributeNode.getAttribute('id'),
title = attributeNode.getAttribute('title'),
type = attributeNode.getAttribute('type');
// ex: id = "in-degree" or "3" <= can be an int to be resolved into the title
// ex: title = "in-degree"
// ex: type = "string"
var attribute = {
id:id,
title:title,
type:type
};
nodesAttributes.push(attribute);
}
} else if(attributesNode.getAttribute('class') == 'edge'){
var attributeNodes = attributesNode.getElementsByTagName('attribute'); // The list of xml nodes 'attribute' (no 's')
for(j = 0; j<attributeNodes.length; j++){
var attributeNode = attributeNodes[j]; // Each xml node 'attribute'
var id = attributeNode.getAttribute('id'),
title = attributeNode.getAttribute('title'),
type = attributeNode.getAttribute('type');
var attribute = {
id:id,
title:title,
type:type
};
edgesAttributes.push(attribute);
}
}
} //out: nodesAttributes Array
console.debug('>>> tr: nodesAttributes', nodesAttributes)
console.debug('>>> tr: edgesAttributes', edgesAttributes)
return {nAttrs: nodesAttributes, eAttrs: edgesAttributes}
}
// Level-00 // Level-00
function scanGexf(gexf) { function scanGexf(gexfContent) {
console.log("ParseCustom : scanGexf") console.log("ParseCustom : scanGexf ======= ")
var categoriesDict={}, categories=[]; var categoriesDict={}, categories=[];
nodesNodes = gexf.getElementsByTagName('nodes');
for(i=0; i<nodesNodes.length; i++){
var nodesNode = nodesNodes[i]; // Each xml node 'nodes' (plural) // adding gexfCheckAttributesMap call
node = nodesNode.getElementsByTagName('node'); // to create a map from nodes/node/@for values to declared attribute name (title)
var declaredAttrs = gexfCheckAttributesMap(gexfContent)
elsNodes = gexfContent.getElementsByTagName('nodes');
// console.debug('>>> tr: elsNodes', elsNodes) // <<<
for(i=0; i<elsNodes.length; i++){
var elNodes = elsNodes[i]; // Each xml node 'nodes' (plural)
node = elNodes.getElementsByTagName('node');
for(j=0; j<node.length; j++){ for(j=0; j<node.length; j++){
attvalueNodes = node[j].getElementsByTagName('attvalue'); attvalueNodes = node[j].getElementsByTagName('attvalue');
for(k=0; k<attvalueNodes.length; k++){ for(k=0; k<attvalueNodes.length; k++){
attvalueNode = attvalueNodes[k]; attvalueNode = attvalueNodes[k];
attr = attvalueNode.getAttribute('for'); attr = attvalueNode.getAttribute('for');
val = attvalueNode.getAttribute('value'); val = attvalueNode.getAttribute('value');
// some attrs are gexf-local indices refering to an <attributes> declaration
// so if it matches declared we translate their integer in title
// FIXME use a dict by id in gexfCheckAttributesMap for loop rm
if(Number.isInteger(Number(attr))) {
// mini loop inside declared node attrs (eg substitute 0 for 'centrality')
for (var l=0;l<declaredAttrs.nAttrs.length;l++) {
let declared = declaredAttrs.nAttrs[l]
if (declared.id == attr) {
attr = declared.title
}
}
}
// console.log('attr', attr)
// THIS WILL BECOME catDict (if ncats == 1 => monopart)
if (attr=="category") categoriesDict[val]=val; if (attr=="category") categoriesDict[val]=val;
} }
} }
} }
for(var cat in categoriesDict) for(var cat in categoriesDict)
// usually a just a few cats over entire node set
// ex: terms
// ex: ISItermsriskV2_140 & ISItermsriskV2_140
console.debug('>>> tr: cat', cat)
categories.push(cat); categories.push(cat);
var catDict = {} var catDict = {}
...@@ -138,9 +237,11 @@ function scanGexf(gexf) { ...@@ -138,9 +237,11 @@ function scanGexf(gexf) {
// for {1,2}partite graphs // for {1,2}partite graphs
function dictfyGexf( gexf , categories ){ function dictfyGexf( gexf , categories ){
console.log("ParseCustom gexf 2nd loop, main data extraction") console.log("ParseCustom gexf 2nd loop, main data extraction, with categories", categories)
// var catDict = {'terms':"0"}
var catDict = {} var catDict = {}
var catCount = {} var catCount = {}
for(var i in categories) catDict[categories[i]] = i; for(var i in categories) catDict[categories[i]] = i;
...@@ -150,51 +251,17 @@ function dictfyGexf( gexf , categories ){ ...@@ -150,51 +251,17 @@ function dictfyGexf( gexf , categories ){
nodes2={}, bipartiteD2N={}, bipartiteN2D={} nodes2={}, bipartiteD2N={}, bipartiteN2D={}
} }
var i, j, k; // --------------------8<-----------------------
var nodesAttributes = []; // The list of attributes of the nodes of the graph that we build in json // HERE REMOVED XML <attributes> parsing
var edgesAttributes = []; // The list of attributes of the edges of the graph that we build in json // b/c a priori useless at this point ?
var attributesNodes = gexf.getElementsByTagName('attributes'); // In the gexf (that is an xml), the list of xml nodes 'attributes' (note the plural 's') // (moved earlier to scanGexf)
//
for(i = 0; i<attributesNodes.length; i++){ // var atts = gexfCheckAttributesMap(gexf)
var attributesNode = attributesNodes[i]; // attributesNode is each xml node 'attributes' (plural) // var nodesAttributes = atts.nAttrs
if(attributesNode.getAttribute('class') == 'node'){ // var edgesAttributes = atts.eAttrs
var attributeNodes = attributesNode.getElementsByTagName('attribute'); // The list of xml nodes 'attribute' (no 's') // --------------------8<-----------------------
for(j = 0; j<attributeNodes.length; j++){
var attributeNode = attributeNodes[j]; // Each xml node 'attribute'
var id = attributeNode.getAttribute('id'),
title = attributeNode.getAttribute('title'),
type = attributeNode.getAttribute('type');
var attribute = {
id:id,
title:title,
type:type
};
nodesAttributes.push(attribute);
}
} else if(attributesNode.getAttribute('class') == 'edge'){
var attributeNodes = attributesNode.getElementsByTagName('attribute'); // The list of xml nodes 'attribute' (no 's')
for(j = 0; j<attributeNodes.length; j++){
var attributeNode = attributeNodes[j]; // Each xml node 'attribute'
var id = attributeNode.getAttribute('id'),
title = attributeNode.getAttribute('title'),
type = attributeNode.getAttribute('type');
var attribute = {
id:id,
title:title,
type:type
};
edgesAttributes.push(attribute);
}
}
} //out: nodesAttributes Array
var nodesNodes = gexf.getElementsByTagName('nodes') // The list of xml nodes 'nodes' (plural) var elsNodes = gexf.getElementsByTagName('nodes') // The list of xml nodes 'nodes' (plural)
labels = []; labels = [];
minNodeSize=999.00; minNodeSize=999.00;
maxNodeSize=0.001; maxNodeSize=0.001;
...@@ -206,25 +273,30 @@ function dictfyGexf( gexf , categories ){ ...@@ -206,25 +273,30 @@ function dictfyGexf( gexf , categories ){
// let sumSizes = 0 // let sumSizes = 0
// let sizeStats = {'mean':null, 'median':null, 'max':0, 'min':1000000000} // let sizeStats = {'mean':null, 'median':null, 'max':0, 'min':1000000000}
for(i=0; i<nodesNodes.length; i++) { // usually there is only 1 <nodes> element...
var nodesNode = nodesNodes[i]; // Each xml node 'nodes' (plural) for(i=0; i<elsNodes.length; i++) {
var nodeNodes = nodesNode.getElementsByTagName('node'); // The list of xml nodes 'node' (no 's') var elNodes = elsNodes[i]; // Each xml element 'nodes' (plural)
var elsNode = elNodes.getElementsByTagName('node'); // The list of xml nodes 'node' (no 's')
for(j=0; j<elsNode.length; j++) {
for(j=0; j<nodeNodes.length; j++) { var elNode = elsNode[j]; // Each xml node 'node' (no 's')
var nodeNode = nodeNodes[j]; // Each xml node 'node' (no 's') // window.NODE = elNode;
window.NODE = nodeNode; if (j == 0) {
console.debug('>>> tr: XML nodes/node (1 of'+elsNode.length+')', elNodes)
}
// [ get ID ] // [ get ID ]
var id = nodeNode.getAttribute('id'); var id = elNode.getAttribute('id');
// [ get Label ] // [ get Label ]
var label = nodeNode.getAttribute('label') || id; var label = elNode.getAttribute('label') || id;
// [ get Size ] // [ get Size ]
var size=false; var size=false;
sizeNodes = nodeNode.getElementsByTagName('size'); sizeNodes = elNode.getElementsByTagName('size');
sizeNodes = sizeNodes.length ? sizeNodes : nodeNode.getElementsByTagName('viz:size'); sizeNodes = sizeNodes.length ? sizeNodes : elNode.getElementsByTagName('viz:size');
if(sizeNodes.length>0){ if(sizeNodes.length>0){
sizeNode = sizeNodes[0]; sizeNode = sizeNodes[0];
size = parseFloat(sizeNode.getAttribute('value')); size = parseFloat(sizeNode.getAttribute('value'));
...@@ -237,12 +309,13 @@ function dictfyGexf( gexf , categories ){ ...@@ -237,12 +309,13 @@ function dictfyGexf( gexf , categories ){
// -------------------------------------------- // --------------------------------------------
}// [ / get Size ] }// [ / get Size ]
// console.debug('>>> tr: node size', size)
// [ get Coordinates ] // [ get Coordinates ]
var x = 100 - 200*Math.random(); var x = 100 - 200*Math.random();
var y = 100 - 200*Math.random(); var y = 100 - 200*Math.random();
var positionNodes = nodeNode.getElementsByTagName('position'); var positionNodes = elNode.getElementsByTagName('position');
positionNodes = positionNodes.length ? positionNodes : nodeNode.getElementsByTagNameNS('*','position'); positionNodes = positionNodes.length ? positionNodes : elNode.getElementsByTagNameNS('*','position');
if(positionNodes.length>0){ if(positionNodes.length>0){
var positionNode = positionNodes[0]; var positionNode = positionNodes[0];
x = parseFloat(positionNode.getAttribute('x')); x = parseFloat(positionNode.getAttribute('x'));
...@@ -252,8 +325,8 @@ function dictfyGexf( gexf , categories ){ ...@@ -252,8 +325,8 @@ function dictfyGexf( gexf , categories ){
y = y*-1 // aka -y y = y*-1 // aka -y
// [ get Colour ] // [ get Colour ]
var colorNodes = nodeNode.getElementsByTagName('color'); var colorNodes = elNode.getElementsByTagName('color');
colorNodes = colorNodes.length ? colorNodes : nodeNode.getElementsByTagNameNS('*','color'); colorNodes = colorNodes.length ? colorNodes : elNode.getElementsByTagNameNS('*','color');
var color; var color;
if(colorNodes.length>0){ if(colorNodes.length>0){
colorNode = colorNodes[0]; colorNode = colorNodes[0];
...@@ -271,35 +344,61 @@ function dictfyGexf( gexf , categories ){ ...@@ -271,35 +344,61 @@ function dictfyGexf( gexf , categories ){
color:color color:color
}); });
// console.debug('>>> tr: read node', node)
// Attribute values // Attribute values
var attributes = [] var attributes = []
var attvalueNodes = nodeNode.getElementsByTagName('attvalue'); var attvalueNodes = elNode.getElementsByTagName('attvalue');
var atts={}; var atts={};
for(k=0; k<attvalueNodes.length; k++){ for(k=0; k<attvalueNodes.length; k++){
var attvalueNode = attvalueNodes[k]; var attvalueNode = attvalueNodes[k];
var attr = attvalueNode.getAttribute('for'); var attr = attvalueNode.getAttribute('for');
var val = attvalueNode.getAttribute('value'); var val = attvalueNode.getAttribute('value');
// TODO use here nodesAttributes
if(catDict[val]) atts["category"] = val; if(catDict[val]) atts["category"] = val;
else atts[attr]=val; else atts[attr]=val;
attributes = atts; attributes = atts;
} }
// nodew=parseInt(attributes["weight"]); // nodew=parseInt(attributes["weight"]);
if ( attributes["category"] ) { if ( attributes["category"] ) {
node_cat = attributes["category"]; node_cat = attributes["category"];
node.type = node_cat; }
if (!catCount[node_cat]) catCount[node_cat] = 0 else {
catCount[node_cat]++; node_cat = 0 // basic TW node type is 0 (~ terms)
}
// node.id = (node_cat==categories[0])? ("D:"+node.id) : ("N:"+node.id); node.type = node_cat;
if(!node.size) console.log("node without size: "+node.id+" : "+node.label); if (!catCount[node_cat]) catCount[node_cat] = 0
catCount[node_cat]++;
node.attributes = attributes; // node.id = (node_cat==categories[0])? ("D:"+node.id) : ("N:"+node.id);
nodes[node.id] = node if(!node.size) console.log("node without size: "+node.id+" : "+node.label);
node.attributes = attributes;
// console.log(node) // save record
} nodes[node.id] = node
if(parseInt(node.size) < parseInt(minNodeSize)) if(parseInt(node.size) < parseInt(minNodeSize))
minNodeSize= node.size; minNodeSize= node.size;
...@@ -310,6 +409,8 @@ function dictfyGexf( gexf , categories ){ ...@@ -310,6 +409,8 @@ function dictfyGexf( gexf , categories ){
} }
} }
console.warn ('parseCustom output nodes', nodes)
// -------------- debug: for local stats ---------------- // -------------- debug: for local stats ----------------
// allSizes.sort(); // allSizes.sort();
// let N = allSizes.length // let N = allSizes.length
...@@ -336,6 +437,9 @@ function dictfyGexf( gexf , categories ){ ...@@ -336,6 +437,9 @@ function dictfyGexf( gexf , categories ){
if(attention) { if(attention) {
var t_type = nodes[it].type var t_type = nodes[it].type
var t_cnumber = nodes[it].attributes["cluster_index"] var t_cnumber = nodes[it].attributes["cluster_index"]
if (!t_cnumber) {
t_cnumber = nodes[it].attributes[TW.nodeClusAtt]
}
nodes[it].attributes["clust_default"] = t_cnumber; nodes[it].attributes["clust_default"] = t_cnumber;
var t_label = (nodes[it].attributes["cluster_label"])?nodes[it].attributes["cluster_label"]:"cluster_"+nodes[it].attributes["cluster_index"] var t_label = (nodes[it].attributes["cluster_label"])?nodes[it].attributes["cluster_label"]:"cluster_"+nodes[it].attributes["cluster_index"]
if(!TW.Clusters[t_type]) { if(!TW.Clusters[t_type]) {
...@@ -385,7 +489,10 @@ function dictfyGexf( gexf , categories ){ ...@@ -385,7 +489,10 @@ function dictfyGexf( gexf , categories ){
}); });
} }
// console.debug('>>> tr: read edge', edge)
if ( nodes[source] && nodes[target] ) { if ( nodes[source] && nodes[target] ) {
console.debug('>>> tr: new edge has matching source and target nodes')
idS=nodes[source].type; idS=nodes[source].type;
idT=nodes[target].type; idT=nodes[target].type;
...@@ -518,12 +625,15 @@ function dictfyGexf( gexf , categories ){ ...@@ -518,12 +625,15 @@ function dictfyGexf( gexf , categories ){
} }
} }
// ------------------------------- resDict <<<
resDict = {} resDict = {}
resDict.catDict = catDict; // TODO unify catDict and catCount (dict is count.keys())
resDict.catCount = catCount; resDict.catDict = catDict; // ex : {'ISIterms':0}
resDict.nodes = nodes; resDict.catCount = catCount; // ex: {'ISIterms':1877} ie #nodes
resDict.nodes = nodes; // { nid1: {label:"...", size:"11.1", attributes:"...", color:"#aaa", etc}, nid2: ...}
resDict.edges = edges; resDict.edges = edges;
resDict.n1 = nodes1; resDict.n1 = nodes1; // relations
if(nodes2) resDict.n2 = nodes2; if(nodes2) resDict.n2 = nodes2;
if(bipartiteD2N) resDict.D2N = bipartiteD2N; if(bipartiteD2N) resDict.D2N = bipartiteD2N;
if(bipartiteN2D) resDict.N2D = bipartiteN2D; if(bipartiteN2D) resDict.N2D = bipartiteN2D;
......
...@@ -7,11 +7,14 @@ SigmaUtils = function () { ...@@ -7,11 +7,14 @@ SigmaUtils = function () {
this.FillGraph = function( initialState , catDict , nodes, edges , graph ) { this.FillGraph = function( initialState , catDict , nodes, edges , graph ) {
console.log("Filling the graaaaph:") console.log("Filling the graaaaph:")
// console.log(catDict) console.log("FillGraph catDict",catDict)
console.log("FillGraph nodes",nodes)
console.log("FillGraph edges",edges)
for(var i in nodes) { for(var i in nodes) {
var n = nodes[i]; var n = nodes[i];
// console.debug('tr >>> fgr node', n)
if(initialState[catDict[n.type]]) { if(initialState[catDict[n.type]] || TW.anynodegoes) {
// var node = { // var node = {
// id : n.id, // id : n.id,
// label : n.label, // label : n.label,
...@@ -30,7 +33,7 @@ SigmaUtils = function () { ...@@ -30,7 +33,7 @@ SigmaUtils = function () {
// no attributes to remove: I use n directly // no attributes to remove: I use n directly
graph.nodes.push(n); graph.nodes.push(n);
if(Number(n.id)==287) console.log("node 287:", n) if(i==2) console.log("node 2 ("+n.id+")", n)
// fill the "labels" global variable // fill the "labels" global variable
updateSearchLabels( n.id , n.label , n.type); updateSearchLabels( n.id , n.label , n.type);
......
...@@ -5,6 +5,71 @@ var sigmaTools = {}; ...@@ -5,6 +5,71 @@ var sigmaTools = {};
sigmaTools = (function(stools) { sigmaTools = (function(stools) {
// a simpler alternative to avoid parseCustom and prepareNodesRenderingProperties
// temporarily after using the new gexf stock parser
// (mostly to transform viz: attrs in real attrs)
stools.myGexfParserReplacement = function(rawGexfNodes, rawGexfEdges) {
// output, indexed by IDs
var newNodes = [],
newEdges = []
for (var j in rawGexfNodes) {
var rawNode = rawGexfNodes[j]
// pre-parse node rgb color
var rgbStr = rawNode.viz.color.match(/rgb\(([^\(]+)\)/)[1]
var newNode = {
id: rawNode.id,
label: rawNode.label,
x: rawNode.viz.position.x,
y: rawNode.viz.position.y,
color: rawNode.viz.color,
size: Math.round(rawNode.viz.size*1000)/1000,
active: false,
hidden: false,
customAttrs: {
grey: false,
highlight: false,
true_color: rawNode.viz.color,
defgrey_color : "rgba("+rgbStr+",.4)"
},
// for metrics like centrality
attributes: {
'clust_default': rawNode.attributes.modularity_class
}
}
newNodes[j] = newNode
}
for (var i in rawGexfEdges) {
var rawEdge = rawGexfEdges[i]
var rgbStr = sigmaTools.edgeRGB(rawEdge.source, rawEdge.target, newNodes)
var leColor = "rgba("+rgbStr+","+TW.edgeDefaultOpacity+")"
var newEid = rawEdge.source+";"+rawEdge.target;
var newEdge = {
id: newEid,
source: rawEdge.source,
target: rawEdge.target,
color: leColor,
weight: Math.round(rawEdge.weight*1000)/1000,
customAttrs: {
grey: false,
activeEdge: false,
true_color: leColor,
rgb: rgbStr
}
}
newEdges[i] = newEdge
}
return {nodes: newNodes, edges: newEdges}
};
stools.rgbToHex = function(R, G, B) { stools.rgbToHex = function(R, G, B) {
return stools.toHex(R) + stools.toHex(G) + stools.toHex(B); return stools.toHex(R) + stools.toHex(G) + stools.toHex(B);
}; };
...@@ -21,26 +86,49 @@ sigmaTools = (function(stools) { ...@@ -21,26 +86,49 @@ sigmaTools = (function(stools) {
}; };
// TODO check duplicate functionalities with repaintEdges // TODO check duplicate functionalities with repaintEdges
stools.edgeRGB = function(src_id, tgt_id) { stools.edgeRGB = function(src_id, tgt_id, nodeIndex) {
//edge color will be the combination of the 2 node colors
var a = TW.Nodes[src_id]['color']; if (!nodeIndex) {
var b = TW.Nodes[tgt_id]['color']; nodeIndex = TW.Nodes
}
if (!Object.keys(nodeIndex).length) {
console.warn('empty nodeIndex')
}
// console.log('edgeRGB, src_id', src_id)
// console.log('edgeRGB, tgt_id', tgt_id)
//
if (!nodeIndex[src_id] || !nodeIndex[tgt_id]) {
return '0,0,0'
}
//edge color will be the combination of the 2 node colors
var a = nodeIndex[src_id]['color'];
var b = nodeIndex[tgt_id]['color'];
var tmp
// console.log("color a", a) // console.log("color a", a)
// console.log("color b", b) // console.log("color b", b)
if(a.charAt(0)!="#") { if(a.charAt(0)!="#") {
tmp = a.replace("rgba(","").replace(")","").split(",") tmp = a.replace(/rgba?\(/,"").replace(")","").split(",")
a = stools.rgbToHex( parseFloat( tmp[0] ) , parseFloat( tmp[1] ) , parseFloat( tmp[2] ) );
// rgb array
a = [parseFloat( tmp[0] ) , parseFloat( tmp[1] ) , parseFloat( tmp[2] )];
}
else {
a = hex2rga(a);
} }
if(b.charAt(0)!="#") { if(b.charAt(0)!="#") {
tmp = b.replace("rgba(","").replace(")","").split(",") tmp = b.replace(/rgba?\(/,"").replace(")","").split(",")
b = stools.rgbToHex( parseFloat( tmp[0] ) , parseFloat( tmp[1] ) , parseFloat( tmp[2] ) ); b = [parseFloat( tmp[0] ) , parseFloat( tmp[1] ) , parseFloat( tmp[2] )];
}
else {
b = hex2rga(b);
} }
// console.log(source+" : "+a+"\t|\t"+target+" : "+b) // console.log(source+" : "+a+"\t|\t"+target+" : "+b)
a = hex2rga(a);
b = hex2rga(b);
var r = (a[0] + b[0]) >> 1; var r = (a[0] + b[0]) >> 1;
var g = (a[1] + b[1]) >> 1; var g = (a[1] + b[1]) >> 1;
var b = (a[2] + b[2]) >> 1; var b = (a[2] + b[2]) >> 1;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment