/Tk-GraphViz-1.01/GraphViz.pm

# · Perl · 2320 lines · 1400 code · 438 blank · 482 comment · 207 complexity · 8c50d5ffd7bd13b855c5d479353a96d2 MD5 · raw file

  1. # -*-Perl-*-
  2. use strict;
  3. $Tk::GraphViz::VERSION = '1.01';
  4. package Tk::GraphViz;
  5. use Tk 800.020;
  6. use Tk::Font;
  7. # Parse::Yapp-generated Parser for parsing record node labels
  8. use Tk::GraphViz::parseRecordLabel;
  9. use base qw(Tk::Derived Tk::Canvas);
  10. #use warnings;
  11. use IO qw(Handle File Pipe);
  12. use Carp;
  13. use Reaper qw( reapPid pidStatus );
  14. use IPC::Open3;
  15. use POSIX qw( :sys_wait_h :errno_h );
  16. use Fcntl;
  17. # Initialize as a derived Tk widget
  18. Construct Tk::Widget 'GraphViz';
  19. ######################################################################
  20. # Class initializer
  21. #
  22. ######################################################################
  23. sub ClassInit
  24. {
  25. my ($class, $mw) = @_;
  26. $class->SUPER::ClassInit($mw);
  27. }
  28. ######################################################################
  29. # Instance initializer
  30. #
  31. ######################################################################
  32. sub Populate
  33. {
  34. my ($self, $args) = @_;
  35. $self->SUPER::Populate($args);
  36. # Default resolution, for scaling
  37. $self->{dpi} = 72;
  38. $self->{margin} = .15 * $self->{dpi};
  39. # Keep track of fonts used, so they can be scaled
  40. # when the canvas is scaled
  41. $self->{fonts} = {};
  42. }
  43. ######################################################################
  44. # Show a GraphViz graph
  45. #
  46. # Major steps:
  47. # - generate layout of the graph, which includes
  48. # locations / color info
  49. # - clear canvas
  50. # - parse layout to add nodes, edges, subgraphs, etc
  51. # - resize to fit the graph
  52. ######################################################################
  53. sub show
  54. {
  55. my ($self, $graph, %opt) = @_;
  56. die __PACKAGE__.": Nothing to show" unless defined $graph;
  57. # Layout is actually done in the background, so the graph
  58. # will get updated when the new layout is ready
  59. $self->_startGraphLayout ( $graph, fit => 1, %opt );
  60. }
  61. ######################################################################
  62. # Begin the process of creating the graph layout.
  63. # Layout is done with a separate process, and it can be time
  64. # consuming. So allow the background task to run to completion
  65. # without blocking this process. When the layout task is complete,
  66. # the graph display is actually updated.
  67. ######################################################################
  68. sub _startGraphLayout
  69. {
  70. my ($self, $graph, %opt) = @_;
  71. my ($filename,$delete_file) = $self->_createDotFile ( $graph, %opt );
  72. # If a previous layout process is running, it needs to be killed
  73. $self->_stopGraphLayout( %opt );
  74. $self->{layout} = [];
  75. if ( ($self->{layout_process} =
  76. $self->_startDot ( $filename, delete_file => $delete_file,
  77. %opt )) ) {
  78. $self->{layout_process}{filename} = $filename;
  79. $self->{layout_process}{delete_file} = $delete_file;
  80. $self->{layout_process}{opt} = \%opt;
  81. $self->_checkGraphLayout ();
  82. } else {
  83. $self->_showGraphLayout( %opt );
  84. }
  85. }
  86. ######################################################################
  87. # Stop a layout task running in the background.
  88. # It is important to do a waitpid() on all the background processes
  89. # to prevent them from becoming orphans/zombies
  90. ######################################################################{
  91. sub _stopGraphLayout
  92. {
  93. my ($self, %opt) = @_;
  94. my $proc = $self->{layout_process};
  95. return 0 unless defined $proc;
  96. if ( defined $proc->{pid} ) {
  97. my @sig = qw( TERM TERM TERM TERM KILL );
  98. for ( my $i = 0; $i < 5; ++$i ) {
  99. last unless defined $proc->{pid};
  100. kill $sig[$i], $proc->{pid};
  101. if ( $self->_checkGraphLayout( noafter => 1 ) ) {
  102. sleep $i+1;
  103. }
  104. }
  105. }
  106. unlink $proc->{filename} if ( $proc->{delete_file} );
  107. delete $self->{layout_process};
  108. }
  109. ######################################################################
  110. # Check whether the background layout task has finished
  111. # Also reads any available output the command has generated to
  112. # this point.
  113. # If the command is not finished, schedules for this method to be
  114. # called again in the future, after some period.
  115. ######################################################################
  116. sub _checkGraphLayout
  117. {
  118. my ($self, %opt) = @_;
  119. my $proc = $self->{layout_process};
  120. if ( !defined $proc ) { return 0; }
  121. if ( !defined $proc->{pid} ) { return 0; }
  122. my $finished = 0;
  123. if ( defined(my $stat = pidStatus($proc->{pid})) ) {
  124. # Process has exited
  125. if ( $stat == 0xff00 ) {
  126. $proc->{error} = "exec failed";
  127. }
  128. elsif ( $stat > 0x80 ) {
  129. $stat >>= 8;
  130. }
  131. else {
  132. if ( $stat & 0x80 ) {
  133. $stat &= ~0x80;
  134. $proc->{error} = "Killed by signal $stat (coredump)";
  135. } else {
  136. $proc->{error} = "Kill by signal $stat";
  137. }
  138. }
  139. $proc->{status} = $stat;
  140. $finished = 1;
  141. }
  142. else {
  143. my $kill = kill ( 0 => $proc->{pid} );
  144. if ( !$kill ) {
  145. $proc->{status} = 127;
  146. $proc->{error} = "pid $proc->{pid} gone, but no status!";
  147. $finished = 1;
  148. }
  149. }
  150. # Read available output...
  151. while ( $self->_readGraphLayout () ) { last if !$finished; }
  152. # When finished, show the new contents
  153. if ( $finished ) {
  154. $proc->{pid} = undef;
  155. $self->_stopGraphLayout();
  156. $self->_showGraphLayout ( %{$proc->{opt}} );
  157. return 0;
  158. }
  159. else {
  160. # Not yet finished, so schedule to check again soon
  161. if ( !defined($opt{noafter}) || !$opt{noafter} ) {
  162. my $checkDelay = 500;
  163. if ( defined($proc->{goodread}) ) { $checkDelay = 0; }
  164. $self->after ( $checkDelay, sub { $self->_checkGraphLayout(%opt); } );
  165. }
  166. return 1;
  167. }
  168. }
  169. ######################################################################
  170. # Display the new graph layout.
  171. # This is called once the layout of the graph has been completed.
  172. # The layout data itself is stored as a list layout elements,
  173. # typically read directly from the background layout task
  174. ######################################################################
  175. sub _showGraphLayout
  176. {
  177. my ($self, %opt) = @_;
  178. # Erase old contents
  179. unless ( defined $opt{keep} && $opt{keep} ) {
  180. $self->delete ( 'all' );
  181. delete $self->{fonts}{_default} if exists $self->{fonts}{_default};
  182. }
  183. # Display new contents
  184. $self->_parseLayout ( $self->{layout}, %opt );
  185. # Update scroll-region to new bounds
  186. $self->_updateScrollRegion( %opt );
  187. if ( defined $opt{fit} && $opt{fit} ) {
  188. $self->fit();
  189. }
  190. 1;
  191. }
  192. ######################################################################
  193. # Create a (temporary) file on disk containing the graph
  194. # in canonical GraphViz/dot format.
  195. #
  196. # '$graph' can be
  197. # - a GraphViz instance
  198. # - a scalar containing graph in dot format:
  199. # must match /^\s*(?:di)?graph /
  200. # - a IO::Handle from which to read a graph in dot format
  201. # (contents will be read and converted to a scalar)
  202. # - a filename giving a file that contains a graph in dot format
  203. #
  204. # Returns a filename that contains the DOT description for the graph,
  205. # and an additional flag to indicate if the file is temprary
  206. ######################################################################
  207. sub _createDotFile
  208. {
  209. my ($self, $graph, %opt) = @_;
  210. my $filename = undef;
  211. my $delete_file = undef;
  212. my $ref = ref($graph);
  213. if ( $ref ne '' ) {
  214. # A blessed reference
  215. if ( $ref->isa('GraphViz') ||
  216. UNIVERSAL::can( $graph, 'as_canon') ) {
  217. ($filename, my $fh) = $self->_mktemp();
  218. eval { $graph->as_canon ( $fh ); };
  219. if ( $@ ) {
  220. die __PACKAGE__.": Error calling GraphViz::as_canon on $graph: $@";
  221. }
  222. $fh->close;
  223. $delete_file = 1;
  224. }
  225. elsif ( $ref->isa('IO::Handle') ) {
  226. ($filename, my $fh) = $self->_mktemp();
  227. while ( <$graph> ) { $fh->print; }
  228. $fh->close;
  229. $delete_file = 1;
  230. }
  231. }
  232. else {
  233. # Not a blessed reference
  234. # Try it as a filename
  235. # Skip the filename test if it has newlines
  236. if ( $graph !~ /\n/m &&
  237. -r $graph ) {
  238. $filename = $graph;
  239. $delete_file = 0;
  240. }
  241. # Try it as a scalar
  242. elsif ( $graph =~ /^\s*(?:di)?graph / ) {
  243. ($filename, my $fh) = $self->_mktemp();
  244. $fh->print ( $graph );
  245. $fh->close;
  246. $delete_file = 1;
  247. }
  248. else {
  249. die __PACKAGE__.": Bad graph";
  250. }
  251. }
  252. confess unless defined($filename) && defined($delete_file);
  253. ($filename, $delete_file);
  254. }
  255. ######################################################################
  256. # Create a temp file for writing, open a handle to it
  257. #
  258. ######################################################################
  259. {
  260. my $_mktemp_count = 0;
  261. sub _mktemp
  262. {
  263. my $tempDir = $ENV{TEMP} || $ENV{TMP} || '/tmp';
  264. my $filename = sprintf ( "%s/Tk-GraphViz.dot.$$.%d.dot",
  265. $tempDir, $_mktemp_count++ );
  266. my $fh = new IO::File ( $filename, 'w' ) ||
  267. confess "Can't write temp file: $filename: $!";
  268. binmode($fh);
  269. ($filename, $fh);
  270. }
  271. }
  272. ######################################################################
  273. # Starting running 'dot' (or some other layout command) in the
  274. # background, to convert a dot file to layout output format.
  275. #
  276. ######################################################################
  277. sub _startDot
  278. {
  279. my ($self, $filename, %opt) = @_;
  280. confess "Can't read file: $filename"
  281. unless -r $filename;
  282. my @layout_cmd = $self->_makeLayoutCommand ( $filename, %opt );
  283. # Simple, non-asynchronous mode: execute the
  284. # process synchnronously and wait for all its output
  285. if ( !defined($opt{async}) || !$opt{async} ) {
  286. my $pipe = new IO::Pipe;
  287. $pipe->reader ( @layout_cmd );
  288. while ( <$pipe> ) { push @{$self->{layout}}, $_; }
  289. if ( $opt{delete_file} ) {
  290. unlink $filename;
  291. }
  292. return undef;
  293. }
  294. # Now execute it
  295. my $in = new IO::Handle;
  296. my $out = new IO::Handle;
  297. $in->autoflush;
  298. local $@ = undef;
  299. my $proc = {};
  300. my $ppid = $$;
  301. eval {
  302. $proc->{pid} = open3 ( $in, $out, '>&STDERR', @layout_cmd );
  303. reapPid ( $proc->{pid} );
  304. # Fork failure?
  305. exit(127) if ( $$ != $ppid );
  306. };
  307. if ( defined($@) && $@ ne '' ) {
  308. $self->{error} = $@;
  309. }
  310. # Close stdin so child process sees eof on its input
  311. $in->close;
  312. $proc->{output} = $out;
  313. $proc->{buf} = '';
  314. $proc->{buflen} = 0;
  315. $proc->{eof} = 0;
  316. # Enable non-blocking reads on the output
  317. $self->_disableBlocking ( $out );
  318. return $proc;
  319. }
  320. ######################################################################
  321. # $self->_disableBlocking ( $fh )
  322. #
  323. # Turn off blocking-mode for the given handle
  324. ######################################################################
  325. sub _disableBlocking
  326. {
  327. my ($self, $fh) = @_;
  328. my $flags = 0;
  329. fcntl ( $fh, &F_GETFL, $flags ) or
  330. confess "Can't get flags for handle";
  331. $flags = ($flags+0) | O_NONBLOCK;
  332. fcntl ( $fh, &F_SETFL, $flags ) or
  333. confess "Can't set flags for handle";
  334. 1;
  335. }
  336. ######################################################################
  337. # Assemble the command for executing dot/neato/etc as a child process
  338. # to generate the layout. The layout of the graph will be read from
  339. # the command's stdout
  340. ######################################################################
  341. sub _makeLayoutCommand
  342. {
  343. my ($self, $filename, %opt) = @_;
  344. my $layout_cmd = $opt{layout} || 'dot';
  345. my @opts = ();
  346. if ( defined $opt{graphattrs} ) {
  347. # Add -Gname=value settings to command line
  348. my $list = $opt{graphattrs};
  349. my $ref = ref($list);
  350. die __PACKAGE__.": Expected array reference for graphattrs"
  351. unless defined $ref && $ref eq 'ARRAY';
  352. while ( my ($key, $val) = splice @$list, 0, 2 ) {
  353. push @opts, "-G$key=\"$val\"";
  354. }
  355. }
  356. if ( defined $opt{nodeattrs} ) {
  357. # Add -Gname=value settings to command line
  358. my $list = $opt{nodeattrs};
  359. my $ref = ref($list);
  360. die __PACKAGE__.": Expected array reference for nodeattrs"
  361. unless defined $ref && $ref eq 'ARRAY';
  362. while ( my ($key, $val) = splice @$list, 0, 2 ) {
  363. push @opts, "-N$key=\"$val\"";
  364. }
  365. }
  366. if ( defined $opt{edgeattrs} ) {
  367. # Add -Gname=value settings to command line
  368. my $list = $opt{edgeattrs};
  369. my $ref = ref($list);
  370. die __PACKAGE__.": Expected array reference for edgeattrs"
  371. unless defined $ref && $ref eq 'ARRAY';
  372. while ( my ($key, $val) = splice @$list, 0, 2 ) {
  373. push @opts, "-E$key=\"$val\"";
  374. }
  375. }
  376. return ($layout_cmd, @opts, '-Tdot', $filename);
  377. }
  378. ######################################################################
  379. # Read data from the background layout process, in a non-blocking
  380. # mode. Reads all the data currently available, up to some reasonable
  381. # buffer size.
  382. ######################################################################
  383. sub _readGraphLayout
  384. {
  385. my ($self) = @_;
  386. my $proc = $self->{layout_process};
  387. if ( !defined $proc ) { return; }
  388. delete $proc->{goodread};
  389. my $rv = sysread ( $proc->{output}, $proc->{buf}, 10240,
  390. $proc->{buflen} );
  391. if ( !defined($rv) && $! == EAGAIN ) {
  392. # Would block, don't do anything right now
  393. return 0;
  394. }
  395. elsif ( $rv == 0 ) {
  396. # 0 bytes read -- EOF
  397. $proc->{eof} = 1;
  398. return 0;
  399. }
  400. else {
  401. $proc->{buflen} += $rv;
  402. $proc->{goodread} = 1;
  403. # Go ahead and split the output that's available now,
  404. # so that this part at least is potentially spread out in time
  405. # while the background process keeps running.
  406. $self->_splitGraphLayout ();
  407. return $rv;
  408. }
  409. }
  410. ######################################################################
  411. # Split the buffered data read from the background layout task
  412. # into individual lines
  413. ######################################################################
  414. sub _splitGraphLayout
  415. {
  416. my ($self) = @_;
  417. my $proc = $self->{layout_process};
  418. if ( !defined $proc ) { return; }
  419. my @lines = split ( /\n/, $proc->{buf} );
  420. # If not at eof, keep the last line in the buffer
  421. if ( !$proc->{eof} ) {
  422. $proc->{buf} = pop @lines;
  423. $proc->{buflen} = length($proc->{buf});
  424. }
  425. push @{$self->{layout}}, @lines;
  426. }
  427. ######################################################################
  428. # Parse the layout data in dot 'text' format, as returned
  429. # by _dot2layout. Nodes / edges / etc defined in the layout
  430. # are added as object in the canvas
  431. ######################################################################
  432. sub _parseLayout
  433. {
  434. my ($self, $layoutLines, %opt) = @_;
  435. my $directed = 1;
  436. my %allNodeAttrs = ();
  437. my %allEdgeAttrs = ();
  438. my %graphAttrs = ();
  439. my ($minX, $minY, $maxX, $maxY) = ( undef, undef, undef, undef );
  440. my @saveStack = ();
  441. my $accum = undef;
  442. foreach ( @$layoutLines ) {
  443. s/\r//g; # get rid of any returns ( dos text files)
  444. chomp;
  445. # Handle line-continuation that gets put in for longer lines,
  446. # as well as lines that are continued with commas at the end
  447. if ( defined $accum ) {
  448. $_ = $accum . $_;
  449. $accum = undef;
  450. }
  451. if ( s/\\\s*$// ||
  452. /\,\s*$/ ) {
  453. $accum = $_;
  454. next;
  455. }
  456. #STDERR->print ( "gv _parse: $_\n" );
  457. if ( /^\s+node \[(.+)\];/ ) {
  458. $self->_parseAttrs ( "$1", \%allNodeAttrs );
  459. next;
  460. }
  461. if ( /^\s+edge \[(.+)\];/ ) {
  462. $self->_parseAttrs ( "$1", \%allEdgeAttrs );
  463. next;
  464. }
  465. if ( /^\s+graph \[(.+)\];/ ) {
  466. $self->_parseAttrs ( "$1", \%graphAttrs );
  467. next;
  468. }
  469. if ( /^\s+subgraph \S+ \{/ ||
  470. /^\s+\{/ ) {
  471. push @saveStack, [ {%graphAttrs},
  472. {%allNodeAttrs},
  473. {%allEdgeAttrs} ];
  474. delete $graphAttrs{label};
  475. delete $graphAttrs{bb};
  476. next;
  477. }
  478. if ( /^\s*\}/ ) {
  479. # End of a graph section
  480. if ( @saveStack ) {
  481. # Subgraph
  482. if ( defined($graphAttrs{bb}) && $graphAttrs{bb} ne '' ) {
  483. my ($x1,$y1,$x2,$y2) = split ( /\s*,\s*/, $graphAttrs{bb} );
  484. $minX = min($minX,$x1);
  485. $minY = min($minY,$y1);
  486. $maxX = max($maxX,$x2);
  487. $maxY = max($maxY,$y2);
  488. $self->_createSubgraph ( $x1, $y1, $x2, $y2, %graphAttrs );
  489. }
  490. my ($g,$n,$e) = @{pop @saveStack};
  491. %graphAttrs = %$g;
  492. %allNodeAttrs = %$n;
  493. %allEdgeAttrs = %$e;
  494. next;
  495. } else {
  496. # End of the graph
  497. # Create any whole-graph label
  498. if ( defined($graphAttrs{bb}) ) {
  499. my ($x1,$y1,$x2,$y2) = split ( /\s*,\s*/, $graphAttrs{bb} );
  500. $minX = min($minX,$x1);
  501. $minY = min($minY,$y1);
  502. $maxX = max($maxX,$x2);
  503. $maxY = max($maxY,$y2);
  504. # delete bb attribute so rectangle is not drawn around whole graph
  505. delete $graphAttrs{bb};
  506. $self->_createSubgraph ( $x1, $y1, $x2, $y2, %graphAttrs );
  507. }
  508. last;
  509. }
  510. }
  511. if ( /\s+(.+) \-[\>\-] (.+) \[(.+)\];/ ) {
  512. # Edge
  513. my ($n1,$n2,$attrs) = ($1,$2,$3);
  514. my %edgeAttrs = %allEdgeAttrs;
  515. $self->_parseAttrs ( $attrs, \%edgeAttrs );
  516. my ($x1,$y1,$x2,$y2) = $self->_createEdge ( $n1, $n2, %edgeAttrs );
  517. $minX = min($minX,$x1);
  518. $minY = min($minY,$y1);
  519. $maxX = max($maxX,$x2);
  520. $maxY = max($maxY,$y2);
  521. next;
  522. }
  523. if ( /\s+(.+) \[(.+)\];/ ) {
  524. # Node
  525. my ($name,$attrs) = ($1,$2);
  526. # Get rid of any leading/tailing quotes
  527. $name =~ s/^\"//;
  528. $name =~ s/\"$//;
  529. my %nodeAttrs = %allNodeAttrs;
  530. $self->_parseAttrs ( $attrs, \%nodeAttrs );
  531. my ($x1,$y1,$x2,$y2) = $self->_createNode ( $name, %nodeAttrs );
  532. $minX = min($minX,$x1);
  533. $minY = min($minY,$y1);
  534. $maxX = max($maxX,$x2);
  535. $maxY = max($maxY,$y2);
  536. next;
  537. }
  538. }
  539. }
  540. ######################################################################
  541. # Parse attributes of a node / edge / graph / etc,
  542. # store the values in a hash
  543. ######################################################################
  544. sub _parseAttrs
  545. {
  546. my ($self, $attrs, $attrHash) = @_;
  547. while ( $attrs =~ s/^,?\s*([^=]+)=// ) {
  548. my ($key) = ($1);
  549. # Scan forward until end of value reached -- the first
  550. # comma not in a quoted string.
  551. # Probably a more efficient method for doing this, but...
  552. my @chars = split(//, $attrs);
  553. my $quoted = 0;
  554. my $val = '';
  555. my $last = '';
  556. my ($i,$n);
  557. for ( ($i,$n) = (0, scalar(@chars)); $i < $n; ++$i ) {
  558. my $ch = $chars[$i];
  559. last if $ch eq ',' && !$quoted;
  560. if ( $ch eq '"' ) { $quoted = !$quoted unless $last eq '\\'; }
  561. $val .= $ch;
  562. $last = $ch;
  563. }
  564. $attrs = join('', splice ( @chars, $i ) );
  565. # Strip leading and trailing ws in key and value
  566. $key =~ s/^\s+|\s+$//g;
  567. $val =~ s/^\s+|\s+$//g;
  568. if ( $val =~ /^\"(.*)\"$/ ) { $val = $1; }
  569. $val =~ s/\\\"/\"/g; # Un-escape quotes
  570. $attrHash->{$key} = $val;
  571. }
  572. }
  573. ######################################################################
  574. # Create a subgraph / cluster
  575. #
  576. ######################################################################
  577. sub _createSubgraph
  578. {
  579. my ($self, $x1, $y1, $x2, $y2, %attrs) = @_;
  580. my $label = $attrs{label};
  581. my $color = $attrs{color} || 'black';
  582. # Want box to be filled with background color by default, so that
  583. # it is 'clickable'
  584. my $fill = $self->cget('-background');
  585. my $tags = [ subgraph => $label, %attrs ];
  586. # Get/Check a valid color
  587. $color = $self->_tryColor($color);
  588. my @styleArgs;
  589. if( $attrs{style} ){
  590. my $style = $attrs{style};
  591. if ( $style =~ /dashed/i ) {
  592. @styleArgs = (-dash => '-');
  593. }
  594. elsif ( $style =~ /dotted/ ) {
  595. @styleArgs = (-dash => '.');
  596. }
  597. elsif ( $style =~ /filled/ ) {
  598. $fill = ( $self->_tryColor($attrs{fillcolor}) || $color );
  599. }
  600. elsif( $style =~ /bold/ ) {
  601. # Bold outline, gets wider line
  602. push @styleArgs, (-width => 2);
  603. }
  604. }
  605. # Create the box if coords are defined
  606. if( $attrs{bb} ) {
  607. my $id = $self->createRectangle ( $x1, -1 * $y2, $x2, -1 * $y1,
  608. -outline => $color,
  609. -fill => $fill, @styleArgs,
  610. -tags => $tags );
  611. $self->lower($id); # make sure it doesn't obscure anything
  612. }
  613. # Create the label, if defined
  614. if ( defined($attrs{label}) ) {
  615. my $lp = $attrs{lp} || '';
  616. my ($x,$y) = split(/\s*,\s*/,$lp);
  617. if ( $lp eq '' ) { ($x,$y) = ($x1, $y2); }
  618. $label =~ s/\\n/\n/g;
  619. $tags->[0] = 'subgraphlabel'; # Replace 'subgraph' w/ 'subgraphlabel'
  620. my @args = ( $x, -1 * $y,
  621. -text => $label,
  622. -tags => $tags );
  623. push @args, ( -state => 'disabled' );
  624. if ( $lp eq '' ) { push @args, ( -anchor => 'nw' ); }
  625. $self->createText ( @args );
  626. }
  627. }
  628. ######################################################################
  629. # Create a node
  630. #
  631. ######################################################################
  632. sub _createNode
  633. {
  634. my ($self, $name, %attrs) = @_;
  635. my ($x,$y) = split(/,/, $attrs{pos});
  636. my $dpi = $self->{dpi};
  637. my $w = $attrs{width} * $dpi; #inches
  638. my $h = $attrs{height} * $dpi; #inches
  639. my $x1 = $x - $w/2.0;
  640. my $y1 = $y - $h/2.0;
  641. my $x2 = $x + $w/2.0;
  642. my $y2 = $y + $h/2.0;
  643. my $label = $attrs{label};
  644. $label = $attrs{label} = $name unless defined $label;
  645. if ( $label eq '\N' ) { $label = $attrs{label} = $name; }
  646. #STDERR->printf ( "createNode: $name \"$label\" ($x1,$y1) ($x2,$y2)\n" );
  647. # Node shape
  648. my $tags = [ node => $name, %attrs ];
  649. my @args = ();
  650. my $outline = $self->_tryColor($attrs{color}) || 'black';
  651. my $fill = $self->_tryColor($attrs{fillcolor}) || $self->cget('-background');
  652. my $fontcolor = $self->_tryColor($attrs{fontcolor}) || 'black';
  653. my $shape = $attrs{shape} || '';
  654. foreach my $style ( split ( /,/, $attrs{style}||'' ) ) {
  655. if ( $style eq 'filled' ) {
  656. $fill = ( $self->_tryColor($attrs{fillcolor}) ||
  657. $self->_tryColor($attrs{color}) ||
  658. 'lightgrey' );
  659. }
  660. elsif ( $style eq 'invis' ) {
  661. $outline = undef;
  662. $fill = undef;
  663. }
  664. elsif ( $style eq 'dashed' ) {
  665. push @args, -dash => '--';
  666. }
  667. elsif ( $style eq 'dotted' ) {
  668. push @args, -dash => '.';
  669. }
  670. elsif ( $style eq 'bold' ) {
  671. push @args, -width => 2.0;
  672. }
  673. elsif ( $style =~ /setlinewidth\((\d+)\)/ ) {
  674. push @args, -width => "$1";
  675. }
  676. }
  677. push @args, -outline => $outline if ( defined($outline) );
  678. push @args, -fill => $fill if ( defined($fill) );
  679. my $orient = $attrs{orientation} || 0.0;
  680. # Node label
  681. $label =~ s/\\n/\n/g;
  682. unless ( $shape eq 'record' ) {
  683. # Normal non-record node types
  684. $self->_createShapeNode ( $shape, $x1, -1*$y2, $x2, -1*$y1,
  685. $orient, @args, -tags => $tags );
  686. $label = undef if ( $shape eq 'point' );
  687. # Node label
  688. if ( defined $label ) {
  689. $tags->[0] = 'nodelabel'; # Replace 'node' w/ 'nodelabel'
  690. @args = ( ($x1 + $x2)/2, -1*($y2 + $y1)/2, -text => $label,
  691. -anchor => 'center', -justify => 'center',
  692. -tags => $tags, -fill => $fontcolor );
  693. push @args, ( -state => 'disabled' );
  694. $self->createText ( @args );
  695. }
  696. }
  697. else {
  698. # Record node types
  699. $self->_createRecordNode ( $label, %attrs, tags => $tags );
  700. }
  701. # Return the bounding box of the node
  702. ($x1,$y1,$x2,$y2);
  703. }
  704. ######################################################################
  705. # Create an item of a specific shape, generally used for creating
  706. # node shapes.
  707. ######################################################################
  708. my %polyShapes =
  709. ( box => [ [ 0, 0 ], [ 0, 1 ], [ 1, 1 ], [ 1, 0 ] ],
  710. rect => [ [ 0, 0 ], [ 0, 1 ], [ 1, 1 ], [ 1, 0 ] ],
  711. rectangle => [ [ 0, 0 ], [ 0, 1 ], [ 1, 1 ], [ 1, 0 ] ],
  712. triangle => [ [ 0, .75 ], [ 0.5, 0 ], [ 1, .75 ] ],
  713. invtriangle => [ [ 0, .25 ], [ 0.5, 1 ], [ 1, .25 ] ],
  714. diamond => [ [ 0, 0.5 ], [ 0.5, 1.0 ], [ 1.0, 0.5 ], [ 0.5, 0.0 ] ],
  715. pentagon => [ [ .5, 0 ], [ 1, .4 ], [ .75, 1 ], [ .25, 1 ], [ 0, .4 ] ],
  716. hexagon => [ [ 0, .5 ], [ .33, 0 ], [ .66, 0 ],
  717. [ 1, .5 ], [ .66, 1 ], [ .33, 1 ] ],
  718. septagon => [ [ .5, 0 ], [ .85, .3 ], [ 1, .7 ], [ .75, 1 ],
  719. [ .25, 1 ], [ 0, .7 ], [ .15, .3 ] ],
  720. octagon => [ [ 0, .3 ], [ 0, .7 ], [ .3, 1 ], [ .7, 1 ],
  721. [ 1, .7 ], [ 1, .3 ], [ .7, 0 ], [ .3, 0 ] ],
  722. trapezium => [ [ 0, 1 ], [ .21, 0 ], [ .79, 0 ], [ 1, 1 ] ],
  723. invtrapezium => [ [ 0, 0], [ .21, 1 ], [ .79, 1 ], [ 1, 0 ] ],
  724. parallelogram => [ [ 0, 1 ], [ .20, 0 ], [ 1, 0 ], [ .80, 1 ] ],
  725. house => [ [ 0, .9 ], [ 0, .5 ], [ .5, 0 ], [ 1, .5 ], [ 1, .9 ] ],
  726. invhouse => [ [ 0, .1 ], [ 0, .5 ], [ .5, 1 ], [ 1, .5 ], [ 1, .1 ] ],
  727. folder => [ [ 0, 0.1 ], [ 0, 1 ], [ 1, 1 ], [ 1, 0.1 ],
  728. [0.9, 0 ], [0.7 , 0 ] , [0.6, 0.1 ] ],
  729. component => [ [ 0, 0 ], [ 0, 0.1 ], [ 0.03, 0.1 ], [ -0.03, 0.1 ],
  730. [ -0.03, 0.3 ], [ 0.03 , 0.3 ], [ 0.03, 0.1 ],
  731. [ 0.03 , 0.3 ], [ 0 , 0.3 ], [ 0, 0.7 ], [ 0.03, 0.7 ],
  732. [ -0.03, 0.7 ], [ -0.03, 0.9 ], [ 0.03 , 0.9 ],
  733. [ 0.03, 0.7 ], [ 0.03 , 0.9 ], [ 0 , 0.9 ],
  734. [ 0, 1 ], [ 1, 1 ], [ 1, 0 ] ],
  735. );
  736. sub _createShapeNode
  737. {
  738. my ($self, $shape, $x1, $y1, $x2, $y2, $orient, %args) = @_;
  739. #STDERR->printf ( "createShape: $shape ($x1,$y1) ($x2,$y2)\n" );
  740. my $id = undef;
  741. my @extraArgs = ();
  742. # Special handling for recursive calls to create periphery shapes
  743. # (for double-, triple-, etc)
  744. my $periphShape = $args{_periph};
  745. if ( defined $periphShape ) {
  746. delete $args{_periph};
  747. # Periphery shapes are drawn non-filled, so they are
  748. # not clickable
  749. push @extraArgs, ( -fill => undef, -state => 'disabled' );
  750. };
  751. # Simple shapes: defined in the polyShape hash
  752. if ( exists $polyShapes{$shape} ) {
  753. $id = $self->_createPolyShape ( $polyShapes{$shape},
  754. $x1, $y1, $x2, $y2, $orient,
  755. %args, @extraArgs );
  756. }
  757. # Other special-case shapes:
  758. elsif ( $shape =~ s/^double// ) {
  759. my $diam = max(abs($x2-$x1),abs($y2-$y1));
  760. my $inset = max(2,min(5,$diam*.1));
  761. return $self->_createShapeNode ( $shape, $x1, $y1, $x2, $y2, $orient,
  762. %args, _periph => [ 1, $inset ] );
  763. }
  764. elsif ( $shape =~ s/^triple// ) {
  765. my $diam = max(abs($x2-$x1),abs($y2-$y1));
  766. my $inset = min(5,$diam*.1);
  767. return $self->_createShapeNode ( $shape, $x1, $y1, $x2, $y2, $orient,
  768. %args, _periph => [ 2, $inset ] );
  769. }
  770. elsif ( $shape eq 'plaintext' ) {
  771. # Don't draw an outline for plaintext
  772. $id = 0;
  773. }
  774. elsif ( $shape eq 'point' ) {
  775. # Draw point as a small oval
  776. $shape = 'oval';
  777. }
  778. elsif ( $shape eq 'ellipse' || $shape eq 'circle' ) {
  779. $shape = 'oval';
  780. }
  781. elsif ( $shape eq 'oval' ) {
  782. }
  783. elsif ( $shape eq '' ) {
  784. # Default shape = ellipse
  785. $shape = 'oval';
  786. }
  787. else {
  788. warn __PACKAGE__.": Unsupported shape type: '$shape', using box";
  789. }
  790. if ( !defined $id ) {
  791. if ( $shape eq 'oval' ) {
  792. $id = $self->createOval ( $x1, $y1, $x2, $y2, %args, @extraArgs );
  793. } else {
  794. $id = $self->createRectangle ( $x1, $y1, $x2, $y2, %args, @extraArgs );
  795. }
  796. }
  797. # Need to create additional periphery shapes?
  798. if ( defined $periphShape ) {
  799. # This method of stepping in a fixed ammount in x and y is not
  800. # correct, because the aspect of the overall shape changes...
  801. my $inset = $periphShape->[1];
  802. $x1 += $inset;
  803. $y1 += $inset;
  804. $x2 -= $inset;
  805. $y2 -= $inset;
  806. if ( --$periphShape->[0] > 0 ) {
  807. @extraArgs = ( _periph => $periphShape );
  808. } else {
  809. @extraArgs = ();
  810. }
  811. return $self->_createShapeNode ( $shape, $x1, $y1, $x2, $y2, $orient,
  812. %args, @extraArgs );
  813. }
  814. $id;
  815. }
  816. ######################################################################
  817. # Create an arbitrary polygonal shape, using a set of unit points.
  818. # The points will be scaled to fit the given bounding box.
  819. ######################################################################
  820. sub _createPolyShape
  821. {
  822. my ($self, $upts, $x1, $y1, $x2, $y2, $orient, %args) = @_;
  823. my ($ox, $oy) = 1.0;
  824. if ( $orient != 0 ) {
  825. $orient %= 360.0;
  826. # Convert to radians, and rotate ccw instead of cw
  827. $orient *= 0.017453; # pi / 180.0
  828. my $c = cos($orient);
  829. my $s = sin($orient);
  830. my $s_plus_c = $s + $c;
  831. my @rupts = ();
  832. foreach my $upt ( @$upts ) {
  833. my ($ux, $uy) = @$upt;
  834. $ux -= 0.5;
  835. $uy -= 0.5;
  836. #STDERR->printf ( "orient: rotate (%.2f,%.2f) by %g deg\n",
  837. # $ux, $uy, $orient / 0.017453 );
  838. $ux = $ux * $c - $uy * $s; # x' = x cos(t) - y sin(t)
  839. $uy = $uy * $s_plus_c; # y' = y sin(t) + y cos(t)
  840. #STDERR->printf ( " --> (%.2f,%.2f)\n", $ux, $uy );
  841. $ux += 0.5;
  842. $uy += 0.5;
  843. push @rupts, [ $ux, $uy ];
  844. }
  845. $upts = \@rupts;
  846. }
  847. my $dx = $x2 - $x1;
  848. my $dy = $y2 - $y1;
  849. my @pts = ();
  850. foreach my $upt ( @$upts ) {
  851. my ($ux, $uy ) = @$upt;
  852. push @pts, ( $x1 + $ux*$dx, $y1 + $uy*$dy );
  853. }
  854. $self->createPolygon ( @pts, %args );
  855. }
  856. ######################################################################
  857. # Draw the node record shapes
  858. ######################################################################
  859. sub _createRecordNode
  860. {
  861. my ($self, $label, %attrs) = @_;
  862. my $tags = $attrs{tags};
  863. # Get Rectangle Coords
  864. my $rects = $attrs{rects};
  865. my @rects = split(' ', $rects);
  866. my @rectsCoords = map [ split(',',$_) ], @rects;
  867. # Setup to parse the label (Label parser object created using Parse::Yapp)
  868. my $parser = new Tk::GraphViz::parseRecordLabel();
  869. $parser->YYData->{INPUT} = $label;
  870. # And parse it...
  871. my $structure = $parser->YYParse
  872. ( yylex => \&Tk::GraphViz::parseRecordLabel::Lexer,
  873. yyerror => \&Tk::GraphViz::parseRecordLabel::Error,
  874. yydebug => 0 );
  875. die __PACKAGE__.": Error Parsing Record Node Label '$label'\n"
  876. unless $structure;
  877. my @labels = @$structure;
  878. # Draw the rectangles
  879. my $portIndex = 1; # Ports numbered from 1. This is used for the port name
  880. # in the tags, if no port name is defined in the dot file
  881. foreach my $rectCoords ( @rectsCoords ) {
  882. my ($port, $text) = %{shift @labels};
  883. # use port index for name, if one not defined
  884. $port = $portIndex unless ( $port =~ /\S/);
  885. my %portTags = (@$tags); # copy of tags
  886. $portTags{port} = $port;
  887. # get rid of leading trailing whitespace
  888. $text =~ s/^\s+//;
  889. $text =~ s/\s+$//;
  890. $portTags{label} = $text;
  891. my ($x1,$y1,$x2,$y2) = @$rectCoords;
  892. $self->createRectangle ( $x1, -$y1, $x2, -$y2, -tags => [%portTags] );
  893. # Find midpoint for label anchor point
  894. my $midX = ($x1 + $x2)/2;
  895. my $midY = ($y1 + $y2)/2;
  896. $portTags{nodelabel} = delete $portTags{node}; # Replace 'node' w/ 'nodelabel'
  897. $self->createText ( $midX, -$midY, -text => $text, -tags => [%portTags]);
  898. $portIndex++;
  899. }
  900. }
  901. ######################################################################
  902. # Create a edge
  903. #
  904. ######################################################################
  905. sub _createEdge
  906. {
  907. my ($self, $n1, $n2, %attrs) = @_;
  908. my $x1 = undef;
  909. my $y1 = undef;
  910. my $x2 = undef;
  911. my $y2 = undef;
  912. my $tags = [ edge => "$n1 $n2",
  913. node1 => $n1, node2 => $n2,
  914. %attrs ];
  915. # Parse the edge position
  916. my $pos = $attrs{pos} || return;
  917. my ($startEndCoords,@coords) = $self->_parseEdgePos ( $pos );
  918. my $arrowhead = $attrs{arrowhead};
  919. my $arrowtail = $attrs{arrowtail};
  920. my @args = ();
  921. # Convert Biezer control points to 4 real points to smooth against
  922. # Canvas line smoothing doesn't use beizers, so we supply more points
  923. # along the manually-calculated bezier points.
  924. @coords = map @$_, @coords; #flatten coords array
  925. my @newCoords;
  926. my ($startIndex, $stopIndex);
  927. $startIndex = 0;
  928. $stopIndex = 7;
  929. my $lastFlag = 0;
  930. my @controlPoints;
  931. while($stopIndex <= $#coords){
  932. @controlPoints = @coords[$startIndex..$stopIndex];
  933. # If this is the last set, set the flag, so we will get
  934. # the last point
  935. $lastFlag = 1 if( $stopIndex == $#coords);
  936. push @newCoords,
  937. $self->_bezierInterpolate(\@controlPoints, 0.1, $lastFlag);
  938. $startIndex += 6;
  939. $stopIndex += 6;
  940. }
  941. # Add start/end coords
  942. if(defined($startEndCoords->{s})){
  943. unshift @newCoords, @{ $startEndCoords->{s} }; # put at the begining
  944. }
  945. if(defined($startEndCoords->{e})){
  946. push @newCoords, @{ $startEndCoords->{e}}; # put at the end
  947. }
  948. # Convert Sign of y-values of coords, record min/max
  949. for( my $i = 0; $i < @newCoords; $i+= 2){
  950. my ($x,$y) = @newCoords[$i, $i+1];
  951. push @args, $x, -1*$y;
  952. #printf ( " $x,$y\n" );
  953. $x1 = min($x1, $x);
  954. $y1 = min($y1, $y);
  955. $x2 = max($x2, $x);
  956. $y2 = max($y2, $y);
  957. }
  958. #STDERR->printf ( "createEdge: $n1->$n2 ($x1,$y1) ($x2,$y2)\n" );
  959. if ( defined($startEndCoords->{s}) &&
  960. defined($startEndCoords->{e}) &&
  961. (not defined $arrowhead) &&
  962. (not defined $arrowtail) ) { # two-sided arrow
  963. push @args, -arrow => 'both';
  964. }
  965. elsif ( defined($startEndCoords->{e}) &&
  966. (not defined $arrowhead) ) { # arrow just at the end
  967. push @args, -arrow => 'last';
  968. }
  969. elsif ( defined($startEndCoords->{s}) &&
  970. (not defined $arrowtail) ) { # arrow just at the start
  971. push @args, -arrow => 'first';
  972. }
  973. my $color = $attrs{color};
  974. foreach my $style ( split(/,/, $attrs{style}||'') ) {
  975. if ( $style eq 'dashed' ) {
  976. push @args, -dash => '--';
  977. }
  978. elsif ( $style eq 'dotted' ) {
  979. push @args, -dash => ',';
  980. }
  981. elsif ( $style =~ /setlinewidth\((\d+)\)/ ) {
  982. push @args, -width => "$1";
  983. }
  984. elsif ( $style =~ /invis/ ) {
  985. # invisible edge, make same as background
  986. $color = $self->cget('-background');
  987. }
  988. }
  989. push @args, -fill => ( $self->_tryColor($color) || 'black' );
  990. # Create the line
  991. $self->createLine ( @args, -smooth => 1, -tags => $tags );
  992. # Create the arrowhead (at end of line)
  993. if ( defined($arrowhead) && $arrowhead =~ /^(.*)dot$/ ) {
  994. my $modifier = $1;
  995. # easy implementation for calculating the arrow position
  996. my ($x1, $y1) = @newCoords[(@newCoords-2), (@newCoords-1)];
  997. my ($x2, $y2) = @newCoords[(@newCoords-4), (@newCoords-3)];
  998. my $x = ($x1 + $x2)/2;
  999. my $y = ($y1 + $y2)/2;
  1000. my @args = ($x-4, -1*($y-4), $x+4, -1*($y+4));
  1001. # check for modifiers
  1002. if ($modifier eq "o") {
  1003. push @args, -fill => $self->cget('-background');
  1004. } else {
  1005. push @args, -fill => ($self->_tryColor($color) || 'black');
  1006. }
  1007. # draw
  1008. $self->createOval ( @args );
  1009. }
  1010. # Create the arrowtail (at start of line)
  1011. if ( defined($arrowtail) && $arrowtail =~ /^(.*)dot$/ ) {
  1012. my $modifier = $1;
  1013. # easy implementation for calculating the arrow position
  1014. my ($x1, $y1) = @newCoords[0, 1];
  1015. my ($x2, $y2) = @newCoords[2, 3];
  1016. my $x = ($x1 + $x2)/2;
  1017. my $y = ($y1 + $y2)/2;
  1018. my @args = ($x-4, -1*($y-4), $x+4, -1*($y+4));
  1019. # check for modifiers
  1020. if ($modifier eq "o") {
  1021. push @args, -fill => $self->cget('-background');
  1022. } else {
  1023. push @args, -fill => ($self->_tryColor($color) || 'black');
  1024. }
  1025. # draw
  1026. $self->createOval ( @args );
  1027. }
  1028. # Create optional label
  1029. my $label = $attrs{label};
  1030. my $lp = $attrs{lp};
  1031. if ( defined($label) && defined($lp) ) {
  1032. $label =~ s/\\n/\n/g;
  1033. $tags->[0] = 'edgelabel'; # Replace 'edge' w/ 'edgelabel'
  1034. my ($x,$y) = split(/,/, $lp);
  1035. my @args = ( $x, -1*$y, -text => $label, -tags => $tags,
  1036. -justify => 'center' );
  1037. push @args, ( -state => 'disabled' );
  1038. $self->createText ( @args );
  1039. }
  1040. # Return the bounding box of the edge
  1041. ($x1,$y1,$x2,$y2);
  1042. }
  1043. ######################################################################
  1044. # Parse the coordinates for an edge from the 'pos' string
  1045. #
  1046. ######################################################################
  1047. sub _parseEdgePos
  1048. {
  1049. my ($self, $pos) = @_;
  1050. # Note: Arrows can be at the start and end, i.e.
  1051. # pos = s,410,104 e,558,59 417,98 ...
  1052. # (See example graph 'graphs/directed/ldbxtried.dot')
  1053. # hash of start/end coords
  1054. # Example: e => [ 12, 3 ], s = [ 1, 3 ]
  1055. my %startEnd;
  1056. # Process all start/end points (could be none, 1, or 2)
  1057. while ( $pos =~ s/^([se])\s*\,\s*(\d+)\s*\,\s*(\d+)\s+// ) {
  1058. my ($where, $x, $y) = ($1, $2, $3);
  1059. $startEnd{$where} = [ $x, $y ];
  1060. }
  1061. my @loc = split(/ |,/, $pos);
  1062. my @coords = ();
  1063. while ( @loc >= 2 ) {
  1064. my ($x,$y) = splice(@loc,0,2);
  1065. push @coords, [$x,$y];
  1066. }
  1067. (\%startEnd, @coords);
  1068. }
  1069. ######################################################################
  1070. # Sub to make points on a curve, based on Bezier control points
  1071. # Inputs:
  1072. # $controlPoints: Array of control points (x/y P0,1,2,3)
  1073. # $tinc: Increment to use for t (t = 0 to 1 )
  1074. # $lastFlag: Flag = 1 to generate the last point (where t = 1)
  1075. #
  1076. # Output;
  1077. # @outputPoints: Array of points along the biezier curve
  1078. #
  1079. # Equations used
  1080. #Found Bezier Equations at http://pfaedit.sourceforge.net/bezier.html
  1081. #
  1082. # A cubic Bezier curve may be viewed as:
  1083. # x = ax*t3 + bx*t2 + cx*t +dx
  1084. # y = ay*t3 + by*t2 + cy*t +dy
  1085. #
  1086. # Where
  1087. #
  1088. # dx = P0.x
  1089. # dy = P0.y
  1090. # cx = 3*P1.x-3*P0.x
  1091. # cy = 3*P1.y-3*P0.y
  1092. # bx = 3*P2.x-6*P1.x+3*P0.x
  1093. # by = 3*P2.y-6*P1.y+3*P0.y
  1094. # ax = P3.x-3*P2.x+3*P1.x-P0.x
  1095. # ay = P3.y-3*P2.y+3*P1.y-P0.y
  1096. ######################################################################
  1097. sub _bezierInterpolate
  1098. {
  1099. my ($self,$controlPoints, $tinc, $lastFlag) = @_;
  1100. # interpolation constants
  1101. my ($ax,$bx,$cx,$dx);
  1102. my ($ay,$by,$cy,$dy);
  1103. $dx = $controlPoints->[0];
  1104. $cx = 3*$controlPoints->[2] - 3*$controlPoints->[0];
  1105. $bx = 3*$controlPoints->[4] - 6*$controlPoints->[2] + 3*$controlPoints->[0];
  1106. $ax = ( $controlPoints->[6] - 3*$controlPoints->[4] + 3*$controlPoints->[2]
  1107. - $controlPoints->[0] );
  1108. $dy = $controlPoints->[1];
  1109. $cy = 3*$controlPoints->[3] - 3*$controlPoints->[1];
  1110. $by = 3*$controlPoints->[5] - 6*$controlPoints->[3] + 3*$controlPoints->[1];
  1111. $ay = ( $controlPoints->[7] - 3*$controlPoints->[5] + 3*$controlPoints->[3]
  1112. - $controlPoints->[1] );
  1113. my @outputPoints;
  1114. for( my $t=0; $t <= 1; $t+=$tinc ){
  1115. # don't do the last point unless lastflag set
  1116. next if($t == 1 && !$lastFlag);
  1117. # Compute X point
  1118. push @outputPoints, ($ax*$t**3 + $bx*$t**2 + $cx*$t +$dx);
  1119. # Compute Y point
  1120. push @outputPoints, ($ay*$t**3 + $by*$t**2 + $cy*$t +$dy);
  1121. }
  1122. return @outputPoints;
  1123. }
  1124. ######################################################################
  1125. # Update scroll region to new bounds, to encompass
  1126. # the entire contents of the canvas
  1127. ######################################################################
  1128. sub _updateScrollRegion
  1129. {
  1130. my ($self) = @_;
  1131. # Ignore passed in in bbox, get a new one
  1132. my ($x1,$y1,$x2,$y2) = $self->bbox('all');
  1133. return 0 unless defined $x1;
  1134. # Set canvas size from graph bounding box
  1135. my $m = 0;#$self->{margin};
  1136. $self->configure ( -scrollregion => [ $x1-$m, $y1-$m, $x2+$m, $y2+$m ],
  1137. -confine => 1 );
  1138. # Reset original scale factor
  1139. $self->{_scaled} = 1.0;
  1140. 1;
  1141. }
  1142. ######################################################################
  1143. # Update the scale factor
  1144. #
  1145. # Called by operations that do scaling
  1146. ######################################################################
  1147. sub _scaleAndMoveView
  1148. {
  1149. my ($self, $scale, $x, $y) = @_;
  1150. $self->scale ( 'all' => 0, 0, $scale, $scale );
  1151. my $new_scaled = $self->{_scaled} * $scale;
  1152. #STDERR->printf ( "\nscaled: %s -> %s\n",
  1153. # $self->{_scaled}, $new_scaled );
  1154. # Scale the fonts:
  1155. my $fonts = $self->{fonts};
  1156. #print "new_scaled = $new_scaled\n";
  1157. foreach my $fontName ( keys %$fonts ) {
  1158. my $font = $fonts->{$fontName}{font};
  1159. my $origSize = $fonts->{$fontName}{origSize};
  1160. # Flag to indicate size is negative (i.e. specified in pixels)
  1161. my $negativeSize = $origSize < 0 ? -1 : 1;
  1162. $origSize = abs($origSize); # Make abs value for finding scale
  1163. # Fonts can't go below size 2, or they suddenly jump up to size 6...
  1164. my $newSize = max(2,int( $origSize*$new_scaled + 0.5));
  1165. $newSize *= $negativeSize;
  1166. $font->configure ( -size => $newSize );
  1167. #print "Font '$fontName' Origsize = $origSize, newsize $newSize, actual size ".$font->actual(-size)."\n";
  1168. }
  1169. $self->{_scaled} = $new_scaled;
  1170. # Reset scroll region
  1171. my @sr = $self->cget( '-scrollregion' );
  1172. my $sr = \@sr;
  1173. if ( @sr == 1 ) { $sr = $sr[0]; }
  1174. $_ *= $scale foreach ( @$sr );
  1175. $self->configure ( -scrollregion => $sr );
  1176. # Change the view to center on correct area
  1177. # $x and $y are expected to be coords in the pre-scaled system
  1178. my ($left, $right) = $self->xview;
  1179. my ($top, $bot) = $self->yview;
  1180. my $xpos = ($x*$scale-$sr->[0])/($sr->[2]-$sr->[0]) - ($right-$left)/2.0;
  1181. my $ypos = ($y*$scale-$sr->[1])/($sr->[3]-$sr->[1]) - ($bot-$top)/2.0;
  1182. $self->xview( moveto => $xpos );
  1183. $self->yview( moveto => $ypos );
  1184. #($left, $right) = $self->xview;
  1185. #($top, $bot) = $self->yview;
  1186. #STDERR->printf( "scaled: midx=%s midy=%s\n",
  1187. # ($left+$right)/2.0, ($top+$bot)/2.0 );
  1188. 1;
  1189. }
  1190. ######################################################################
  1191. # Setup some standard bindings.
  1192. #
  1193. # This enables some standard useful functionality for scrolling,
  1194. # zooming, etc.
  1195. #
  1196. # The bindings need to interfere as little as possible with typical
  1197. # bindings that might be employed in an application using this
  1198. # widget (e.g. Button-1).
  1199. #
  1200. # Also, creating these bindings (by calling this method) is strictly
  1201. # optional.
  1202. ######################################################################
  1203. sub createBindings
  1204. {
  1205. my ($self, %opt) = @_;
  1206. if ( scalar(keys %opt) == 0 # Empty options list
  1207. || defined $opt{'-default'} && $opt{'-default'} ) {
  1208. # Default zoom bindings
  1209. $opt{'-zoom'} = 1;
  1210. # Default scroll bindings
  1211. $opt{'-scroll'} = 1;
  1212. # Key-pad bindings
  1213. $opt{'-keypad'} = 1;
  1214. }
  1215. if ( defined $opt{'-zoom'} ) {
  1216. $self->_createZoomBindings( %opt );
  1217. }
  1218. if ( defined $opt{'-scroll'} ) {
  1219. $self->_createScrollBindings( %opt );
  1220. }
  1221. if ( defined $opt{'-keypad'} ) {
  1222. $self->_createKeypadBindings( %opt );
  1223. }
  1224. }
  1225. ######################################################################
  1226. # Setup bindings for zooming operations
  1227. #
  1228. # These are bound to a specific mouse button and optional modifiers.
  1229. # - To zoom in: drag out a box from top-left/right to bottom-right/left
  1230. # enclosing the new region to display
  1231. # - To zoom out: drag out a box from bottom-left/right to top-right/left.
  1232. # size of the box determines zoom out factor.
  1233. ######################################################################
  1234. sub _createZoomBindings
  1235. {
  1236. my ($self, %opt) = @_;
  1237. # Interpret zooming options
  1238. # What mouse button + modifiers starts zoom?
  1239. my $zoomSpec = $opt{'-zoom'};
  1240. die __PACKAGE__.": No -zoom option" unless defined $zoomSpec;
  1241. if ( $zoomSpec =~ /^\<.+\>$/ ) {
  1242. # This should be a partial bind event spec, e.g. <1>, or <Shift-3>
  1243. # -- it must end in a button number
  1244. die __PACKAGE__.": Illegal -zoom option"
  1245. unless ( $zoomSpec =~ /^\<.+\-\d\>$/ ||
  1246. $zoomSpec =~ /^\<\d\>$/ );
  1247. }
  1248. else {
  1249. # Anything else: use the default
  1250. $zoomSpec = '<Shift-2>';
  1251. }
  1252. # Color for zoom rect
  1253. my $zoomColor = $opt{'-zoomcolor'} || 'red';
  1254. # Initial press starts drawing zoom rect
  1255. my $startEvent = $zoomSpec;
  1256. $startEvent =~ s/(\d\>)$/ButtonPress-$1/;
  1257. #STDERR->printf ( "startEvent = $startEvent\n" );
  1258. $self->Tk::bind ( $startEvent => sub { $self->_startZoom ( $zoomSpec,
  1259. $zoomColor ) });
  1260. }
  1261. ######################################################################
  1262. # Called whenever a zoom event is started. This creates the initial
  1263. # zoom rectangle, and installs (temporary) bindings for mouse motion
  1264. # and release to drag out the zoom rect and then compute the zoom
  1265. # operation.
  1266. #
  1267. # The motion / button release bindings have to be installed temporarily
  1268. # so they don't conflict with other bindings (such as for scrolling
  1269. # or panning). The original bindings for those events have to be
  1270. # restored once the zoom operation is completed.
  1271. ######################################################################
  1272. sub _startZoom
  1273. {
  1274. my ($self, $zoomSpec, $zoomColor) = @_;
  1275. # Start of the zoom rectangle
  1276. my $x = $self->canvasx ( $Tk::event->x );
  1277. my $y = $self->canvasy ( $Tk::event->y );
  1278. my @zoomCoords = ( $x, $y, $x, $y );
  1279. my $zoomRect = $self->createRectangle
  1280. ( @zoomCoords, -outline => $zoomColor );
  1281. # Install the Motion binding to drag out the rectangle -- store the
  1282. # origin binding.
  1283. my $dragEvent = '<Motion>';
  1284. #STDERR->printf ( "dragEvent = $dragEvent\n" );
  1285. my $origDragBind = $self->Tk::bind ( $dragEvent );
  1286. $self->Tk::bind ( $dragEvent => sub {
  1287. $zoomCoords[2] = $self->canvasx ( $Tk::event->x );
  1288. $zoomCoords[3] = $self->canvasy ( $Tk::event->y );
  1289. $self->coords ( $zoomRect => @zoomCoords );
  1290. } );
  1291. # Releasing button finishes zoom rect, and causes zoom to happen.
  1292. my $stopEvent = $zoomSpec;
  1293. $stopEvent =~ s/^\<.*(\d\>)$/<ButtonRelease-$1/;
  1294. #STDERR->printf ( "stopEvent = $stopEvent\n" );
  1295. my $threshold = 10;
  1296. my $origStopBind = $self->Tk::bind ( $stopEvent );
  1297. $self->Tk::bind ( $stopEvent => sub {
  1298. # Delete the rect
  1299. $self->delete ( $zoomRect );
  1300. # Restore original bindings
  1301. $self->Tk::bind ( $dragEvent => $origDragBind );
  1302. $self->Tk::bind ( $stopEvent => $origStopBind );
  1303. # Was the rectangle big enough?
  1304. my $dx = $zoomCoords[2] - $zoomCoords[0];
  1305. my $dy = $zoomCoords[3] - $zoomCoords[1];
  1306. return if ( abs($dx) < $threshold ||
  1307. abs($dy) < $threshold );
  1308. # Find the zooming factor
  1309. my $zx = $self->width() / abs($dx);
  1310. my $zy = $self->height() / abs($dy);
  1311. my $scale = min($zx, $zy);
  1312. # Zoom in our out?
  1313. # top->bottom drag means out,
  1314. # bottom->top drag means in.
  1315. # (0,0) is top left, so $dy > 0 means top->bottom
  1316. if ( $dy > 0 ) {
  1317. # Zooming in!
  1318. #STDERR->printf ( "Zooming in: $scale\n" );
  1319. } else {
  1320. # Zooming out!
  1321. $scale = 1 - 1.0 / $scale;
  1322. #STDERR->printf ( "Zooming out: $scale\n" );
  1323. }
  1324. # Scale everying up / down
  1325. $self->_scaleAndMoveView
  1326. ( $scale,
  1327. ($zoomCoords[0]+$zoomCoords[2])/2.0,
  1328. ($zoomCoords[1]+$zoomCoords[3])/2.0 );
  1329. });
  1330. 1;
  1331. }
  1332. ######################################################################
  1333. # Setup bindings for scrolling / panning operations
  1334. #
  1335. ######################################################################
  1336. sub _createScrollBindings
  1337. {
  1338. my ($self, %opt) = @_;
  1339. # Interpret scrolling options
  1340. # What mouse button + modifiers starts scroll?
  1341. my $scrollSpec = $opt{'-scroll'};
  1342. die __PACKAGE__.": No -scroll option" unless defined $scrollSpec;
  1343. if ( $scrollSpec =~ /^\<.+\>$/ ) {
  1344. # This should be a partial bind event spec, e.g. <1>, or <Shift-3>
  1345. # -- it must end in a button number
  1346. die __PACKAGE__.": Illegal -scroll option"
  1347. unless ( $scrollSpec =~ /^\<.+\-\d\>$/ ||
  1348. $scrollSpec =~ /^\<\d\>$/ );
  1349. }
  1350. else {
  1351. # Anything else: use the default
  1352. $scrollSpec = '<2>';
  1353. }
  1354. # Initial press starts panning
  1355. my $startEvent = $scrollSpec;
  1356. $startEvent =~ s/(\d\>)$/ButtonPress-$1/;
  1357. #STDERR->printf ( "startEvent = $startEvent\n" );
  1358. $self->Tk::bind ( $startEvent => sub { $self->_startScroll
  1359. ( $scrollSpec ) } );
  1360. }
  1361. ######################################################################
  1362. # Called whenever a scroll event is started. This installs (temporary)
  1363. # bindings for mouse motion and release to complete the scrolling.
  1364. #
  1365. # The motion / button release bindings have to be installed temporarily
  1366. # so they don't conflict with other bindings (such as for zooming)
  1367. # The original bindings for those events have to be restored once the
  1368. # zoom operation is completed.
  1369. ######################################################################
  1370. sub _startScroll
  1371. {
  1372. my ($self, $scrollSpec) = @_;
  1373. # State data to keep track of scroll operation
  1374. my $startx = $self->canvasx ( $Tk::event->x );
  1375. my $starty = $self->canvasy ( $Tk::event->y );
  1376. # Dragging causes scroll to happen
  1377. my $dragEvent = '<Motion>';
  1378. #STDERR->printf ( "dragEvent = $dragEvent\n" );
  1379. my $origDragBind = $self->Tk::bind ( $dragEvent );
  1380. $self->Tk::bind ( $dragEvent => sub {
  1381. my $x = $self->canvasx ( $Tk::event->x );
  1382. my $y = $self->canvasy ( $Tk::event->y );
  1383. # Compute scroll ammount
  1384. my $dx = $x - $startx;
  1385. my $dy = $y - $starty;
  1386. #STDERR->printf ( "Scrolling: dx=$dx, dy=$dy\n" );
  1387. # Feels better is scroll speed is reduced.
  1388. # Also is more natural inverted, feeld like dragging
  1389. # the canvas
  1390. $dx *= -.9;
  1391. $dy *= -.9;
  1392. my ($xv) = $self->xview();
  1393. my ($yv) = $self->yview();
  1394. my @sr = $self->cget( '-scrollregion' );
  1395. #STDERR->printf ( " xv=$xv, yv=$yv\n" );
  1396. my $xpct = $xv + $dx/($sr[2]-$sr[0]);
  1397. my $ypct = $yv + $dy/($sr[3]-$sr[1]);
  1398. #STDERR->printf ( " xpct=$xpct, ypct=$ypct\n" );
  1399. $self->xview ( moveto => $xpct );
  1400. $self->yview ( moveto => $ypct );
  1401. # This is the new reference point for
  1402. # next motion event
  1403. $startx = $x;
  1404. $starty = $y;
  1405. #STDERR->printf ( " scrolled\n" );
  1406. } );
  1407. # Releasing button finishes scrolling
  1408. my $stopEvent = $scrollSpec;
  1409. $stopEvent =~ s/^\<.*(\d\>)$/<ButtonRelease-$1/;
  1410. #STDERR->printf ( "stopEvent = $stopEvent\n" );
  1411. my $origStopBind = $self->Tk::bind ( $stopEvent );
  1412. $self->Tk::bind ( $stopEvent => sub {
  1413. # Restore original bindings
  1414. $self->Tk::bind ( $dragEvent => $origDragBind );
  1415. $self->Tk::bind ( $stopEvent => $origStopBind );
  1416. } );
  1417. 1;
  1418. }
  1419. ######################################################################
  1420. # Setup bindings for keypad keys to do zooming and scrolling
  1421. #
  1422. # This binds +/- on the keypad to zoom in and out, and the arrow/number
  1423. # keys to scroll.
  1424. ######################################################################
  1425. sub _createKeypadBindings
  1426. {
  1427. my ($self, %opt) = @_;
  1428. $self->Tk::bind ( '<KeyPress-KP_Add>' =>
  1429. sub { $self->zoom( -in => 1.15 ) } );
  1430. $self->Tk::bind ( '<KeyPress-KP_Subtract>' =>
  1431. sub { $self->zoom( -out => 1.15 ) } );
  1432. $self->Tk::bind ( '<KeyPress-KP_1>' =>
  1433. sub { $self->xview( scroll => -1, 'units' );
  1434. $self->yview( scroll => 1, 'units' ) } );
  1435. $self->Tk::bind ( '<KeyPress-KP_2>' =>
  1436. sub { $self->yview( scroll => 1, 'units' ) } );
  1437. $self->Tk::bind ( '<KeyPress-KP_3>' =>
  1438. sub { $self->xview( scroll => 1, 'units' );
  1439. $self->yview( scroll => 1, 'units' ) } );
  1440. $self->Tk::bind ( '<KeyPress-KP_4>' =>
  1441. sub { $self->xview( scroll => -1, 'units' ) } );
  1442. $self->Tk::bind ( '<KeyPress-KP_6>' =>
  1443. sub { $self->xview( scroll => 1, 'units' ) } );
  1444. $self->Tk::bind ( '<KeyPress-KP_7>' =>
  1445. sub { $self->xview( scroll => -1, 'units' );
  1446. $self->yview( scroll => -1, 'units' ) } );
  1447. $self->Tk::bind ( '<KeyPress-KP_8>' =>
  1448. sub { $self->yview( scroll => -1, 'units' ) } );
  1449. $self->Tk::bind ( '<KeyPress-KP_9>' =>
  1450. sub { $self->xview( scroll => 1, 'units' );
  1451. $self->yview( scroll => -1, 'units' ) } );
  1452. 1;
  1453. }
  1454. #######################################################################
  1455. ## Setup binding for 'fit' operation
  1456. ##
  1457. ## 'fit' scales the entire contents of the graph to fit within the
  1458. ## visible portion of the canvas.
  1459. #######################################################################
  1460. #sub _createFitBindings
  1461. #{
  1462. # my ($self, %opt) = @_;
  1463. #
  1464. # # Interpret options
  1465. #
  1466. # # What event to bind to?
  1467. # my $fitEvent = $opt{'-fit'};
  1468. # die __PACKAGE__.": No -fit option" unless defined $fitEvent;
  1469. # if ( $fitEvent =~ /^\<.+\>$/ ) {
  1470. # die __PACKAGE__.": Illegal -fit option"
  1471. # unless ( $fitEvent =~ /^\<.+\>$/ );
  1472. # }
  1473. # else {
  1474. # # Anything else: use the default
  1475. # $fitEvent = '<Key-f>';
  1476. # }
  1477. #
  1478. # STDERR->printf ( "fit event = $fitEvent\n" );
  1479. # $self->Tk::bind ( $fitEvent => sub { $self->fit( 'all' ) });
  1480. # 1;
  1481. #}
  1482. ######################################################################
  1483. # Scale the graph to fit within the canvas
  1484. #
  1485. ######################################################################
  1486. sub fit
  1487. {
  1488. my ($self, $idOrTag) = @_;
  1489. $idOrTag = 'all' unless defined $idOrTag;
  1490. my $w = $self->width();
  1491. my $h = $self->height();
  1492. my ($x1,$y1,$x2,$y2) = $self->bbox( $idOrTag );
  1493. return 0 unless ( defined $x1 && defined $x2 &&
  1494. defined $y1 && defined $y2 );
  1495. my $dx = abs($x2 - $x1);
  1496. my $dy = abs($y2 - $y1);
  1497. my $scalex = $w / $dx;
  1498. my $scaley = $h / $dy;
  1499. my $scale = min ( $scalex, $scaley );
  1500. if ( $scalex >= 1.0 && $scaley >= 1.0 ) {
  1501. $scale = max ( $scalex, $scaley );
  1502. }
  1503. $self->_scaleAndMoveView ( $scale, 0, 0 );
  1504. $self->xview( moveto => 0 );
  1505. $self->yview( moveto => 0 );
  1506. 1;
  1507. }
  1508. ######################################################################
  1509. # Zoom in or out, keep top-level centered.
  1510. #
  1511. ######################################################################
  1512. sub zoom
  1513. {
  1514. my ($self, $dir, $scale) = @_;
  1515. if ( $dir eq '-in' ) {
  1516. # Make things bigger
  1517. }
  1518. elsif ( $dir eq '-out' ) {
  1519. # Make things smaller
  1520. $scale = 1 / $scale;
  1521. }
  1522. my ($xv1,$xv2) = $self->xview();
  1523. my ($yv1,$yv2) = $self->yview();
  1524. my $xvm = ($xv2 + $xv1)/2.0;
  1525. my $yvm = ($yv2 + $yv1)/2.0;
  1526. my ($l, $t, $r, $b) = $self->cget( -scrollregion );
  1527. $self->_scaleAndMoveView ( $scale,
  1528. $l + $xvm *($r - $l),
  1529. $t + $yvm *($b - $t) );
  1530. 1;
  1531. }
  1532. sub zoomTo
  1533. {
  1534. my ($self, $tagOrId) = @_;
  1535. $self->fit();
  1536. my @bb = $self->bbox( $tagOrId );
  1537. return unless @bb == 4 && defined($bb[0]);
  1538. my $w = $bb[2] - $bb[0];
  1539. my $h = $bb[3] - $bb[1];
  1540. my $scale = 2;
  1541. my $x1 = $bb[0] - $scale * $w;
  1542. my $y1 = $bb[1] - $scale * $h;
  1543. my $x2 = $bb[2] + $scale * $w;
  1544. my $y2 = $bb[3] + $scale * $h;
  1545. #STDERR->printf("zoomTo: bb = @bb\n".
  1546. # " w=$w h=$h\n".
  1547. # " x1,$y1, $x2,$y2\n" );
  1548. $self->zoomToRect( $x1, $y1, $x2, $y2 );
  1549. }
  1550. sub zoomToRect
  1551. {
  1552. my ($self, @box) = @_;
  1553. # make sure x1,y1 = lower left, x2,y2 = upper right
  1554. ($box[0],$box[2]) = ($box[2],$box[0]) if $box[2] < $box[0];
  1555. ($box[1],$box[3]) = ($box[3],$box[1]) if $box[3] < $box[1];
  1556. # What is the scale relative to current bounds?
  1557. my ($l,$r) = $self->xview;
  1558. my ($t,$b) = $self->yview;
  1559. my $curr_w = $r - $l;
  1560. my $curr_h = $b - $t;
  1561. my @sr = $self->cget( -scrollregion );
  1562. my $sr_w = $sr[2] - $sr[0];
  1563. my $sr_h = $sr[3] - $sr[1];
  1564. my $new_l = max(0.0,$box[0] / $sr_w);
  1565. my $new_t = max(0.0,$box[1] / $sr_h);
  1566. my $new_r = min(1.0,$box[2] / $sr_w);
  1567. my $new_b = min(1.0,$box[3] / $sr_h);
  1568. my $new_w = $new_r - $new_l;
  1569. my $new_h = $new_b - $new_t;
  1570. my $scale = max( $curr_w/$new_w, $curr_h/$new_h );
  1571. $self->_scaleAndMoveView( $scale,
  1572. ($box[0] + $box[2])/2.0,
  1573. ($box[1] + $box[3])/2.0 );
  1574. 1;
  1575. }
  1576. ######################################################################
  1577. # Over-ridden createText Method
  1578. #
  1579. # Handles the embedded \l\r\n graphViz control characters
  1580. ######################################################################
  1581. sub createText
  1582. {
  1583. my ($self, $x, $y, %attrs) = @_;
  1584. if( defined($attrs{-text}) ) {
  1585. # Set Justification, based on any \n \l \r in the text label
  1586. my $label = $attrs{-text};
  1587. my $justify = 'center';
  1588. # Per the dotguide.pdf, a '\l', '\r', or '\n' is
  1589. # just a line terminator, not a newline. So in cases
  1590. # where the label ends in one of these characters, we are
  1591. # going to remove the newline char later
  1592. my $removeNewline;
  1593. if( $label =~ /\\[nlr]$/){
  1594. $removeNewline = 1;
  1595. }
  1596. if( $label =~ s/\\l/\n/g ){
  1597. $justify = 'left';
  1598. }
  1599. if( $label =~ s/\\r/\n/g ){
  1600. $justify = 'right';
  1601. }
  1602. # Change \n to actual \n
  1603. if( $label =~ s/\\n/\n/g ){
  1604. $justify = 'center';
  1605. }
  1606. # remove ending newline if flag set
  1607. if( $removeNewline){
  1608. $label =~ s/\n$//;
  1609. }
  1610. # Fix any escaped chars
  1611. # like \} to }, and \\{ to \{
  1612. $label =~ s/\\(?!\\)(.)/$1/g;
  1613. $attrs{-text} = $label;
  1614. $attrs{-justify} = $justify;
  1615. # Fix the label tag, if there is one
  1616. my $tags;
  1617. if( defined($tags = $attrs{-tags})){
  1618. my %tags = (@$tags);
  1619. $tags{label} = $label if(defined($tags{label}));
  1620. $attrs{-tags} = [%tags];
  1621. }
  1622. # Get the default font, if not defined already
  1623. my $fonts = $self->{fonts};
  1624. unless(defined($fonts->{_default}) ){
  1625. # Create dummy item, so we can see what font is used
  1626. my $dummyID = $self->SUPER::createText
  1627. ( 100,25, -text => "You should never see this" );
  1628. my $defaultfont = $self->itemcget($dummyID,-font);
  1629. # Make a copy that we will mess with:
  1630. $defaultfont = $defaultfont->Clone;
  1631. $fonts->{_default}{font} = $defaultfont;
  1632. $fonts->{_default}{origSize} = $defaultfont->actual(-size);
  1633. # Delete the dummy item
  1634. $self->delete($dummyID);
  1635. }
  1636. # Assign the default font
  1637. unless( defined($attrs{-font}) ){
  1638. $attrs{-font} = $fonts->{_default}{font};
  1639. }
  1640. }
  1641. # Call Inherited createText
  1642. $self->SUPER::createText ( $x, $y, %attrs );
  1643. }
  1644. ######################################################################
  1645. # Sub to try a color name, returns the color name if recognized
  1646. # 'black' and issues a warning if not
  1647. ######################################################################
  1648. sub _tryColor
  1649. {
  1650. my ($self,$color) = @_;
  1651. return undef unless defined($color);
  1652. # Special cases
  1653. if( $color eq 'crimson' ) {
  1654. # crimison not defined in Tk, so use GraphViz's definition
  1655. return sprintf("#%02X%02x%02X", 246,231,220);
  1656. }
  1657. elsif( $color =~ /^(-?\d+\.?\d*)\s+(-?\d+\.?\d*)\s+(-?\d+\.?\d*)\s*$/ ) {
  1658. # three color numbers
  1659. my($hue,$sat,$bright) = ($1,$2,$3);
  1660. return $self->_hsb2rgb($hue,$sat,$bright);
  1661. }
  1662. # Don't check color if it is a hex rgb value
  1663. unless( $color =~ /^\#\w+/ ) {
  1664. my $tryColor = $color;
  1665. $tryColor =~ s/\_//g; # get rid of any underscores
  1666. my @rgb;
  1667. eval { @rgb = $self->rgb($tryColor); };
  1668. if ($@) {
  1669. warn __PACKAGE__.": Unkown color $color, using black instead\n";
  1670. $color = 'black';
  1671. } else {
  1672. $color = $tryColor;
  1673. }
  1674. }
  1675. $color;
  1676. }
  1677. ######################################################################
  1678. # Sub to convert from Hue-Sat-Brightness to RGB hex number
  1679. #
  1680. ######################################################################
  1681. sub _hsb2rgb
  1682. {
  1683. my ($self,$h,$s,$v) = @_;
  1684. my ($r,$g,$b);
  1685. if( $s <= 0){
  1686. $v = int($v);
  1687. ($r,$g,$b) = ($v,$v,$v);
  1688. }
  1689. else{
  1690. if( $h >= 1){
  1691. $h = 0;
  1692. }
  1693. $h = 6*$h;
  1694. my $f = $h - int($h);
  1695. my $p = $v * (1 - $s);
  1696. my $q = $v * ( 1 - ($s * $f));
  1697. my $t = $v * ( 1 - ($s * (1-$f)));
  1698. my $i = int($h);
  1699. if( $i == 0){ ($r,$g,$b) = ($v, $t, $p);}
  1700. elsif( $i == 1){ ($r,$g,$b) = ($q, $v, $p);}
  1701. elsif( $i == 2){($r,$g,$b) = ($p, $v, $t);}
  1702. elsif( $i == 3){($r,$g,$b) = ($p, $q, $v);}
  1703. elsif( $i == 4){($r,$g,$b) = ($t, $p, $v);}
  1704. elsif( $i == 5){($r,$g,$b) = ($v, $p, $q);}
  1705. }
  1706. sprintf("#%02X%02x%02X", 255*$r, 255*$g, 244*$b);
  1707. }
  1708. ######################################################################
  1709. # Utility functions
  1710. ######################################################################
  1711. sub min {
  1712. if ( defined($_[0]) ) {
  1713. if ( defined($_[1]) ) { return ($_[0] < $_[1])? $_[0] : $_[1]; }
  1714. else { return $_[0]; }
  1715. } else {
  1716. if ( defined($_[1]) ) { return $_[1]; }
  1717. else { return undef; }
  1718. }
  1719. }
  1720. sub max {
  1721. if ( defined($_[0]) ) {
  1722. if ( defined($_[1]) ) { return ($_[0] > $_[1])? $_[0] : $_[1]; }
  1723. else { return $_[0]; }
  1724. } else {
  1725. if ( defined($_[1]) ) { return $_[1]; }
  1726. else { return undef; }
  1727. }
  1728. }
  1729. __END__
  1730. =head1 NAME
  1731. Tk::GraphViz - Render an interactive GraphViz graph
  1732. =head1 SYNOPSIS
  1733. use Tk::GraphViz;
  1734. my $gv = $mw->GraphViz ( qw/-width 300 -height 300/ )
  1735. ->pack ( qw/-expand yes -fill both/ );
  1736. $gv->show ( $dotfile );
  1737. =head1 DESCRIPTION
  1738. The B<GraphViz> widget is derived from B<Tk::Canvas>. It adds the ability to render graphs in the canvas. The graphs can be specified either using the B<DOT> graph-description language, or using via a B<GraphViz> object.
  1739. When B<show()> is called, the graph is passed to the B<dot> command to generate the layout info. That info is then used to create rectangles, lines, etc in the canvas that reflect the generated layout.
  1740. Once the items have been created in the graph, they can be used like any normal canvas items: events can be bound, etc. In this way, interactive graphing applications can be created very easily.
  1741. =head1 METHODS
  1742. =head2 $gv->show ( graph, ?opt => val, ...? )
  1743. Renders the given graph in the canvas. The graph itself can be specified in a number of formats. 'graph' can be one of the following:
  1744. =over 4
  1745. =item - An instance of the GraphViz class (or subclass thereof)
  1746. =item - A scalar containing a graph in DOT format. The scalar must match /^\s*(?:di)?graph /.
  1747. =item - An instance of the IO::Handle class (or subclass thereof), from which to read a graph in DOT format.
  1748. =item - The name / path of a file that contains a graph in DOT format.
  1749. =back
  1750. show() will recognize some options that control how the graph is rendered, etc. The recognized options:
  1751. =over 4
  1752. =item layout => CMD
  1753. Specifies an alternate command to invoke to generate the layout of the graph. If not given, then default is 'dot'. This can be used, for example, to use 'neato' instead of 'dot'.
  1754. =item graphattrs => [ name => value, ... ]
  1755. Allows additional default graph attributes to be specified. Each name => value pair will be passed to dot as '-Gname=value' on the command-line.
  1756. =item nodeattrs => [ name => value, ... ]
  1757. Allows additional default node attributes to be specified. Each name => value pair will be passed to dot as '-Nname=value' on the command-line.
  1758. =item edgeattrs => [ name => value, ... ]
  1759. Allows additional default edge attributes to be specified. Each name => value pair will be passed to dot as '-Ename=value' on the command-line.
  1760. =back
  1761. For example, to use neato to generate a layout with non-overlapping nodes and spline edges:
  1762. $gv->show ( $file, layout => 'neato',
  1763. graphattrs => [qw( overlap false spline true )] );
  1764. =head2 $gv->createBindings ( ?option => value? )
  1765. The Tk::GraphViz canvas can be configured with some bindings for standard operations. If no options are given, the default bindings for zooming and scrolling will be enabled. Alternative bindings can be specified via these options:
  1766. =over 4
  1767. =item -zoom => I<true>
  1768. Creates the default bindings for zooming. Zooming in or out in the canvas will be bound to <Shift-2> (Shift + mouse button 2). To zoom in, click and drag out a zoom rectangle from top left to bottom right. To zoom out, click and drag out a zoom rectangle from bottom left to top right.
  1769. =item -zoom => I<spec>
  1770. This will bind zooming to an alternative event sequence. Examples:
  1771. -zoom => '<1>' # Zoom on mouse button 1
  1772. -zoom => '<Ctrl-3>' # Zoom on Ctrl + mouse button 3
  1773. =item -scroll => I<true>
  1774. Creates the default bindings for scrolling / panning. Scrolling the canvas will be bound to <2> (Mouse button 2).
  1775. =item -scroll => I<spec>
  1776. This will bind scrolling to an alternative event sequence. Examples:
  1777. -scroll => '<1>' # Scroll on mouse button 1
  1778. -scroll => '<Ctrl-3>' # Scroll on Ctrl + mouse button 3
  1779. =item -keypad => I<true>
  1780. Binds the keypad arrow / number keys to scroll the canvas, and the keypad +/- keys to zoom in and out. Note that the canvas must have the keyboard focus for these bindings to be activated. This is done by default when createBindings() is called without any options.
  1781. =back
  1782. =head2 $gv->fit()
  1783. Scales all of the elements in the canvas to fit the canvas' width and height.
  1784. =head2 $gv->zoom( -in => factor )
  1785. Zoom in by scaling everything up by the given scale factor. The factor should be > 1.0 in order to get reasonable behavior.
  1786. =head2 $gv->zoom( -out => factor )
  1787. Zoom out by scaling everything down by the given scale factor. This is equivalent to
  1788. $gv->zoom ( -in => 1/factor )
  1789. The factor show be > 1.0 in order to get reasonable behavior.
  1790. =head1 TAGS
  1791. In order to facilitate binding, etc, all of the graph elements (nodes, edges, subgraphs) that a created in the cavas. Specific tags are given to each class of element. Additionally, all attributes attached to an element in the graph description (e.g. 'color', 'style') will be included as tags.
  1792. =head2 Nodes
  1793. Node elements are identified with a 'node' tag. For example, to bind something to all nodes in a graph:
  1794. $gv->bind ( 'node', '<Any-Enter>', sub { ... } );
  1795. The value of the 'node' tag is the name of the node in the graph (which is not equivalent to the node label -- that is the 'label' tag)
  1796. =head2 Edges
  1797. Edge elements are identified with a 'edge' tag. For example, to bind something to all edges in a graph:
  1798. $gv->bind ( 'edge', '<Any-Enter>', sub { ... } );
  1799. The value of the 'edge' tag is an a string of the form "node1 node2", where node1 and node2 are the names of the respective nodes. To make it convenient to get the individual node names, the edge also has tags 'node1' and 'node2', which give the node names separately.
  1800. =head2 Subgraphs
  1801. Subgraph elements are identified with a 'subgraph' tag. The value of the 'subgraph' is the name of the subgraph / cluster.
  1802. =head1 EXAMPLES
  1803. The following example creates a GraphViz widgets to display a graph from a file specified on the command line. Whenever a node is clicked, the node name and label are printed to stdout:
  1804. use GraphViz;
  1805. use Tk;
  1806. my $mw = new MainWindow ();
  1807. my $gv = $mw->Scrolled ( 'GraphViz',
  1808. -background => 'white',
  1809. -scrollbars => 'sw' )
  1810. ->pack ( -expand => '1', -fill => 'both' );
  1811. $gv->bind ( 'node', '<Button-1>', sub {
  1812. my @tags = $gv->gettags('current');
  1813. push @tags, undef unless (@tags % 2) == 0;
  1814. my %tags = @tags;
  1815. printf ( "Clicked node: '%s' => %s\n",
  1816. $tags{node}, $tags{label} );
  1817. } );
  1818. $gv->show ( shift );
  1819. MainLoop;
  1820. =head1 BUGS AND LIMITATIONS
  1821. Lots of DOT language features not yet implemented
  1822. =over 4
  1823. =item Various node shapes and attributes: polygon, skew, ...
  1824. =item Edge arrow head types
  1825. =head1 ACKNOWLEDGEMENTS
  1826. See http://www.graphviz.org/ for more info on the graphviz tools.
  1827. =head1 AUTHOR
  1828. Jeremy Slade E<lt>jeremy@jkslade.netE<gt>
  1829. Other contributors:
  1830. Mike Castle,
  1831. John Cerney,
  1832. Phi Kasten,
  1833. Jogi Kuenstner
  1834. Tobias Lorenz,
  1835. Charles Minc,
  1836. Reinier Post,
  1837. Slaven Rezic
  1838. =head1 COPYRIGHT AND LICENSE
  1839. Copyright 2003-2008 by Jeremy Slade
  1840. This library is free software; you can redistribute it and/or modify
  1841. it under the same terms as Perl itself.
  1842. =cut