1 module tagion.tools.graphview; 2 import std.algorithm : min; 3 import std.array : join; 4 import std.conv : to; 5 import std.file : exists, fwrite = write; 6 import std.format; 7 import std.functional : toDelegate; 8 import std.getopt; 9 import std.outbuffer; 10 import std.path : extension; 11 import std.traits : EnumMembers; 12 import std.traits : isIntegral; 13 import tagion.basic.basic : EnumText; 14 import tagion.hashgraphview.EventView : EventView; 15 import tagion.hibon.Document : Document; 16 import tagion.hibon.HiBONJSON; 17 import tagion.hibon.HiBONValid : error_callback; 18 import tagion.hibon.HiBONFile : fread; 19 import tagion.tools.revision; 20 import tagion.utils.BitMask; 21 22 protected enum _params = [ 23 "events", 24 "size", 25 ]; 26 27 mixin(EnumText!("Params", _params)); 28 29 // 30 31 enum pastel19 = [ 32 "#fbb4ae", 33 "#b3cde3", 34 "#ccebc5", 35 "#decde4", 36 "#fed9a6", 37 "#ffffcc", 38 "#e5d8bd", 39 "#fddaec", 40 "#f2f2f2" 41 ]; 42 43 string color(T)(string[] colors, T index) if (isIntegral!T) { 44 import std.math : abs; 45 46 const i = abs(index) % colors.length; 47 return colors[i]; 48 } 49 50 enum fileextensions { 51 HIBON = ".hibon" // JSON = ".json" 52 } 53 54 enum dot_fileextension { 55 CMAPX = "cmapx", /// Produces HTML map files for client-side image maps. 56 PDF = "pdf", /++ Adobe PDF via the Cairo library. We have seen problems when embedding 57 into, other documents. Instead, use -Tps2 as described below. +/ 58 PLAIN = "txt", /++ Simple, line-based ASCII format. Appendix E describes this output. An 59 alternate format is plain-ext, which provides port names on the head and 60 tail nodes of edges. +/ 61 PNG = "png", /// PNG (Portable Network Graphics) output. 62 PS = "ps", /// PostScript (EPSF) output. 63 PS2 = "ps2", /++ 64 PostScript (EPSF) output with PDF annotations. 65 This output should be distilled into PDF, 66 such as for pdflatex, before being included in a document. 67 (Use ps2pdf; epstopdf doesn’t handle %%BoundingBox: (atend).) 68 +/ 69 SVG = "svg", /// SVG output. The alternate form svgz produces compressed SVG. 70 VRML = "vrml", /// VRML output. 71 WBMP = "wbmp", ///Wireless BitMap (WBMP) format. 72 } 73 74 // dot -Tsvg test.dot -o test1.svg 75 76 struct Dot { 77 import std.format; 78 79 static string INDENT; 80 static this() { 81 INDENT = " "; 82 } 83 // OutputBuf obuf; 84 string name; 85 const size_t node_size; 86 // string indet; 87 EventView[uint] events; 88 // this(string name, string indent=" ") { 89 // this.obuf = obuf; 90 // this.name = name; 91 // this.indent=indent; 92 // } 93 94 this(const Document doc, string name) { 95 node_size = doc[Params.size].get!ulong; 96 const events_doc = doc[Params.events].get!Document; 97 foreach (e; events_doc[]) { 98 auto event = EventView(e.get!Document); 99 events[event.id] = event; 100 } 101 this.name = name; 102 } 103 104 private void edge(ref OutBuffer obuf, const(string) indent, const bool father_flag, ref const EventView e) const { 105 string witness_text; 106 void local_edge(string[] texts, const uint end_id, string[] options = null) { 107 string[] edge_params; 108 edge_params ~= format(`label="%s"`, texts.join("\n")); 109 edge_params ~= options; 110 obuf.writefln(`%s%s -> %s [%s];`, indent, e.id, end_id, 111 edge_params.join(", ")); 112 } 113 114 string mask2text(const(uint[]) mask) { 115 const mask_text = format("%s", BitMask(mask)); 116 return mask_text[0 .. min(node_size, mask_text.length)]; 117 } 118 119 if (father_flag && e.father !is e.father.init && e.father in events) { 120 const father_event = events[e.father]; 121 string[] texts; 122 // texts ~= mask2text(father_event.witness_mask); 123 local_edge(texts, e.father); 124 } 125 else if (e.mother !is e.mother.init && e.mother in events) { 126 const mother_event = events[e.mother]; 127 string[] texts; 128 string[] options; 129 // const witness_mask_text=mask2text(mother_event.witness_mask); 130 //texts~=mask2text(mother_event.witness_mask); 131 const round_received_mask = mask2text(mother_event.round_received_mask); 132 if (round_received_mask) { 133 texts ~= format("%s:e", round_received_mask); 134 } 135 if (mother_event.witness) { 136 options ~= format(`color="%s"`, "red"); 137 options ~= format(`fontcolor="%s"`, "blue"); 138 options ~= format(`shape="%s"`, "plane"); 139 140 // const strongly_seening_mask = mask2text( 141 // // mother_event.strongly_seeing_mask); 142 // if (strongly_seening_mask) { 143 // texts ~= format("%s:s", strongly_seening_mask); 144 // } 145 // const round_seen_mask = mask2text(mother_event.round_seen_mask); 146 // if (round_seen_mask) { 147 // texts ~= format("%s:r", round_seen_mask); //mask2text(mother_event.round_seen_mask)); 148 // } 149 } 150 // texts ~= format("%s:w", witness_mask_text); 151 152 // // texts~=mask2text(mother_event.strongly_seeing_mask); 153 // } 154 // else { 155 // texts~=witness_mask_text; 156 // } 157 local_edge(texts, e.mother, options); 158 } 159 } 160 161 private void node(ref OutBuffer obuf, const(string) indent, ref const EventView e) const { 162 if (e.father !is e.father.init) { 163 //obuf.writefln("%s%s -> %s;", indent~INDENT, e.id, e.father); 164 edge(obuf, indent ~ INDENT, true, e); 165 166 } 167 obuf.writefln(`%s%s [pos="%s, %s!"];`, indent ~ INDENT, e.id, e.node_id * 2, e.order); 168 169 if (e.witness) { 170 const color = (e.famous) ? "red" : "lightgreen"; 171 obuf.writefln(`%s%s [fillcolor="%s"];`, indent ~ INDENT, e.id, color); 172 } 173 else { 174 obuf.writefln(`%s%s [fillcolor="%s"];`, indent ~ INDENT, e.id, pastel19.color(e.round_received)); 175 } 176 if (e.error) { 177 obuf.writefln(`%s%s [shape="%s"];`, indent ~ INDENT, e.id, "star"); 178 } 179 else if (e.father_less) { 180 obuf.writefln(`%s%s [shape="%s"];`, indent ~ INDENT, e.id, "egg"); 181 } 182 string round_text = (e.round is int.min) ? "\u2693" : e.round.to!string; 183 if (e.round_received !is int.min) { 184 round_text ~= format(":%s", e.round_received); 185 } 186 // if (e.erased) { 187 // obuf.writefln(`%s%s [fontcolor="%s"];`, indent~INDENT, e.id, "yellow"); 188 // } 189 obuf.writefln(`%s%s [xlabel="%s"];`, indent ~ INDENT, e.id, round_text); 190 } 191 192 void draw(ref OutBuffer obuf, const(string) indent = null) { 193 import stdio = std.stdio; 194 195 obuf.writefln("%sdigraph %s {", indent, name); 196 // obuf.writefln(`%snode [margin=0.1 fontcolor=blue fixedsize=true fontsize=32 width=1.2 shape=ellipse rankdir=TB style=filled splines="line"]`, indent); 197 obuf.writefln(`%ssplines=line; pin=true;`, indent); 198 //obuf.writefln(`%sgraph [ranksep="1", nodesep="2"]`, indent); 199 obuf.writefln(`%snode [margin=0.1 fontcolor=blue fixedsize=true fontsize=24 width=1.2 shape=ellipse rankdir=TB style=filled splines=true];`, indent); 200 // edge [lblstyle="above, sloped"]; 201 //obuf.writefln(`%sedge [lblstyle="above, sloped"];`, indent); 202 scope (exit) { 203 obuf.writefln("%s}", indent); 204 } 205 void subgraphs(const(string) indent) { 206 OutBuffer[size_t] subbuf; 207 scope (exit) { 208 foreach (buf; subbuf) { 209 buf.writefln("%s}", indent); 210 obuf.writefln("%s", buf); 211 212 } 213 } 214 const sub_indent = indent ~ INDENT; 215 foreach (e; events) { 216 OutBuffer subgraph_header(ref const EventView e) { 217 auto sbuf = new OutBuffer; 218 sbuf.writefln("%ssubgraph node_%d { peripheries=0", indent, e.node_id); 219 return sbuf; 220 } 221 // stdio.writefln("%s %d", e.id, e.mother); 222 auto sub_obuf = subbuf.require(e.node_id, subgraph_header(e)); 223 if (e.mother !is e.mother.init) { 224 //sub_obuf.writefln("%s%s -> %s;", sub_indent, e.id, e.mother); 225 edge(sub_obuf, indent ~ INDENT, false, e); 226 } 227 } 228 } 229 230 subgraphs(indent ~ INDENT); 231 foreach (e; events) { 232 node(obuf, indent ~ INDENT, e); 233 // if (e.father !is e.father.init) { 234 // //obuf.writefln("%s%s -> %s;", indent~INDENT, e.id, e.father); 235 // edge(obuf, indent~INDENT, true, e); 236 237 // } 238 // obuf.writefln(`%s%s [pos="%s, %s!"];`, indent~INDENT, e.id, e.node_id*2, e.order); 239 // if (e.witness) { 240 // obuf.writefln(`%s%s [fillcolor="%s"];`, indent~INDENT, e.id, "red"); 241 // } 242 // if (e.father_less) { 243 // obuf.writefln(`%s%s [shape="%s"];`, indent~INDENT, e.id, "egg"); 244 // } 245 // if (e.witness_mask.length) { 246 // BitArray witness_mask; 247 // obuf.writefln(`%s%s [fillcolor="%s"];`, indent~INDENT, e.id, "red"); 248 249 // } 250 } 251 } 252 } 253 254 import tagion.tools.Basic; 255 256 mixin Main!_main; 257 258 int _main(string[] args) { 259 import std.stdio; 260 261 immutable program = args[0]; 262 263 bool version_switch; 264 265 string inputfilename; 266 string outputfilename; 267 268 auto logo = import("logo.txt"); 269 auto main_args = getopt(args, 270 std.getopt.config.caseSensitive, 271 std.getopt.config.bundling, 272 "version", "display the version", &version_switch, 273 "inputfile|i", "Sets the HiBON input file name", &inputfilename, // "outputfile|o", "Sets the output file name", &outputfilename, 274 // "bin|b", "Use HiBON or else use JSON", &binary, 275 // "value|V", format("Bill value : default: %d", value), &value, 276 // "pretty|p", format("JSON Pretty print: Default: %s", pretty), &pretty, 277 // "passphrase|P", format("Passphrase of the keypair : default: %s", passphrase), &passphrase 278 279 280 281 ); 282 283 if (version_switch) { 284 revision_text.writeln; 285 return 0; 286 } 287 288 if (main_args.helpWanted) { 289 defaultGetoptPrinter( 290 [ 291 "Documentation: https://tagion.org/", 292 "", 293 "", 294 "Example:", 295 "graphview Alice.hibon | neato -Tsvg -o outputfile.svg", 296 "Usage:", 297 // format("%s [<option>...] <in-file> <out-file>", program), 298 format("%s [<option>...] <in-file>", program), 299 "", 300 "Where:", 301 "<in-file> Is an input file in .hibon format", 302 // "<out-file> Is an output file in .json or .hibon format", 303 // " stdout is used of the output is not specifed the", 304 "", 305 306 "<option>:", 307 ].join("\n"), 308 main_args.options); 309 return 0; 310 } 311 312 if (args.length > 1) { 313 inputfilename = args[1]; 314 } 315 else { 316 stderr.writefln("Input file missing"); 317 return 1; 318 } 319 320 const input_extension = inputfilename.extension; 321 switch (input_extension) { 322 case fileextensions.HIBON: 323 Document doc = fread(inputfilename); 324 const error_code = doc.valid(toDelegate(&error_callback)); 325 if (error_code !is Document.Element.ErrorCode.NONE) { 326 writeln("Document format error"); 327 writefln("For the file %s", inputfilename); 328 return 1; 329 } 330 auto dot = Dot(doc, "G"); 331 auto obuf = new OutBuffer; 332 dot.draw(obuf); 333 writefln("%s", obuf); 334 break; 335 default: 336 stderr.writefln("File extensions %s not valid (only %s)", 337 input_extension, [EnumMembers!fileextensions]); 338 } 339 340 return 0; 341 }