PageRenderTime 64ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 1ms

/Tk-GraphViz-1.01/GraphViz.pm

#
Perl | 2320 lines | 1429 code | 424 blank | 467 comment | 205 complexity | 8c50d5ffd7bd13b855c5d479353a96d2 MD5 | raw file

Large files files are truncated, but you can click here to view the full 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' ) } );

Large files files are truncated, but you can click here to view the full file