Files
devops/perl/lib/Graph-Easy-0.76/blib/lib/Graph/Easy/As_ascii.pm
2025-09-17 16:08:16 +08:00

1429 lines
38 KiB
Perl
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#############################################################################
# Render Nodes/Edges/Cells as ASCII/Unicode box drawing art
#
# (c) by Tels 2004-2007. Part of Graph::Easy
#############################################################################
package Graph::Easy::As_ascii;
$VERSION = '0.76';
use utf8;
#############################################################################
#############################################################################
package Graph::Easy::Edge::Cell;
use strict;
use warnings;
my $edge_styles = [
{
# style hor, ver, cross, corner (SE, SW, NE, NW)
'solid' => [ '--', "|", '+', '+','+','+','+' ], # simple line
'double' => [ '==', "H", "#", '#','#','#','#' ], # double line
'double-dash' => [ '= ', '"', "#", '#','#','#','#' ], # double dashed line
'dotted' => [ '..', ":", ':', '.','.','.','.' ], # dotted
'dashed' => [ '- ', "'", '+', '+','+','+','+' ], # dashed
'dot-dash' => [ '.-', "!", '+', '+','+','+','+' ], # dot-dash
'dot-dot-dash' => [ '..-', "!", '+', '+','+','+','+' ], # dot-dot-dash
'wave' => [ '~~', "}", '+', '*','*','*','*' ], # wave
'bold' => [ '##', "#", '#', '#','#','#','#' ], # bold
'bold-dash' => [ '# ', "#", '#', '#','#','#','#' ], # bold-dash
'wide' => [ '##', "#", '#', '#','#','#','#' ], # wide
'broad' => [ '##', "#", '#', '#','#','#','#' ], # broad
},
{
# style hor, ver, cross, corner (SE, SW, NE, NW)
'solid' => [ '─', '│', '┼', '┌', '┐', '└', '┘' ],
'double' => [ '═', '║', '╬', '╔', '╗', '╚', '╝' ],
'double-dash' => [ '═'.' ', '∥', '╬', '╔', '╗', '╚', '╝' ], # double dashed
'dotted' => [ '·', ':', '┼', '┌', '┐', '└', '┘' ], # dotted
'dashed' => [ '╴', '╵', '┘', '┌', '┐', '╵', '┘' ], # dashed
'dot-dash' => [ '·'.'-', "!", '┼', '┌', '┐', '└', '┘' ], # dot-dash
'dot-dot-dash' => [ ('·' x 2).'-', "!", '┼', '┌', '┐', '└', '┘' ], # dot-dot-dash
'wave' => [ '', '≀', '┼', '┌', '┐', '└', '┘' ], # wave
'bold' => [ '━', '┃', '╋', '┏', '┓', '┗', '┛' ], # bold
'bold-dash' => [ '━'.' ', '╻', '╋', '┏', '┓', '┗', '┛' ], # bold-dash
'broad' => [ '▬', '▮', '█', '█', '█', '█', '█' ], # wide
'wide' => [ '█', '█', '█', '█', '█', '█', '█' ], # broad
# these two make it nec. to support multi-line styles for the vertical edge pieces
# 'broad-dash' => [ '◼', '◼', '◼', '◼', '◼', '◼', '◼' ], # broad-dash
# 'wide-dash' => [ ('█'x 2) .' ', '█', '█', '█', '█', '█', '█' ], # wide-dash
},
];
sub _edge_style
{
my ($self, $st) = @_;
my $g = $self->{graph}->{_ascii_style} || 0;
$st = $self->{style} unless defined $st;
$edge_styles->[$g]->{ $st };
}
# | | | | : } |
# ===+=== ###+### ....!.... ~~~+~~~ ----+--- ...+... .-.+.-.-
# | | | | : { |
my $cross_styles = [
# normal cross
[
{
'boldsolid' => '┿',
'solidbold' => '╂',
'doublesolid' => '╪',
'soliddouble' => '╫',
'dashedsolid' => '┤',
'soliddashed' => '┴',
'doubledashed' => '╧',
'dasheddouble' => '╢',
},
{
'boldsolid' => '+',
'dashedsolid' => '+',
'dottedsolid' => '!',
'dottedwave' => '+',
'doublesolid' => '+',
'dot-dashsolid' => '+',
'dot-dot-dashsolid' => '+',
'soliddotted' => '+',
'solidwave' => '+',
'soliddashed' => '+',
'soliddouble' => 'H',
'wavesolid' => '+',
},
],
undef, # HOR, cannot happen
undef, # VER, cannot happen
undef,
undef,
undef,
undef,
# S_E_W -+-
# |
[
{
'solidsolid' => '┬',
'boldbold' => '┳',
'doubledouble' => '╦',
'dasheddashed' => '╴',
'dotteddotted' => '·',
},
],
# N_E_W |
# -+-
[
{
'solidsolid' => '┴',
'boldbold' => '┻',
'doubledouble' => '╩',
'dotteddotted' => '·',
},
],
# E_N_S |
# +-
# |
[
{
'solidsolid' => '├',
'boldbold' => '┣',
'doubledouble' => '╠',
'dotteddotted' => ':',
},
],
# W_N_S |
# -+
# |
[
{
'solidsolid' => '┤',
'boldbold' => '┫',
'doubledouble' => '╣',
'dotteddotted' => ':',
},
] ];
sub _arrow_style
{
my $self = shift;
my $edge = $self->{edge};
my $as = $edge->attribute('arrowstyle');
$as = 'none' if $edge->{undirected};
$as;
}
sub _arrow_shape
{
my $self = shift;
my $edge = $self->{edge};
my $as = $edge->attribute('arrowshape');
$as;
}
sub _cross_style
{
my ($self, $st, $corner_type) = @_;
my $g = $self->{graph}->{_ascii_style} || 0;
# 0 => 1, 1 => 0
$g = 1 - $g;
# for ASCII, one style fist all (e.g a joint has still "+" as corner)
$corner_type = 0 unless defined $corner_type;
$corner_type = 0 if $g == 1;
$cross_styles->[$corner_type]->[$g]->{ $st };
}
sub _insert_label
{
my ($self, $fb, $xs, $ys, $ws, $hs, $align_ver) = @_;
my $align = $self->{edge}->attribute('align');
my ($lines,$aligns) = $self->_aligned_label($align);
$ys = $self->{h} - scalar @$lines + $ys if $ys < 0;
$ws ||= 0; $hs ||= 0;
my $w = $self->{w} - $ws - $xs;
my $h = $self->{h} - $hs - $ys;
$self->_printfb_aligned ($fb, $xs, $ys, $w, $h, $lines, $aligns, $align_ver);
}
sub _draw_hor
{
# draw a HOR edge piece
my ($self, $fb) = @_;
my $style = $self->_edge_style();
my $w = $self->{w};
# '-' => '-----', '.-' => '.-.-.-'
# "(2 + ... )" to get space for the offset
my $len = length($style->[0]);
my $line = $style->[0] x (2 + $w / $len);
# '.-.-.-' => '-.-.-' if $x % $ofs == 1 (e.g. on odd positions)
my $ofs = $self->{rx} % $len;
my $type = ($self->{type} & (~EDGE_MISC_MASK));
substr($line,0,$ofs) = '' if $ofs != 0
&& ($type != EDGE_SHORT_E && $type != EDGE_SHORT_W);
$line = substr($line, 0, $w) if length($line) > $w;
# handle start/end point
my $flags = $self->{type} & EDGE_FLAG_MASK;
my $as = $self->_arrow_style();
my $ashape; $ashape = $self->_arrow_shape() if $as ne 'none';
my $x = 0; # offset for the edge line
my $xs = 1; # offset for the edge label
my $xr = 0; # right offset for label
if (($flags & EDGE_START_W) != 0)
{
$x++; chop($line); # ' ---'
$xs++;
}
if (($flags & EDGE_START_E) != 0)
{
chop($line); # '--- '
}
if (($flags & EDGE_END_E) != 0)
{
# '--> '
chop($line);
substr($line,-1,1) = $self->_arrow($as, ARROW_RIGHT, $ashape) if $as ne 'none';
$xr++;
}
if (($flags & EDGE_END_W) != 0)
{
# ' <--'
substr($line,0,1) = ' ' if $as eq 'none';
substr($line,0,2) = ' ' . $self->_arrow($as, ARROW_LEFT, $ashape) if $as ne 'none';
$xs++;
}
$self->_printfb_line ($fb, $x, $self->{h} - 2, $line);
$self->_insert_label($fb, $xs, 0, $xs+$xr, 2, 'bottom' )
if ($self->{type} & EDGE_LABEL_CELL);
}
sub _draw_ver
{
# draw a VER edge piece
my ($self, $fb) = @_;
my $style = $self->_edge_style();
my $h = $self->{h};
# '|' => '|||||', '{}' => '{}{}{}'
my $line = $style->[1] x (1 + $h / length($style->[1]));
$line = substr($line, 0, $h) if length($line) > $h;
my $flags = $self->{type} & EDGE_FLAG_MASK;
# XXX TODO: handle here start points
# we get away with not handling them because in VER edges
# starting points are currently invisible.
my $as = $self->_arrow_style();
if ($as ne 'none')
{
my $ashape = $self->_arrow_shape();
substr($line,0,1) = $self->_arrow($as,ARROW_UP, $ashape)
if (($flags & EDGE_END_N) != 0);
substr($line,-1,1) = $self->_arrow($as,ARROW_DOWN, $ashape)
if (($flags & EDGE_END_S) != 0);
}
$self->_printfb_ver ($fb, 2, 0, $line);
$self->_insert_label($fb, 4, 1, 4, 2, 'middle')
if ($self->{type} & EDGE_LABEL_CELL);
}
sub _draw_cross
{
# draw a CROSS sections, or a joint (which is a 3/4 cross)
my ($self, $fb) = @_;
# vertical piece
my $style = $self->_edge_style( $self->{style_ver} );
my $invisible = 0;
my $line;
my $flags = $self->{type} & EDGE_FLAG_MASK;
my $type = $self->{type} & EDGE_TYPE_MASK;
my $as = $self->_arrow_style();
my $y = $self->{h} - 2;
print STDERR "# drawing cross at $self->{x},$self->{y} with flags $flags\n" if $self->{debug};
if ($self->{style_ver} ne 'invisible')
{
my $h = $self->{h};
# '|' => '|||||', '{}' => '{}{}{}'
$line = $style->[1] x (2 + $h / length($style->[1]));
$line = substr($line, 0, $h) if length($line) > $h;
if ($as ne 'none')
{
my $ashape = $self->_arrow_shape();
substr($line,0,1) = $self->_arrow($as,ARROW_UP, $ashape)
if (($flags & EDGE_END_N) != 0);
substr($line,-1,1) = $self->_arrow($as,ARROW_DOWN, $ashape)
if (($flags & EDGE_END_S) != 0);
}
# create joints
substr($line,0,$y) = ' ' x $y if $type == EDGE_S_E_W;
substr($line,$y,2) = ' ' if $type == EDGE_N_E_W;
$self->_printfb_ver ($fb, 2, 0, $line);
}
else { $invisible++; }
# horizontal piece
$style = $self->_edge_style();
my $ashape; $ashape = $self->_arrow_style() if $as ne 'none';
if ($self->{style} ne 'invisible')
{
my $w = $self->{w};
# '-' => '-----', '.-' => '.-.-.-'
my $len = length($style->[0]);
$line = $style->[0] x (2 + $w / $len);
# '.-.-.-' => '-.-.-' if $x % $ofs == 1 (e.g. on odd positions)
my $ofs = $self->{rx} % $len;
substr($line,0,$ofs) = '' if $ofs != 0;
$line = substr($line, 0, $w) if length($line) > $w;
my $x = 0;
if (($flags & EDGE_START_W) != 0)
{
$x++; chop($line); # ' ---'
}
if (($flags & EDGE_START_E) != 0)
{
chop($line); # '--- '
}
if (($flags & EDGE_END_E) != 0)
{
# '--> '
chop($line);
substr($line,-1,1) = $self->_arrow($as, ARROW_RIGHT, $ashape)
if $as ne 'none';
}
if (($flags & EDGE_END_W) != 0)
{
# ' <--'
substr($line,0,1) = ' ' if $as eq 'none';
substr($line,0,2) = ' ' . $self->_arrow($as, ARROW_LEFT, $ashape)
if $as ne 'none';
}
substr($line,0,2) = ' ' if $type == EDGE_E_N_S;
substr($line,2,$self->{w}-2) = ' ' x ($self->{w}-2) if $type == EDGE_W_N_S;
$self->_printfb_line ($fb, $x, $y, $line);
}
else { $invisible++; }
if (!$invisible)
{
# draw the crossing character only if both lines are visible
my $cross = $style->[2];
my $s = $self->{style} . $self->{style_ver};
$cross = ($self->_cross_style($s,$type) || $cross); # if $self->{style_ver} ne $self->{style};
$self->_printfb ($fb, 2, $y, $cross);
}
# done
}
sub _draw_corner
{
# draw a corner (N_E, S_E etc)
my ($self, $fb) = @_;
my $type = $self->{type} & EDGE_TYPE_MASK;
my $flags = $self->{type} & EDGE_FLAG_MASK;
############
# ........
# 0 : :
# 1 : : label would appear here
# 2 : +---: (w-3) = 3 chars wide
# 3 : | : always 1 char high
# .......:
# 012345
# draw the vertical piece
# get the style
my $style = $self->_edge_style();
my $h = 1; my $y = $self->{h} -1;
if ($type == EDGE_N_E || $type == EDGE_N_W)
{
$h = $self->{h} - 2; $y = 0;
}
# '|' => '|||||', '{}' => '{}{}{}'
my $line = $style->[1] x (1 + $h / length($style->[1]));
$line = substr($line, 0, $h) if length($line) > $h;
my $as = $self->_arrow_style();
my $ashape;
if ($as ne 'none')
{
$ashape = $self->_arrow_shape();
substr($line,0,1) = $self->_arrow($as, ARROW_UP, $ashape)
if (($flags & EDGE_END_N) != 0);
substr($line,-1,1) = $self->_arrow($as, ARROW_DOWN, $ashape)
if (($flags & EDGE_END_S) != 0);
}
$self->_printfb_ver ($fb, 2, $y, $line);
# horizontal piece
my $w = $self->{w} - 3; $y = $self->{h} - 2; my $x = 3;
if ($type == EDGE_N_W || $type == EDGE_S_W)
{
$w = 2; $x = 0;
}
# '-' => '-----', '.-' => '.-.-.-'
my $len = length($style->[0]);
$line = $style->[0] x (2 + $w / $len);
# '.-.-.-' => '-.-.-' if $x % $ofs == 1 (e.g. on odd positions)
my $ofs = ($x + $self->{rx}) % $len;
substr($line,0,$ofs) = '' if $ofs != 0;
$line = substr($line, 0, $w) if length($line) > $w;
substr($line,-1,1) = ' ' if ($flags & EDGE_START_E) != 0;
substr($line,0,1) = ' ' if ($flags & EDGE_START_W) != 0;
if (($flags & EDGE_END_E) != 0)
{
substr($line,-1,1) = ' ' if $as eq 'none';
substr($line,-2,2) = $self->_arrow($as, ARROW_RIGHT, $ashape) . ' ' if $as ne 'none';
}
if (($flags & EDGE_END_W) != 0)
{
substr($line,0,1) = ' ' if $as eq 'none';
substr($line,0,2) = ' ' . $self->_arrow($as, ARROW_LEFT, $ashape) if $as ne 'none';
}
$self->_printfb_line ($fb, $x, $y, $line);
my $idx = 3; # corner (SE, SW, NE, NW)
$idx = 4 if $type == EDGE_S_W;
$idx = 5 if $type == EDGE_N_E;
$idx = 6 if $type == EDGE_N_W;
# insert the corner character
$self->_printfb ($fb, 2, $y, $style->[$idx]);
}
sub _draw_loop_hor
{
my ($self, $fb) = @_;
my $type = $self->{type} & EDGE_TYPE_MASK;
my $flags = $self->{type} & EDGE_FLAG_MASK;
############
# ..........
# 0 : :
# 1 : : label would appear here
# 2 : +--+ : (w-6) = 2 chars wide
# 3 : | v : 1 char high
# .........:
# 01234567
############
# ..........
# 0 : | ^ : ver is h-2 chars high
# 1 : | | : label would appear here
# 2 : +--+ : (w-6) = 2 chars wide
# 3 : :
# .........:
# 01234567
# draw the vertical pieces
# get the style
my $style = $self->_edge_style();
my $h = 1; my $y = $self->{h} - 1;
if ($type == EDGE_S_W_N)
{
$h = $self->{h} - 2; $y = 0;
}
# '|' => '|||||', '{}' => '{}{}{}'
my $line = $style->[1] x (1 + $h / length($style->[1]));
$line = substr($line, 0, $h) if length($line) > $h;
my $as = $self->_arrow_style();
my $ashape; $ashape = $self->_arrow_shape() if $as ne 'none';
if ($self->{edge}->{bidirectional} && $as ne 'none')
{
substr($line,0,1) = $self->_arrow($as, ARROW_UP, $ashape) if (($flags & EDGE_END_N) != 0);
substr($line,-1,1) = $self->_arrow($as, ARROW_DOWN, $ashape) if (($flags & EDGE_END_S) != 0);
}
$self->_printfb_ver ($fb, $self->{w}-3, $y, $line);
if ($as ne 'none')
{
substr($line,0,1) = $self->_arrow($as, ARROW_UP, $ashape) if (($flags & EDGE_END_N) != 0);
substr($line,-1,1) = $self->_arrow($as, ARROW_DOWN, $ashape) if (($flags & EDGE_END_S) != 0);
}
$self->_printfb_ver ($fb, 2, $y, $line);
# horizontal piece
my $w = $self->{w} - 6; $y = $self->{h} - 2; my $x = 3;
# '-' => '-----', '.-' => '.-.-.-'
my $len = length($style->[0]);
$line = $style->[0] x (2 + $w / $len);
# '.-.-.-' => '-.-.-' if $x % $ofs == 1 (e.g. on odd positions)
my $ofs = ($x + $self->{rx}) % $len;
substr($line,0,$ofs) = '' if $ofs != 0;
$line = substr($line, 0, $w) if length($line) > $w;
$self->_printfb_line ($fb, $x, $y, $line);
my $corner_idx = 3; $corner_idx = 5 if $type == EDGE_S_W_N;
# insert the corner characters
$self->_printfb ($fb, 2, $y, $style->[$corner_idx]);
$self->_printfb ($fb, $self->{w}-3, $y, $style->[$corner_idx+1]);
my $align = 'bottom'; $align = 'top' if $type == EDGE_S_W_N;
$self->_insert_label($fb, 4, 0, 4, 2, $align)
if ($self->{type} & EDGE_LABEL_CELL);
# done
}
sub _draw_loop_ver
{
my ($self, $fb) = @_;
my $type = $self->{type} & EDGE_TYPE_MASK;
my $flags = $self->{type} & EDGE_FLAG_MASK;
############
# ........
# 0 : : label would appear here
# 1 : +-- :
# 2 : | :
# 3 : +-> :
# .......:
# 012345
# ........
# 0 : : label would appear here
# 1 : --+ :
# 2 : | :
# 3 : <-+ :
# .......:
# 012345
###########################################################################
# draw the vertical piece
# get the style
my $style = $self->_edge_style();
my $h = 1; my $y = $self->{h} - 3;
# '|' => '|||||', '{}' => '{}{}{}'
my $line = $style->[1] x (1 + $h / length($style->[1]));
$line = substr($line, 0, $h) if length($line) > $h;
my $x = 2; $x = $self->{w}-3 if ($type == EDGE_E_S_W);
$self->_printfb_ver ($fb, $x, $y, $line);
###########################################################################
# horizontal pieces
my $w = $self->{w} - 3; $y = $self->{h} - 4;
$x = 2; $x = 1 if ($type == EDGE_E_S_W);
# '-' => '-----', '.-' => '.-.-.-'
my $len = length($style->[0]);
$line = $style->[0] x (2 + $w / $len);
# '.-.-.-' => '-.-.-' if $x % $ofs == 1 (e.g. on odd positions)
my $ofs = ($x + $self->{rx}) % $len;
substr($line,0,$ofs) = '' if $ofs != 0;
$line = substr($line, 0, $w) if length($line) > $w;
my $as = $self->_arrow_style();
my $ashape; $ashape = $self->_arrow_shape() if $as ne 'none';
if ($self->{edge}->{bidirectional} && $as ne 'none')
{
substr($line,0,1) = $self->_arrow($as, ARROW_LEFT, $ashape) if (($flags & EDGE_END_W) != 0);
substr($line,-1,1) = $self->_arrow($as, ARROW_RIGHT, $ashape) if (($flags & EDGE_END_E) != 0);
}
$self->_printfb_line ($fb, $x, $y, $line);
if ($as ne 'none')
{
substr($line,0,1) = $self->_arrow($as, ARROW_LEFT, $ashape) if (($flags & EDGE_END_W) != 0);
substr($line,-1,1) = $self->_arrow($as, ARROW_RIGHT, $ashape) if (($flags & EDGE_END_E) != 0);
}
$self->_printfb_line ($fb, $x, $self->{h} - 2, $line);
$x = 2; $x = $self->{w}-3 if ($type == EDGE_E_S_W);
my $corner_idx = 3; $corner_idx = 4 if $type == EDGE_E_S_W;
# insert the corner characters
$self->_printfb ($fb, $x, $y, $style->[$corner_idx]);
$self->_printfb ($fb, $x, $self->{h}-2, $style->[$corner_idx+2]);
$x = 4; $x = 3 if ($type == EDGE_E_S_W);
$self->_insert_label($fb, $x, 0, $x, 4, 'bottom')
if ($self->{type} & EDGE_LABEL_CELL);
# done
}
# which method to call for which edge type
my $draw_dispatch =
{
EDGE_HOR() => '_draw_hor',
EDGE_VER() => '_draw_ver',
EDGE_S_E() => '_draw_corner',
EDGE_S_W() => '_draw_corner',
EDGE_N_E() => '_draw_corner',
EDGE_N_W() => '_draw_corner',
EDGE_CROSS() => '_draw_cross',
EDGE_W_N_S() => '_draw_cross',
EDGE_E_N_S() => '_draw_cross',
EDGE_N_E_W() => '_draw_cross',
EDGE_S_E_W() => '_draw_cross',
EDGE_N_W_S() => '_draw_loop_hor',
EDGE_S_W_N() => '_draw_loop_hor',
EDGE_E_S_W() => '_draw_loop_ver',
EDGE_W_S_E() => '_draw_loop_ver',
};
sub _draw_label
{
# This routine is cunningly named _draw_label, because it actually
# draws the edge line(s). The label text will be drawn by the individual
# routines called below.
my ($self, $fb, $x, $y) = @_;
my $type = $self->{type} & EDGE_TYPE_MASK;
# for cross sections, we maybe need to draw one of the parts:
return if $self->attribute('style') eq 'invisible' && $type ne EDGE_CROSS;
my $m = $draw_dispatch->{$type};
$self->_croak("Unknown edge type $type") unless defined $m;
# store the coordinates of our upper-left corner (for seamless rendering)
$self->{rx} = $x || 0; $self->{ry} = $y || 0;
$self->$m($fb);
delete $self->{rx}; delete $self->{ry}; # no longer needed
}
#############################################################################
#############################################################################
package Graph::Easy::Node;
use strict;
sub _framebuffer
{
# generate an actual framebuffer consisting of spaces
my ($self, $w, $h) = @_;
print STDERR "# trying to generate framebuffer of undefined width for $self->{name}\n",
join (": ", caller(),"\n") if !defined $w;
my @fb;
my $line = ' ' x $w;
for my $y (1..$h)
{
push @fb, $line;
}
\@fb;
}
sub _printfb_aligned
{
my ($self,$fb, $x1,$y1, $w,$h, $lines, $aligns, $align_ver) = @_;
$align_ver = 'middle' unless $align_ver;
# $align_ver eq 'middle':
my $y = $y1 + ($h / 2) - (scalar @$lines / 2);
if ($align_ver eq 'top')
{
$y = $y1;
$y1 = 0;
}
if ($align_ver eq 'bottom')
{
$y = $h - scalar @$lines; $y1 = 0;
}
my $xc = ($w / 2);
my $i = 0;
while ($i < @$lines)
{
# get the line and her alignment
my ($l,$al) = ($lines->[$i],$aligns->[$i]);
my $x = 0; # left is default
$x = $xc - length($l) / 2 if $al eq 'c';
$x = $w - length($l) if $al eq 'r';
# now print the line (inlined print_fb_line for speed)
substr ($fb->[int($y+$i+$y1)], int($x+$x1), length($l)) = $l;
$i++;
}
}
sub _printfb_line
{
# Print one textline into a framebuffer
# Caller MUST ensure proper size of FB, for speed reasons,
# we do not check whether text fits!
my ($self, $fb, $x, $y, $l) = @_;
# [0] = '0123456789...'
substr ($fb->[$y], $x, length($l)) = $l;
}
sub _printfb
{
# Print (potential a multiline) text into a framebuffer
# Caller MUST ensure proper size of FB, for speed reasons,
# we do not check whether the text fits!
my ($self, $fb, $x, $y, @lines) = @_;
# [0] = '0123456789...'
# [1] = '0123456789...' etc
for my $l (@lines)
{
# # XXX DEBUG:
# if ( $x + length($l) > length($fb->[$y]))
# {
# require Carp;
# Carp::confess("substr outside framebuffer");
# }
substr ($fb->[$y], $x, length($l)) = $l; $y++;
}
}
sub _printfb_ver
{
# Print a string vertical into a framebuffer.
# Caller MUST ensure proper size of FB, for speed reasons,
# we do not check whether text fits!
my ($self, $fb, $x, $y, $line) = @_;
# this more than twice as fast as:
# "@pieces = split//,$line; _printfb(...)"
my $y1 = $y + length($line);
substr ($fb->[$y1], $x, 1) = chop($line) while ($y1-- > $y);
}
# for ASCII and box drawing:
# the array contains for each style:
# upper left edge
# upper right edge
# lower right edge
# lower left edge
# hor style (top edge)
# hor style (bottom side)
# ver style (right side) (multiple characters possible)
# ver style (left side) (multiple characters possible)
# T crossing (see drawing below)
# T to right
# T to left
# T to top
# T shape (to bottom)
#
# +-----4-----4------+
# | | | |
# | | | |
# | | | |
# 1-----0-----3------2 1 = T to right, 2 = T to left, 3 T to top
# | | 0 = cross, 4 = T shape
# | |
# | |
# +-----+
my $border_styles =
[
{
solid => [ '+', '+', '+', '+', '-', '-', [ '|' ], [ '|' ], '+', '+', '+', '+', '+' ],
dotted => [ '.', '.', ':', ':', '.', '.', [ ':' ], [ ':' ], '.', '.', '.', '.', '.' ],
dashed => [ '+', '+', '+', '+', '- ', '- ', [ "'" ], [ "'" ], '+', '+', '+', '+', '+' ],
'dot-dash' => [ '+', '+', '+', '+', '.-', '.-', [ '!' ], [ '!' ], '+', '+', '+', '+', '+' ],
'dot-dot-dash' => [ '+', '+', '+', '+', '..-', '..-', [ '|', ':' ], [ '|',':' ], '+', '+', '+', '+', '+' ],
bold => [ '#', '#', '#', '#', '#', '#', [ '#' ], [ '#' ], '#', '#', '#', '#', '#' ],
'bold-dash' => [ '#', '#', '#', '#', '# ', '# ', ['#',' ' ], [ '#',' ' ], '#', '#', '#', '#', '#' ],
double => [ '#', '#', '#', '#', '=', '=', [ 'H' ], [ 'H' ], '#', '#', '#', '#', '#' ],
'double-dash' => [ '#', '#', '#', '#', '= ', '= ', [ '"' ], [ '"' ], '#', '#', '#', '#', '#' ],
wave => [ '+', '+', '+', '+', '~', '~', [ '{', '}' ], [ '{','}' ], '+', '+', '+', '+', '+' ],
broad => [ '#', '#', '#', '#', '#', '#', [ '#' ], [ '#' ], '#', '#', '#', '#', '#' ],
wide => [ '#', '#', '#', '#', '#', '#', [ '#' ], [ '#' ], '#', '#', '#', '#', '#' ],
none => [ ' ', ' ', ' ', ' ', ' ', ' ', [ ' ' ], [ ' ' ], ' ', ' ', ' ', ' ', ' ' ],
},
{
solid => [ '┌', '┐', '┘', '└', '─', '─', [ '│' ], [ '│' ], '┼', '├', '┤', '┴', '┬' ],
double => [ '╔', '╗', '╝', '╚', '═', '═', [ '║' ], [ '║' ], '┼', '├', '┤', '┴', '┬' ],
dotted => [ '┌', '┐', '┘', '└', '⋯', '⋯', [ '⋮' ], [ '⋮' ], '┼', '├', '┤', '┴', '┬' ],
dashed => [ '┌', '┐', '┘', '└', '', '', [ '╎' ], [ '╎' ], '┼', '├', '┤', '┴', '┬' ],
'dot-dash' => [ '┌', '┐', '┘', '└', '·'.'-', '·'.'-', ['!'], ['!'], '┼', '├', '┤', '┴', '┬' ],
'dot-dot-dash' => [ '┌', '┐', '┘', '└', ('·' x 2) .'-', ('·' x 2) .'-', [ '│', ':' ], [ '│', ':' ], '┼', '├', '┤', '┴', '┬' ],
bold => [ '┏', '┓', '┛', '┗', '━', '━', [ '┃' ], [ '┃' ], '┼', '├', '┤', '┴', '┬' ],
'bold-dash' => [ '┏', '┓', '┛', '┗', '━'.' ', '━'.' ', [ '╻' ], [ '╻' ], '┼', '├', '┤', '┴', '┬' ],
'double-dash' => [ '╔', '╗', '╝', '╚', '═'.' ', '═'.' ', [ '∥' ], [ '∥' ], '┼', '├', '┤', '┴', '┬' ],
wave => [ '┌', '┐', '┘', '└', '', '', [ '≀' ], [ '≀' ], '┼', '├', '┤', '┴', '┬' ],
broad => [ '▛', '▜', '▟', '▙', '▀', '▄', [ '▌' ], [ '▐' ], '▄', '├', '┤', '┴', '┬' ],
wide => [ '█', '█', '█', '█', '█', '█', [ '█' ], [ '█' ], '█', '█', '█', '█', '█' ],
none => [ ' ', ' ', ' ', ' ', ' ', ' ', [ ' ' ], [ ' ' ], ' ', ' ', ' ', ' ', ' ', ],
},
];
# for boxart and rounded corners on node-borders:
# upper left edge
# upper right edge
# lower right edge
# lower left edge
my $rounded_edges = [ '╭', '╮', '╯', '╰', ];
# for ASCII/boxart drawing slopes/slants
# lower-left to upper right (repeated twice)
# lower-right to upper left (repeated twice)
my $slants = [
# ascii
{
solid => [ '/' , '\\' ],
dotted => [ '.' , '.' ],
dashed => [ '/ ', '\\ ' ],
'dot-dash' => [ './', '.\\' ],
'dot-dot-dash' => [ '../', '..\\' ],
bold => [ '#' , '#' ],
'bold-dash' => [ '# ' , '# ' ],
'double' => [ '/' , '\\' ],
'double-dash' => [ '/ ' , '\\ ' ],
wave => [ '/ ' , '\\ ' ],
broad => [ '#' , '#' ],
wide => [ '#' , '#' ],
},
# boxart
{
solid => [ '' , '╲' ],
dotted => [ '⋰' , '⋱' ],
dashed => [ ' ', '╲ ' ],
'dot-dash' => [ '.', '.╲' ],
'dot-dot-dash' => [ '⋰╱', '⋱╲' ],
bold => [ '#' , '#' ],
'bold-dash' => [ '# ' , '# ' ],
'double' => [ '' , '╲' ],
'double-dash' => [ ' ' , '╲ ' ],
wave => [ ' ' , '╲ ' ],
broad => [ '#' , '#' ],
wide => [ '#' , '#' ],
},
];
# ASCII and box art: the different point shapes and styles
my $point_shapes =
[ {
filled =>
{
'star' => '*',
'square' => '#',
'dot' => '.',
'circle' => 'o', # unfortunately, there is no filled o in ASCII
'cross' => '+',
'diamond' => '<>',
'x' => 'X',
},
closed =>
{
'star' => '*',
'square' => '#',
'dot' => '.',
'circle' => 'o',
'cross' => '+',
'diamond' => '<>',
'x' => 'X',
},
},
{
filled =>
{
'star' => '★',
'square' => '■',
'dot' => '·',
'circle' => '●',
'cross' => '+',
'diamond' => '◆',
'x' => '',
},
closed =>
{
'star' => '☆',
'square' => '□',
'dot' => '·',
'circle' => '○',
'cross' => '+',
'diamond' => '◇',
'x' => '',
},
}
];
sub _point_style
{
my ($self, $shape, $style) = @_;
return '' if $shape eq 'invisible';
if ($style =~ /^(star|square|dot|circle|cross|diamond)\z/)
{
# support the old "pointstyle: diamond" notion:
$shape = $style; $style = 'filled';
}
$style = 'filled' unless defined $style;
my $g = $self->{graph}->{_ascii_style} || 0;
$point_shapes->[$g]->{$style}->{$shape};
}
sub _border_style
{
my ($self, $style, $type) = @_;
# make a copy so that we can modify it
my $g = $self->{graph}->{_ascii_style} || 0;
my $s = [ @{ $border_styles->[ $g ]->{$style} } ];
die ("Unknown $type border style '$style'") if @$s == 0;
my $shape = 'rect';
$shape = $self->attribute('shape') unless $self->isa_cell();
return $s unless $shape eq 'rounded';
# if shape: rounded, overlay the rounded edge pieces
splice (@$s, 0, 4, @$rounded_edges)
if $style =~ /^(solid|dotted|dashed|dot-dash|dot-dot-dash)\z/;
# '####' => ' ### '
splice (@$s, 0, 4, (' ', ' ', ' ', ' '))
if $g == 0 || $style =~ /^(bold|wide|broad|double|double-dash|bold-dash)\z/;
$s;
}
#############################################################################
# different arrow styles and shapes in ASCII and boxart
my $arrow_form =
{
normal => 0,
sleek => 1, # slightly squashed
};
my $arrow_shapes =
{
triangle => 0,
diamond => 1,
box => 2,
dot => 3,
inv => 4, # an inverted triangle
line => 5,
cross => 6,
x => 7,
};
# todo: ≪ ≫
my $arrow_styles =
[
[
# triangle
{
open => [ '>', '<', '^', 'v' ],
closed => [ '>', '<', '^', 'v' ],
filled => [ '>', '<', '^', 'v' ],
},
{
open => [ '>', '<', '∧', '' ],
closed => [ '▷', '◁', '△', '▽' ],
filled => [ '▶', '◀', '▲', '▼' ],
}
], [
# diamond
{
open => [ '>', '<', '^', 'v' ],
closed => [ '>', '<', '^', 'v' ],
filled => [ '>', '<', '^', 'v' ],
},
{
open => [ '>', '<', '∧', '' ],
closed => [ '◇', '◇', '◇', '◇' ],
filled => [ '◆', '◆', '◆', '◆' ],
}
], [
# box
{
open => [ ']', '[', '°', 'u' ],
closed => [ 'D', 'D', 'D', 'D' ],
filled => [ '#', '#', '#', '#' ],
},
{
open => [ '⊐', '⊐', '⊓', '⊔' ],
closed => [ '◻', '◻', '◻', '◻' ],
filled => [ '◼', '◼', '◼', '◼' ],
}
], [
# dot
{
open => [ ')', '(', '^', 'u' ],
closed => [ 'o', 'o', 'o', 'o' ],
filled => [ '*', '*', '*', '*' ],
},
{
open => [ ')', '(', '◠', '◡' ],
closed => [ '○', '○', '○', '○' ],
filled => [ '●', '●', '●', '●' ],
}
], [
# inv
{
open => [ '<', '>', 'v', '^' ],
closed => [ '<', '>', 'v', '^' ],
filled => [ '<', '>', 'v', '^' ],
},
{
open => [ '<', '>', '', '∧' ],
closed => [ '◁', '▷', '▽', '△' ],
filled => [ '◀', '▶', '▼', '▲' ],
}
], [
# line
{
open => [ '|', '|', '_', '-' ],
closed => [ '|', '|', '_', '-' ],
filled => [ '|', '|', '_', '-' ],
},
{
open => [ '⎥', '⎢', '_', '¯' ],
closed => [ '⎥', '⎢', '_', '¯' ],
filled => [ '⎥', '⎢', '_', '¯' ],
}
], [
# cross
{
open => [ '+', '+', '+', '+' ],
closed => [ '+', '+', '+', '+' ],
filled => [ '+', '+', '+', '+' ],
},
{
open => [ '┼', '┼', '┼', '┼' ],
closed => [ '┼', '┼', '┼', '┼' ],
filled => [ '┼', '┼', '┼', '┼' ],
}
], [
# x
{
open => [ 'x', 'x', 'x', 'x' ],
closed => [ 'x', 'x', 'x', 'x' ],
filled => [ 'x', 'x', 'x', 'x' ],
},
{
open => [ 'x', 'x', 'x', 'x' ],
closed => [ 'x', 'x', 'x', 'x' ],
filled => [ '⧓', '⧓', 'x', 'x' ],
}
]
];
sub _arrow
{
# return an arror, depending on style and direction
my ($self, $style, $dir, $shape) = @_;
$shape = '' unless defined $shape;
$shape = $arrow_shapes->{$shape} || 0;
my $g = $self->{graph}->{_ascii_style} || 0;
$arrow_styles->[$shape]->[$g]->{$style}->[$dir];
}
# To convert an HTML arrow to Unicode:
my $arrow_dir = {
'&gt;' => 0,
'&lt;' => 1,
'^' => 2,
'v' => 3,
};
sub _unicode_arrow
{
# return an arror in unicode, depending on style and direction
my ($self, $shape, $style, $arrow_text) = @_;
$shape = '' unless defined $shape;
$shape = $arrow_shapes->{$shape} || 0;
my $dir = $arrow_dir->{$arrow_text} || 0;
$arrow_styles->[$shape]->[1]->{$style}->[$dir];
}
#############################################################################
#
# +---4---4---4---+
# | | | | |
# | | | | |
# | | | | |
# 1---0---3---0---2 1 = T to right, 2 = T to left, 3 T to top
# | | | | 0 = cross, 4 = T shape
# | | | |
# | | | |
# +---+ +---+
sub _draw_border
{
# draws a border into the framebuffer
my ($self, $fb, $do_right, $do_bottom, $do_left, $do_top, $x, $y) = @_;
return if $do_right.$do_left.$do_bottom.$do_top eq 'nonenonenonenone';
my $g = $self->{graph};
my $w = $self->{w};
if ($do_top ne 'none')
{
my $style = $self->_border_style($do_top, 'top');
# top-left corner piece is only there if we have a left border
my $tl = $style->[0]; $tl = '' if $do_left eq 'none';
# generate the top border
my $top = $style->[4] x (($self->{w}) / length($style->[4]) + 1);
my $len = length($style->[4]);
# for seamless rendering
if (defined $x)
{
my $ofs = $x % $len;
substr($top,0,$ofs) = '' if $ofs != 0;
}
# insert left upper corner (if it is there)
substr($top,0,1) = $tl if $tl ne '';
$top = substr($top,0,$w) if length($top) > $w;
# top-right corner piece is only there if we have a right border
substr($top,-1,1) = $style->[1] if $do_right ne 'none';
# if the border must be collapsed, modify top-right edge piece:
if ($self->{border_collapse_right})
{
# place "4" (see drawing above)
substr($top,-1,1) = $style->[10];
}
# insert top row into FB
$self->_printfb( $fb, 0,0, $top);
}
if ($do_bottom ne 'none')
{
my $style = $self->_border_style($do_bottom, 'bottom');
# bottom-left corner piece is only there if we have a left border
my $bl = $style->[3]; $bl = '' if $do_left eq 'none';
# the bottom row '+--------+' etc
my $bottom = $style->[5] x (($self->{w}) / length($style->[5]) + 1);
my $len = length($style->[5]);
# for seamless rendering
if (defined $x)
{
my $ofs = $x % $len;
substr($bottom,0,$ofs) = '' if $ofs != 0;
}
# insert left bottom corner (if it is there)
substr($bottom,0,1) = $bl if $bl ne '';
$bottom = substr($bottom,0,$w) if length($bottom) > $w;
# bottom-right corner piece is only there if we have a right border
substr($bottom,-1,1) = $style->[2] if $do_right ne 'none';
# if the border must be collapsed, modify bottom-right edge piece:
if ($self->{border_collapse_right} || $self->{border_collapse_bottom})
{
if ($self->{rightbelow_count} > 0)
{
# place a cross or T piece (see drawing above)
my $piece = 8; # cross
# inverted T
$piece = 11 if $self->{rightbelow_count} < 2 && !$self->{have_below};
$piece = 10 if $self->{rightbelow_count} < 2 && !$self->{have_right};
substr($bottom,-1,1) = $style->[$piece];
}
}
# insert bottom row into FB
$self->_printfb( $fb, 0,$self->{h}-1, $bottom);
}
return if $do_right.$do_left eq 'nonenone'; # both none => done
my $style = $self->_border_style($do_left, 'left');
my $left = $style->[6];
my $lc = scalar @{ $style->[6] } - 1; # count of characters
$style = $self->_border_style($do_right, 'right');
my $right = $style->[7];
my $rc = scalar @{ $style->[7] } - 1; # count of characters
my (@left, @right);
my $l = 0; my $r = 0; # start with first character
my $s = 1; $s = 0 if $do_top eq 'none';
my $h = $self->{h} - 2;
$h ++ if defined $x && $do_bottom eq 'none'; # for seamless rendering
for ($s..$h)
{
push @left, $left->[$l]; $l ++; $l = 0 if $l > $lc;
push @right, $right->[$r]; $r ++; $r = 0 if $r > $rc;
}
# insert left/right columns into FB
$self->_printfb( $fb, 0, $s, @left) unless $do_left eq 'none';
$self->_printfb( $fb, $w-1, $s, @right) unless $do_right eq 'none';
$self;
}
sub _draw_label
{
# Draw the node label into the framebuffer
my ($self, $fb, $x, $y, $shape) = @_;
if ($shape eq 'point')
{
# point-shaped nodes do not show their label in ASCII
my $style = $self->attribute('pointstyle');
my $shape = $self->attribute('pointshape');
my $l = $self->_point_style($shape,$style);
$self->_printfb_line ($fb, 2, $self->{h} - 2, $l) if $l;
return;
}
# +----
# | Label
# 2,1: ----^
my $w = $self->{w} - 4; my $xs = 2;
my $h = $self->{h} - 2; my $ys = 0.5;
my $border = $self->attribute('borderstyle');
if ($border eq 'none')
{
$w += 2; $h += 2;
$xs = 1; $ys = 0;
}
my $align = $self->attribute('align');
$self->_printfb_aligned ($fb, $xs, $ys, $w, $h, $self->_aligned_label($align));
}
sub as_ascii
{
# renders a node or edge like:
# +--------+ .......... ""
# | A node | or : A node : or " --> "
# +--------+ .......... ""
my ($self, $x,$y) = @_;
my $shape = 'rect';
$shape = $self->attribute('shape') unless $self->isa_cell();
if ($shape eq 'edge')
{
my $edge = Graph::Easy::Edge->new();
my $cell = Graph::Easy::Edge::Cell->new( edge => $edge, x => $x, y => $y );
$cell->{w} = $self->{w};
$cell->{h} = $self->{h};
$cell->{att}->{label} = $self->label();
$cell->{type} =
Graph::Easy::Edge::Cell->EDGE_HOR +
Graph::Easy::Edge::Cell->EDGE_LABEL_CELL;
return $cell->as_ascii();
}
# invisible nodes, or very small ones
return '' if $shape eq 'invisible' || $self->{w} == 0 || $self->{h} == 0;
my $fb = $self->_framebuffer($self->{w}, $self->{h});
# point-shaped nodes do not have a border
if ($shape ne 'point')
{
#########################################################################
# draw our border into the framebuffer
my $cache = $self->{cache};
my $b_top = $cache->{top_border} || 'none';
my $b_left = $cache->{left_border} || 'none';
my $b_right = $cache->{right_border} || 'none';
my $b_bottom = $cache->{bottom_border} || 'none';
$self->_draw_border($fb, $b_right, $b_bottom, $b_left, $b_top);
}
###########################################################################
# "draw" the label into the framebuffer (e.g. the node/edge and the text)
$self->_draw_label($fb, $x, $y, $shape);
join ("\n", @$fb);
}
1;
__END__
=head1 NAME
Graph::Easy::As_ascii - Generate ASCII art
=head1 SYNOPSIS
use Graph::Easy;
my $graph = Graph::Easy->new();
$graph->add_edge('Bonn', 'Berlin');
print $graph->as_ascii();
=head1 DESCRIPTION
C<Graph::Easy::As_ascii> contains the code to render Nodes/Edges as
ASCII art. It is used by Graph::Easy automatically, and there should
be no need to use it directly.
=head1 EXPORT
Exports nothing.
=head1 SEE ALSO
L<Graph::Easy>.
=head1 AUTHOR
Copyright (C) 2004 - 2007 by Tels L<http://bloodgate.com>.
See the LICENSE file for more details.
=cut