| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package WWW::Analytics::MultiTouch::Tabular; | 
| 2 |  |  |  |  |  |  |  | 
| 3 | 3 |  |  | 3 |  | 112392 | use warnings; | 
|  | 3 |  |  |  |  | 7 |  | 
|  | 3 |  |  |  |  | 115 |  | 
| 4 | 3 |  |  | 3 |  | 17 | use strict; | 
|  | 3 |  |  |  |  | 7 |  | 
|  | 3 |  |  |  |  | 94 |  | 
| 5 |  |  |  |  |  |  |  | 
| 6 | 3 |  |  | 3 |  | 14 | use IO::File; | 
|  | 3 |  |  |  |  | 6 |  | 
|  | 3 |  |  |  |  | 519 |  | 
| 7 | 3 |  |  | 3 |  | 17 | use strict; | 
|  | 3 |  |  |  |  | 5 |  | 
|  | 3 |  |  |  |  | 90 |  | 
| 8 | 3 |  |  | 3 |  | 18 | use warnings; | 
|  | 3 |  |  |  |  | 4 |  | 
|  | 3 |  |  |  |  | 86 |  | 
| 9 | 3 |  |  | 3 |  | 3103 | use Text::Table; | 
|  | 3 |  |  |  |  | 99749 |  | 
|  | 3 |  |  |  |  | 117 |  | 
| 10 | 3 |  |  | 3 |  | 5430 | use Text::CSV_XS; | 
|  | 3 |  |  |  |  | 57331 |  | 
|  | 3 |  |  |  |  | 249 |  | 
| 11 | 3 |  |  | 3 |  | 10856 | use Spreadsheet::WriteExcel; | 
|  | 3 |  |  |  |  | 315256 |  | 
|  | 3 |  |  |  |  | 170 |  | 
| 12 | 3 |  |  | 3 |  | 3627 | use Spreadsheet::WriteExcel::Utility; | 
|  | 3 |  |  |  |  | 13033 |  | 
|  | 3 |  |  |  |  | 597 |  | 
| 13 | 3 |  |  | 3 |  | 41 | use Digest::MD5 qw/md5_hex/; | 
|  | 3 |  |  |  |  | 6 |  | 
|  | 3 |  |  |  |  | 527 |  | 
| 14 | 3 |  |  | 3 |  | 1167 | use Params::Validate qw(:all); | 
|  | 3 |  |  |  |  | 25429 |  | 
|  | 3 |  |  |  |  | 812 |  | 
| 15 | 3 |  |  | 3 |  | 27 | use List::Util qw/max/; | 
|  | 3 |  |  |  |  | 6 |  | 
|  | 3 |  |  |  |  | 369 |  | 
| 16 | 3 |  |  | 3 |  | 1529 | use Storable qw/dclone/; | 
|  | 3 |  |  |  |  | 4990 |  | 
|  | 3 |  |  |  |  | 280 |  | 
| 17 | 3 |  |  | 3 |  | 1113 | use Hash::Merge qw/merge/; | 
|  | 3 |  |  |  |  | 9308 |  | 
|  | 3 |  |  |  |  | 193 |  | 
| 18 | 3 |  |  | 3 |  | 38 | use POSIX qw/floor/; | 
|  | 3 |  |  |  |  | 6 |  | 
|  | 3 |  |  |  |  | 38 |  | 
| 19 | 3 |  |  | 3 |  | 5376 | use LWP::UserAgent; | 
|  | 3 |  |  |  |  | 52788 |  | 
|  | 3 |  |  |  |  | 123 |  | 
| 20 | 3 |  |  | 3 |  | 1774 | use File::Temp; | 
|  | 3 |  |  |  |  | 15691 |  | 
|  | 3 |  |  |  |  | 391 |  | 
| 21 | 3 |  |  | 3 |  | 2241 | use Encode; | 
|  | 3 |  |  |  |  | 25632 |  | 
|  | 3 |  |  |  |  | 679 |  | 
| 22 |  |  |  |  |  |  |  | 
| 23 |  |  |  |  |  |  | my %legal_name = map { $_ => 1 } qw/font size color bold italic underline font_strikeout font_script font_outline font_shadow num_format locked hidden align valign rotation text_wrap test_justlast center_across indent shrink pattern bg_color fg_color border bottom top left right border_color bottom_color top_color left_color right_color/; | 
| 24 |  |  |  |  |  |  |  | 
| 25 |  |  |  |  |  |  | my %chart_methods = ( title => [ qw/name name_formula/ ], | 
| 26 |  |  |  |  |  |  | x_axis => [ qw/name name_formula/ ], | 
| 27 |  |  |  |  |  |  | y_axis => [ qw/name name_formula/ ], | 
| 28 |  |  |  |  |  |  | legend => [ qw/position/ ], | 
| 29 |  |  |  |  |  |  | chartarea => [ qw/color line_color line_pattern line_weight/ ], | 
| 30 |  |  |  |  |  |  | plotarea => [ qw/visible color line_color line_pattern line_weight/ ], | 
| 31 |  |  |  |  |  |  | ); | 
| 32 |  |  |  |  |  |  |  | 
| 33 |  |  |  |  |  |  |  | 
| 34 |  |  |  |  |  |  | sub new | 
| 35 |  |  |  |  |  |  | { | 
| 36 | 1 |  |  | 1 | 1 | 60 | my $class = shift; | 
| 37 |  |  |  |  |  |  |  | 
| 38 | 1 |  |  |  |  | 41 | my %params = validate(@_, { format => 0, | 
| 39 |  |  |  |  |  |  | filename => 0, | 
| 40 |  |  |  |  |  |  | header_layout => 0, | 
| 41 |  |  |  |  |  |  | footer_layout => 0, | 
| 42 |  |  |  |  |  |  | }); | 
| 43 |  |  |  |  |  |  |  | 
| 44 | 1 |  |  |  |  | 8 | for my $method (qw/format filename filehandle/) { | 
| 45 | 3 |  |  | 3 |  | 61 | no strict 'refs'; | 
|  | 3 |  |  |  |  | 6 |  | 
|  | 3 |  |  |  |  | 13681 |  | 
| 46 | 3 |  |  |  |  | 20 | *{"${class}::$method"} = sub { | 
| 47 | 22 |  |  | 22 |  | 683 | my $self = shift; | 
| 48 | 22 |  |  |  |  | 46 | my $ret = $self->{$method}; | 
| 49 | 22 | 100 |  |  |  | 50 | $self->{$method} = $_[0] if defined $_[0]; | 
| 50 | 22 |  |  |  |  | 86 | return $ret; | 
| 51 |  |  |  |  |  |  | } | 
| 52 | 3 |  |  |  |  | 20 | } | 
| 53 |  |  |  |  |  |  |  | 
| 54 | 1 |  | 33 |  |  | 9 | my $self = bless \%params, ref $class || $class; | 
| 55 |  |  |  |  |  |  |  | 
| 56 | 1 |  |  |  |  | 5 | $self->open; | 
| 57 |  |  |  |  |  |  |  | 
| 58 | 1 |  |  |  |  | 3 | return $self; | 
| 59 |  |  |  |  |  |  | } | 
| 60 |  |  |  |  |  |  |  | 
| 61 |  |  |  |  |  |  |  | 
| 62 |  |  |  |  |  |  | sub print | 
| 63 |  |  |  |  |  |  | { | 
| 64 | 2 |  |  | 2 | 1 | 8608 | my ($self, $data) = @_; | 
| 65 |  |  |  |  |  |  |  | 
| 66 | 2 |  |  |  |  | 10 | local $_ = $self->format; | 
| 67 |  |  |  |  |  |  | SWITCH: { | 
| 68 | 2 | 100 |  |  |  | 6 | m/csv/ && do { $self->csv($data), last SWITCH }; | 
|  | 2 |  |  |  |  | 16 |  | 
|  | 1 |  |  |  |  | 6 |  | 
| 69 | 1 | 50 |  |  |  | 5 | m/xls/ && do { $self->xls($data), last SWITCH }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 70 | 1 |  |  |  |  | 5 | $self->txt($data); | 
| 71 |  |  |  |  |  |  | }; | 
| 72 |  |  |  |  |  |  | } | 
| 73 |  |  |  |  |  |  |  | 
| 74 |  |  |  |  |  |  | sub txt | 
| 75 |  |  |  |  |  |  | { | 
| 76 | 1 |  |  | 1 | 1 | 2 | my ($self, $datasets) = @_; | 
| 77 | 1 |  |  |  |  | 5 | my $handle = $self->filehandle; | 
| 78 | 1 |  |  |  |  | 13 | binmode $handle, ':utf8'; | 
| 79 | 1 |  |  |  |  | 5 | foreach my $data (@$datasets) { | 
| 80 | 2 |  |  |  |  | 6 | my $tab = Text::Table->new(map { _text_of($_) } @{$data->{'headings'}}); | 
|  | 6 |  |  |  |  | 13 |  | 
|  | 2 |  |  |  |  | 7 |  | 
| 81 |  |  |  |  |  |  |  | 
| 82 | 2 |  |  |  |  | 2467 | $tab->load(map { my $row = $_; [ map { _text_of($_) } @$row ] } @{$data->{'data'}}); | 
|  | 6 |  |  |  |  | 9 |  | 
|  | 6 |  |  |  |  | 13 |  | 
|  | 18 |  |  |  |  | 31 |  | 
|  | 2 |  |  |  |  | 8 |  | 
| 83 | 2 | 50 |  |  |  | 458 | $handle->print($data->{'sheetname'} . "\n") if $data->{'sheetname'}; | 
| 84 | 2 |  |  |  |  | 23 | $handle->print(_text_of($data->{'title'}) . "\n" . $tab->table); | 
| 85 | 2 |  |  |  |  | 14415 | $handle->print("\n\n"); | 
| 86 | 2 | 50 | 33 |  |  | 54 | if ($data->{'notes'} && @{$data->{'notes'}}) { | 
|  | 0 |  |  |  |  | 0 |  | 
| 87 | 0 |  |  |  |  | 0 | $handle->print(_text_of($_) . "\n") for @{$data->{'notes'}}; | 
|  | 0 |  |  |  |  | 0 |  | 
| 88 | 0 |  |  |  |  | 0 | $handle->print("\n\n"); | 
| 89 |  |  |  |  |  |  | } | 
| 90 |  |  |  |  |  |  | } | 
| 91 |  |  |  |  |  |  | } | 
| 92 |  |  |  |  |  |  |  | 
| 93 |  |  |  |  |  |  |  | 
| 94 |  |  |  |  |  |  | sub csv | 
| 95 |  |  |  |  |  |  | { | 
| 96 | 1 |  |  | 1 | 1 | 3 | my ($self, $datasets) = @_; | 
| 97 |  |  |  |  |  |  |  | 
| 98 | 1 |  |  |  |  | 4 | my $handle = $self->filehandle; | 
| 99 | 1 |  |  |  |  | 8 | binmode $handle, ':utf8'; | 
| 100 |  |  |  |  |  |  |  | 
| 101 | 1 |  |  |  |  | 11 | my $csv = Text::CSV_XS->new( { 'binary'=>1 } ); | 
| 102 | 1 |  |  |  |  | 134 | foreach my $data (@$datasets) { | 
| 103 | 2 | 50 |  |  |  | 8 | if ($data->{'sheetname'}) { | 
| 104 | 2 |  |  | 1 |  | 92 | $csv->print($handle, [ $data->{'sheetname'} ]); | 
|  | 1 |  |  |  |  | 12 |  | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 7 |  | 
| 105 | 2 |  |  |  |  | 22 | $handle->print("\n"); | 
| 106 |  |  |  |  |  |  | } | 
| 107 | 2 |  |  |  |  | 15 | $csv->print($handle, [ _text_of($data->{'title'}) ]); | 
| 108 | 2 |  |  |  |  | 16 | $handle->print("\n"); | 
| 109 | 2 |  |  |  |  | 9 | $csv->print($handle, [ map { _text_of($_) } @{$data->{'headings'}}] ); | 
|  | 6 |  |  |  |  | 10 |  | 
|  | 2 |  |  |  |  | 6 |  | 
| 110 | 2 |  |  |  |  | 17 | $handle->print("\n"); | 
| 111 | 6 |  |  |  |  | 26 | map { my $row = $_; | 
|  | 2 |  |  |  |  | 5 |  | 
| 112 | 6 |  |  |  |  | 12 | $csv->print($handle, [ map { _text_of($_) } @$row ]); | 
|  | 18 |  |  |  |  | 29 |  | 
| 113 | 6 |  |  |  |  | 52 | $handle->print("\n"); | 
| 114 | 2 |  |  |  |  | 11 | } @{$data->{'data'}}; | 
| 115 | 2 |  |  |  |  | 15 | $handle->print("\n\n\n"); | 
| 116 | 2 | 50 | 33 |  |  | 35 | if ($data->{'notes'} && @{$data->{'notes'}}) { | 
|  | 0 |  |  |  |  | 0 |  | 
| 117 | 0 |  |  |  |  | 0 | $handle->print(_text_of($_) . "\n") for @{$data->{'notes'}}; | 
|  | 0 |  |  |  |  | 0 |  | 
| 118 | 0 |  |  |  |  | 0 | $handle->print("\n\n\n"); | 
| 119 |  |  |  |  |  |  | } | 
| 120 |  |  |  |  |  |  | } | 
| 121 |  |  |  |  |  |  | } | 
| 122 |  |  |  |  |  |  |  | 
| 123 |  |  |  |  |  |  | sub xls | 
| 124 |  |  |  |  |  |  | { | 
| 125 | 0 |  |  | 0 | 1 | 0 | my ($self, $worksheets) = @_; | 
| 126 |  |  |  |  |  |  |  | 
| 127 | 0 |  |  |  |  | 0 | my $handle = $self->filehandle; | 
| 128 | 0 |  |  |  |  | 0 | binmode $handle, ':raw'; | 
| 129 | 0 |  |  |  |  | 0 | my $xls = Spreadsheet::WriteExcel->new($handle); | 
| 130 |  |  |  |  |  |  |  | 
| 131 | 0 |  |  |  |  | 0 | my $bold = $xls->add_format(); | 
| 132 |  |  |  |  |  |  |  | 
| 133 | 0 |  |  |  |  | 0 | $bold->set_bold(); | 
| 134 |  |  |  |  |  |  |  | 
| 135 |  |  |  |  |  |  | # Collate formats | 
| 136 |  |  |  |  |  |  | # fmts stores colour data, and format data (indexed by md5 of format hash) | 
| 137 | 0 |  |  |  |  | 0 | my %fmts = ( colours => {}, | 
| 138 |  |  |  |  |  |  | colour_indexes => [ 19, 21, 24 .. 32, 34 .. 52, 53 .. 63 ], | 
| 139 |  |  |  |  |  |  | last_colour_index => 0, | 
| 140 |  |  |  |  |  |  | _tempfiles => {}, | 
| 141 |  |  |  |  |  |  | ); | 
| 142 | 0 |  |  |  |  | 0 | for my $tab (@$worksheets) { | 
| 143 | 0 |  |  |  |  | 0 | _collate_formats($xls, \%fmts, $tab->{title}); | 
| 144 | 0 | 0 |  |  |  | 0 | _collate_formats($xls, \%fmts, $_) for @{$tab->{headings} || []}; | 
|  | 0 |  |  |  |  | 0 |  | 
| 145 | 0 |  |  |  |  | 0 | for my $row (@{$tab->{'data'}}) { | 
|  | 0 |  |  |  |  | 0 |  | 
| 146 | 0 |  |  |  |  | 0 | for my $cell (@$row) { | 
| 147 | 0 |  |  |  |  | 0 | _collate_formats($xls, \%fmts, $cell); | 
| 148 |  |  |  |  |  |  | } | 
| 149 |  |  |  |  |  |  | } | 
| 150 |  |  |  |  |  |  | } | 
| 151 |  |  |  |  |  |  |  | 
| 152 | 0 |  |  |  |  | 0 | my $tabcount = 0; | 
| 153 | 0 |  |  |  |  | 0 | foreach my $tab (@$worksheets) { | 
| 154 | 0 |  |  |  |  | 0 | $tabcount++; | 
| 155 | 0 |  | 0 |  |  | 0 | my $name = $tab->{'sheetname'} || "Sheet $tabcount"; | 
| 156 | 0 | 0 |  |  |  | 0 | $name = substr($name, 0, 31) if length($name) > 31; | 
| 157 | 0 |  |  |  |  | 0 | $name = Encode::encode_utf8($name); | 
| 158 | 0 |  |  |  |  | 0 | my $worksheet = $xls->add_worksheet($name); | 
| 159 | 0 |  |  |  |  | 0 | $name = $worksheet->get_name(); # in case characters get altered | 
| 160 | 0 |  | 0 |  |  | 0 | my $row = $self->{layout}->{start_row} || 0; | 
| 161 | 0 |  |  |  |  | 0 | my $col = 0; | 
| 162 | 0 | 0 |  |  |  | 0 | my @cols = map { length(ref($_) eq 'ARRAY' ? $_->[0] : $_); } @{$tab->{'headings'}}; | 
|  | 0 |  |  |  |  | 0 |  | 
|  | 0 |  |  |  |  | 0 |  | 
| 163 |  |  |  |  |  |  |  | 
| 164 | 0 |  |  |  |  | 0 | _write_layout($worksheet, $xls, \%fmts, $tab, | 
| 165 |  |  |  |  |  |  | merge($self->{header_layout}, $tab->{header_layout}), | 
| 166 |  |  |  |  |  |  | 0, | 
| 167 |  |  |  |  |  |  | \$row, \@cols); | 
| 168 |  |  |  |  |  |  |  | 
| 169 | 0 |  |  |  |  | 0 | $row++; | 
| 170 | 0 |  |  |  |  | 0 | _write_cell($worksheet, \%fmts, $row++, 0, $tab->{'title'}, $bold); | 
| 171 | 0 |  |  |  |  | 0 | for my $header (@{$tab->{headings}}) { | 
|  | 0 |  |  |  |  | 0 |  | 
| 172 | 0 |  |  |  |  | 0 | _write_cell($worksheet, \%fmts, $row, $col++, $header, $bold); | 
| 173 |  |  |  |  |  |  | } | 
| 174 | 0 |  |  |  |  | 0 | $row++; | 
| 175 |  |  |  |  |  |  |  | 
| 176 | 0 |  |  |  |  | 0 | my $start_row = $row; # keep data start row for chart references | 
| 177 | 0 |  |  |  |  | 0 | foreach my $line (@{$tab->{'data'}}) { | 
|  | 0 |  |  |  |  | 0 |  | 
| 178 | 0 |  |  |  |  | 0 | $col = 0; | 
| 179 | 0 |  |  |  |  | 0 | for my $cell (@{$line}) { | 
|  | 0 |  |  |  |  | 0 |  | 
| 180 | 0 |  |  |  |  | 0 | _write_cell($worksheet, \%fmts, $row, $col, $cell); | 
| 181 | 0 |  |  |  |  | 0 | $col++; | 
| 182 |  |  |  |  |  |  | } | 
| 183 | 0 |  |  |  |  | 0 | $row++; | 
| 184 |  |  |  |  |  |  |  | 
| 185 | 0 |  |  |  |  | 0 | for my $i (0..@$line) { | 
| 186 | 0 | 0 | 0 |  |  | 0 | $cols[$i] = length(_text_of($line->[$i])) if ($line->[$i] && length(_text_of($line->[$i])) > ($cols[$i] || 0)); | 
|  |  |  | 0 |  |  |  |  | 
| 187 |  |  |  |  |  |  | } | 
| 188 |  |  |  |  |  |  | } | 
| 189 |  |  |  |  |  |  |  | 
| 190 | 0 |  |  |  |  | 0 | foreach my $chart (@{$tab->{chart}}) { | 
|  | 0 |  |  |  |  | 0 |  | 
| 191 | 0 |  | 0 |  |  | 0 | my $obj = $xls->add_chart( type => ($chart->{type} || 'line'), embedded => 1 ); | 
| 192 | 0 |  |  |  |  | 0 | for my $series (@{$chart->{series}}) { | 
|  | 0 |  |  |  |  | 0 |  | 
| 193 | 0 |  |  |  |  | 0 | $obj->add_series(categories => xl_range_formula($name, | 
| 194 |  |  |  |  |  |  | $series->{categories}[0] + $start_row, | 
| 195 |  |  |  |  |  |  | $series->{categories}[1] + $start_row, | 
| 196 |  |  |  |  |  |  | $series->{categories}[2], | 
| 197 |  |  |  |  |  |  | $series->{categories}[3]), | 
| 198 |  |  |  |  |  |  | values => xl_range_formula($name, | 
| 199 |  |  |  |  |  |  | $series->{values}[0] + $start_row, | 
| 200 |  |  |  |  |  |  | $series->{values}[1] + $start_row, | 
| 201 |  |  |  |  |  |  | $series->{values}[2], | 
| 202 |  |  |  |  |  |  | $series->{values}[3]), | 
| 203 |  |  |  |  |  |  | name_formula => _to_formula($name, $start_row, $series->{name_formula}), | 
| 204 |  |  |  |  |  |  | name => $series->{name}, | 
| 205 |  |  |  |  |  |  | ); | 
| 206 |  |  |  |  |  |  | } | 
| 207 |  |  |  |  |  |  |  | 
| 208 | 0 |  |  |  |  | 0 | for my $method (keys %chart_methods) { | 
| 209 | 0 | 0 |  |  |  | 0 | next unless exists $chart->{$method}; | 
| 210 |  |  |  |  |  |  |  | 
| 211 | 0 |  |  |  |  | 0 | my %props = map { $_ => $chart->{$method}{$_} } | 
|  | 0 |  |  |  |  | 0 |  | 
| 212 | 0 |  |  |  |  | 0 | grep { exists $chart->{$method}{$_} } @{$chart_methods{$method}}; | 
|  | 0 |  |  |  |  | 0 |  | 
| 213 | 0 |  |  |  |  | 0 | $method = 'set_' . $method; | 
| 214 | 0 | 0 |  |  |  | 0 | if (scalar keys %props > 0) { | 
| 215 | 0 | 0 |  |  |  | 0 | if (exists $props{name_formula}) { | 
| 216 | 0 |  |  |  |  | 0 | $props{name_formula} = _to_formula($name, $start_row, $props{name_formula}); | 
| 217 |  |  |  |  |  |  | } | 
| 218 | 0 |  |  |  |  | 0 | _add_custom_colour($xls, \%fmts, \%props); | 
| 219 | 0 |  |  |  |  | 0 | $obj->$method(%props); | 
| 220 |  |  |  |  |  |  | } | 
| 221 |  |  |  |  |  |  | } | 
| 222 | 0 |  |  |  |  | 0 | my $ins_row = ++$row; | 
| 223 | 0 | 0 |  |  |  | 0 | if (defined $chart->{abs_row}) { | 
| 224 | 0 |  |  |  |  | 0 | $ins_row = $chart->{abs_row} + $start_row; | 
| 225 |  |  |  |  |  |  | } | 
| 226 |  |  |  |  |  |  | else { | 
| 227 | 0 |  | 0 |  |  | 0 | $row += ($chart->{row} || 0) + floor(20 * ($chart->{y_scale} || 1)); | 
|  |  |  | 0 |  |  |  |  | 
| 228 |  |  |  |  |  |  | } | 
| 229 | 0 |  | 0 |  |  | 0 | $worksheet->insert_chart($ins_row + ($chart->{row} || 0), | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
| 230 |  |  |  |  |  |  | $chart->{abs_col} || $chart->{col} || 0, | 
| 231 |  |  |  |  |  |  | $obj, 0, 0, $chart->{x_scale} || 1, $chart->{y_scale} || 1); | 
| 232 |  |  |  |  |  |  | } | 
| 233 | 0 |  |  |  |  | 0 | $row++; | 
| 234 |  |  |  |  |  |  |  | 
| 235 | 0 |  |  |  |  | 0 | _write_layout($worksheet, $xls, \%fmts, $tab, | 
| 236 |  |  |  |  |  |  | merge($self->{footer_layout}, $tab->{footer_layout}), | 
| 237 |  |  |  |  |  |  | $row, | 
| 238 |  |  |  |  |  |  | \$row, \@cols); | 
| 239 |  |  |  |  |  |  |  | 
| 240 | 0 |  |  |  |  | 0 | for my $i (0..@cols) { | 
| 241 | 0 |  |  |  |  | 0 | $cols[$i] += 2; | 
| 242 | 0 | 0 |  |  |  | 0 | $cols[$i] = 6 if ($cols[$i] < 6); | 
| 243 | 0 | 0 |  |  |  | 0 | $cols[$i] = 60 if ($cols[$i] > 60); | 
| 244 | 0 |  |  |  |  | 0 | $worksheet->set_column($i, $i, $cols[$i]); | 
| 245 |  |  |  |  |  |  | } | 
| 246 |  |  |  |  |  |  | } | 
| 247 |  |  |  |  |  |  |  | 
| 248 | 0 |  |  |  |  | 0 | $xls->close(); | 
| 249 | 0 |  |  |  |  | 0 | unlink $_ for values %{$fmts{_tempfiles}}; | 
|  | 0 |  |  |  |  | 0 |  | 
| 250 |  |  |  |  |  |  | } | 
| 251 |  |  |  |  |  |  |  | 
| 252 |  |  |  |  |  |  | sub _to_formula { | 
| 253 | 0 |  |  | 0 |  | 0 | my ($name, $start_row, $name_formula) = @_; | 
| 254 | 0 |  |  |  |  | 0 | return "='$name'!" . xl_rowcol_to_cell($name_formula->[0] + $start_row, | 
| 255 |  |  |  |  |  |  | $name_formula->[1], 1, 1); | 
| 256 |  |  |  |  |  |  | } | 
| 257 |  |  |  |  |  |  |  | 
| 258 |  |  |  |  |  |  | sub _get_sub_text { | 
| 259 | 0 |  |  | 0 |  | 0 | my $subst = shift; | 
| 260 | 0 |  | 0 |  |  | 0 | return (ref($subst) eq 'ARRAY' ? $subst->[0] : $subst) || ''; | 
| 261 |  |  |  |  |  |  | } | 
| 262 |  |  |  |  |  |  |  | 
| 263 |  |  |  |  |  |  | sub _substitute_text { | 
| 264 | 0 |  |  | 0 |  | 0 | my $tab = shift; | 
| 265 | 0 |  |  |  |  | 0 | my $texts = shift; | 
| 266 |  |  |  |  |  |  |  | 
| 267 | 0 | 0 |  |  |  | 0 | $texts = ref($texts) eq 'ARRAY' ? dclone($texts) : [ $texts ]; | 
| 268 | 0 |  |  |  |  | 0 | for (@$texts) { | 
| 269 | 0 |  |  |  |  | 0 | s{(?{$1}) }eg; | 
|  | 0 |  |  |  |  | 0 |  | 
| 270 | 0 |  |  |  |  | 0 | s/\\@/@/g; | 
| 271 |  |  |  |  |  |  | } | 
| 272 | 0 |  |  |  |  | 0 | return $texts; | 
| 273 |  |  |  |  |  |  | } | 
| 274 |  |  |  |  |  |  |  | 
| 275 |  |  |  |  |  |  |  | 
| 276 |  |  |  |  |  |  | sub _row_of { | 
| 277 | 0 |  |  | 0 |  | 0 | my $img = shift; | 
| 278 | 0 |  |  |  |  | 0 | my $baserow = shift; | 
| 279 |  |  |  |  |  |  |  | 
| 280 | 0 | 0 |  |  |  | 0 | if (defined $img->{row}) { | 
| 281 | 0 |  |  |  |  | 0 | $_[0] = $img->{row}; | 
| 282 | 0 |  |  |  |  | 0 | return $baserow + $img->{row}; | 
| 283 |  |  |  |  |  |  | } | 
| 284 | 0 |  |  |  |  | 0 | return $baserow + ++$_[0]; | 
| 285 |  |  |  |  |  |  | } | 
| 286 |  |  |  |  |  |  |  | 
| 287 |  |  |  |  |  |  | sub _text_of { | 
| 288 | 52 |  |  | 52 |  | 71 | my $cell = shift; | 
| 289 |  |  |  |  |  |  |  | 
| 290 | 52 | 50 |  |  |  | 109 | $cell = $cell->[0] if ref($cell) eq 'ARRAY'; | 
| 291 | 52 | 50 |  |  |  | 237 | return defined $cell ? $cell : ''; | 
| 292 |  |  |  |  |  |  | } | 
| 293 |  |  |  |  |  |  |  | 
| 294 |  |  |  |  |  |  | sub _cache_image_file { | 
| 295 | 0 |  |  | 0 |  | 0 | my ($fmts, $img) = @_; | 
| 296 |  |  |  |  |  |  |  | 
| 297 | 0 | 0 |  |  |  | 0 | return $img->{filename} unless $img->{filename} =~ m{^https?://}; | 
| 298 |  |  |  |  |  |  |  | 
| 299 | 0 | 0 | 0 |  |  | 0 | if (exists($fmts->{_tempfiles}{$img->{filename}}) | 
| 300 |  |  |  |  |  |  | && -f $fmts->{_tempfiles}{$img->{filename}}) { | 
| 301 | 0 |  |  |  |  | 0 | return $img->{filename} = $fmts->{_tempfiles}{$img->{filename}}; | 
| 302 |  |  |  |  |  |  | } | 
| 303 | 0 |  |  |  |  | 0 | my $ua = LWP::UserAgent->new; | 
| 304 | 0 |  |  |  |  | 0 | my $response = $ua->get($img->{filename}); | 
| 305 | 0 | 0 |  |  |  | 0 | if (! $response->is_success) { | 
| 306 | 0 |  |  |  |  | 0 | warn "Failed to download $img->{filename}\n"; | 
| 307 | 0 |  |  |  |  | 0 | return; | 
| 308 |  |  |  |  |  |  | } | 
| 309 | 0 | 0 |  |  |  | 0 | if (my $dst = File::Temp->new(UNLINK => 0)) { | 
| 310 | 0 |  |  |  |  | 0 | print $dst $response->decoded_content; | 
| 311 | 0 |  |  |  |  | 0 | close $dst; | 
| 312 | 0 |  |  |  |  | 0 | return $img->{filename} = $fmts->{_tempfiles}{$img->{filename}} = $dst->filename; | 
| 313 |  |  |  |  |  |  | } | 
| 314 | 0 |  |  |  |  | 0 | warn "Failed to store $img->{filename}\n"; | 
| 315 | 0 |  |  |  |  | 0 | return; | 
| 316 |  |  |  |  |  |  | } | 
| 317 |  |  |  |  |  |  |  | 
| 318 |  |  |  |  |  |  | sub _write_layout { | 
| 319 | 0 |  |  | 0 |  | 0 | my ($worksheet, $xls, $fmts, $tab, $layout, $baserow, $rowref, $colsref) = @_; | 
| 320 |  |  |  |  |  |  |  | 
| 321 | 0 |  |  |  |  | 0 | my $current_row = 0; | 
| 322 | 0 |  |  |  |  | 0 | for my $method (keys %$layout) { | 
| 323 | 0 |  |  |  |  | 0 | my $params = $layout->{$method}; | 
| 324 | 0 | 0 | 0 |  |  | 0 | if ($method eq 'image') { | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
| 325 | 0 | 0 |  |  |  | 0 | $params = [ $params ] unless ref($params) eq 'ARRAY'; | 
| 326 | 0 |  |  |  |  | 0 | for my $img (@{$params}) { | 
|  | 0 |  |  |  |  | 0 |  | 
| 327 | 0 | 0 |  |  |  | 0 | _cache_image_file($fmts, $img) or next; | 
| 328 | 0 |  | 0 |  |  | 0 | $worksheet->insert_image(_row_of($img, $baserow, $current_row), | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
| 329 |  |  |  |  |  |  | $img->{col} || 0, | 
| 330 |  |  |  |  |  |  | $img->{filename}, | 
| 331 |  |  |  |  |  |  | $img->{x_offset} || 0, | 
| 332 |  |  |  |  |  |  | $img->{y_offset} || 0, | 
| 333 |  |  |  |  |  |  | $img->{x_scale} || 1, | 
| 334 |  |  |  |  |  |  | $img->{y_scale} || 1, | 
| 335 |  |  |  |  |  |  | ); | 
| 336 |  |  |  |  |  |  | } | 
| 337 |  |  |  |  |  |  | } | 
| 338 |  |  |  |  |  |  | elsif ($method eq 'header' || $method eq 'footer') { | 
| 339 | 0 | 0 |  |  |  | 0 | $params = [ $params ] unless ref($params) eq 'ARRAY'; | 
| 340 | 0 |  |  |  |  | 0 | for my $hdr (@{$params}) { | 
|  | 0 |  |  |  |  | 0 |  | 
| 341 | 0 |  |  |  |  | 0 | my $texts = _substitute_text($tab, $hdr->{text}); | 
| 342 | 0 | 0 | 0 |  |  | 0 | if ($hdr->{colspan} || $hdr->{rowspan}) { | 
| 343 | 0 |  | 0 |  |  | 0 | my $format = _collate_formats($xls, $fmts, [ undef, $hdr->{cell_format} ||= { bold => 1 }], 1); | 
| 344 | 0 |  |  |  |  | 0 | my $i = 0; | 
| 345 | 0 |  |  |  |  | 0 | for my $text (@$texts) { | 
| 346 | 0 |  |  |  |  | 0 | my $r = _row_of($hdr, $baserow + $i, $current_row); | 
| 347 | 0 |  | 0 |  |  | 0 | my $c = $hdr->{col} || 0; | 
| 348 | 0 |  | 0 |  |  | 0 | $worksheet->merge_range($r, | 
|  |  |  | 0 |  |  |  |  | 
| 349 |  |  |  |  |  |  | $c, | 
| 350 |  |  |  |  |  |  | $r + ($hdr->{rowspan} || 0), | 
| 351 |  |  |  |  |  |  | $c + ($hdr->{colspan} || 0), | 
| 352 |  |  |  |  |  |  | $text, | 
| 353 |  |  |  |  |  |  | $format); | 
| 354 | 0 |  |  |  |  | 0 | $i++; | 
| 355 | 0 |  | 0 |  |  | 0 | $$rowref = max($$rowref, $r + ($hdr->{rowspan} || 0)); | 
| 356 | 0 | 0 | 0 |  |  | 0 | if (! $hdr->{colspan} && defined $colsref) { | 
| 357 | 0 |  |  |  |  | 0 | $colsref->[$hdr->{col}] = max($colsref->[$hdr->{col}], length($text)); | 
| 358 |  |  |  |  |  |  | } | 
| 359 |  |  |  |  |  |  | } | 
| 360 |  |  |  |  |  |  | } | 
| 361 |  |  |  |  |  |  | else { | 
| 362 | 0 |  |  |  |  | 0 | _collate_formats($xls, $fmts, [ undef, $hdr->{cell_format} ]); | 
| 363 | 0 |  |  |  |  | 0 | my $i = 0; | 
| 364 | 0 |  |  |  |  | 0 | for my $text (@$texts) { | 
| 365 | 0 |  | 0 |  |  | 0 | $text ||= ''; | 
| 366 | 0 |  |  |  |  | 0 | my $r = _row_of($hdr, $baserow + $i, $current_row); | 
| 367 | 0 |  | 0 |  |  | 0 | my $c = $hdr->{col} || 0; | 
| 368 | 0 |  |  |  |  | 0 | _write_cell($worksheet, $fmts, $r, $c, [ $text, $hdr->{cell_format} ]); | 
| 369 | 0 |  |  |  |  | 0 | $i++; | 
| 370 | 0 |  |  |  |  | 0 | $$rowref = max($$rowref, $r); | 
| 371 | 0 | 0 | 0 |  |  | 0 | $colsref->[$c] = max($colsref->[$c] || 0, length($text)) if defined $colsref; | 
| 372 |  |  |  |  |  |  | } | 
| 373 |  |  |  |  |  |  | } | 
| 374 |  |  |  |  |  |  | } | 
| 375 |  |  |  |  |  |  | } | 
| 376 |  |  |  |  |  |  | elsif ($worksheet->can($method)) { | 
| 377 | 0 | 0 |  |  |  | 0 | $worksheet->$method(ref($params) eq 'ARRAY' ? @$params : $params); | 
| 378 |  |  |  |  |  |  | } | 
| 379 |  |  |  |  |  |  |  | 
| 380 |  |  |  |  |  |  | } | 
| 381 |  |  |  |  |  |  | } | 
| 382 |  |  |  |  |  |  |  | 
| 383 |  |  |  |  |  |  | sub _to_legal_format { | 
| 384 | 0 |  |  | 0 |  | 0 | my $fmt = shift; | 
| 385 |  |  |  |  |  |  |  | 
| 386 | 0 | 0 |  |  |  | 0 | my %vals = map { $_ => $fmt->{$_} } | 
|  | 0 |  |  |  |  | 0 |  | 
| 387 | 0 |  |  |  |  | 0 | grep { exists($legal_name{$_}) && defined($fmt->{$_}) } | 
| 388 | 0 |  |  |  |  | 0 | keys %{$fmt}; | 
| 389 | 0 | 0 |  |  |  | 0 | if (scalar keys %vals != scalar keys %$fmt) { | 
| 390 | 0 |  |  |  |  | 0 | warn "Invalid format key found: " . join(' ', grep { ! exists($legal_name{$_}) } keys %{$fmt}) . "\n"; | 
|  | 0 |  |  |  |  | 0 |  | 
|  | 0 |  |  |  |  | 0 |  | 
| 391 |  |  |  |  |  |  | } | 
| 392 |  |  |  |  |  |  |  | 
| 393 | 0 |  |  |  |  | 0 | return \%vals; | 
| 394 |  |  |  |  |  |  | } | 
| 395 |  |  |  |  |  |  |  | 
| 396 |  |  |  |  |  |  | # Convert format to unique string | 
| 397 |  |  |  |  |  |  | # Not required to be reversible | 
| 398 |  |  |  |  |  |  | sub _to_format_key { | 
| 399 | 0 |  |  | 0 |  | 0 | my $fmt = shift; | 
| 400 | 0 |  |  |  |  | 0 | my $key = ''; | 
| 401 |  |  |  |  |  |  |  | 
| 402 | 0 |  |  |  |  | 0 | for my $k (sort keys %$fmt) { | 
| 403 | 0 |  |  |  |  | 0 | my $v = $fmt->{$k}; | 
| 404 | 0 |  |  |  |  | 0 | $key .= "$k=$v&"; | 
| 405 |  |  |  |  |  |  | } | 
| 406 |  |  |  |  |  |  |  | 
| 407 | 0 |  |  |  |  | 0 | return md5_hex($key); | 
| 408 |  |  |  |  |  |  | } | 
| 409 |  |  |  |  |  |  |  | 
| 410 |  |  |  |  |  |  | sub _add_custom_colour { | 
| 411 | 0 |  |  | 0 |  | 0 | my ($xls, $fmts, $fmt) = @_; | 
| 412 |  |  |  |  |  |  |  | 
| 413 |  |  |  |  |  |  | # Create custom colours for RGB triples #RRGGBB | 
| 414 | 0 |  |  |  |  | 0 | for my $colourkey (grep { m/color/ } keys %$fmt) { | 
|  | 0 |  |  |  |  | 0 |  | 
| 415 | 0 |  |  |  |  | 0 | my $colourval = $fmt->{$colourkey}; | 
| 416 | 0 | 0 |  |  |  | 0 | next unless $colourval =~ m/^#/; | 
| 417 | 0 | 0 |  |  |  | 0 | if (! exists($fmts->{colours}{$colourval})) { | 
| 418 | 0 |  |  |  |  | 0 | $fmts->{colours}{$colourval} = $xls->set_custom_color($fmts->{colour_indexes}[$fmts->{last_colour_index}++], $colourval); | 
| 419 |  |  |  |  |  |  | } | 
| 420 | 0 |  |  |  |  | 0 | $fmt->{$colourkey} = $fmts->{colours}{$colourval}; | 
| 421 |  |  |  |  |  |  | } | 
| 422 |  |  |  |  |  |  | } | 
| 423 |  |  |  |  |  |  |  | 
| 424 |  |  |  |  |  |  | sub _collate_formats { | 
| 425 | 0 |  |  | 0 |  | 0 | my ($xls, $fmts, $cell, $merged) = @_; | 
| 426 | 0 | 0 | 0 |  |  | 0 | if (ref($cell) eq 'ARRAY' && @$cell == 2 && ref($cell->[1]) eq 'HASH') { | 
|  |  |  | 0 |  |  |  |  | 
| 427 | 0 |  |  |  |  | 0 | my $fmt = _to_legal_format($cell->[1]); | 
| 428 | 0 | 0 |  |  |  | 0 | my $fmt_key = _to_format_key($merged ? { %$fmt, merged => 1 } : $fmt); | 
| 429 | 0 |  |  |  |  | 0 | _add_custom_colour($xls, $fmts, $fmt); | 
| 430 | 0 | 0 |  |  |  | 0 | unless (exists $fmts->{$fmt_key}) { | 
| 431 | 0 |  |  |  |  | 0 | $fmts->{$fmt_key} = $xls->add_format(%$fmt); | 
| 432 |  |  |  |  |  |  | } | 
| 433 | 0 |  |  |  |  | 0 | return $fmts->{$fmt_key} | 
| 434 |  |  |  |  |  |  | } | 
| 435 | 0 |  |  |  |  | 0 | return; | 
| 436 |  |  |  |  |  |  | } | 
| 437 |  |  |  |  |  |  |  | 
| 438 |  |  |  |  |  |  | sub _write_cell { | 
| 439 | 0 |  |  | 0 |  | 0 | my ($worksheet, $fmts, $row, $col, $cell, $def_fmt) = @_; | 
| 440 |  |  |  |  |  |  |  | 
| 441 | 0 | 0 |  |  |  | 0 | if (ref($cell) eq 'ARRAY') { | 
| 442 | 0 |  | 0 |  |  | 0 | $worksheet->write($row, $col, $cell->[0], $fmts->{_to_format_key(_to_legal_format($cell->[1]))} || $def_fmt); | 
| 443 |  |  |  |  |  |  | } | 
| 444 |  |  |  |  |  |  | else { | 
| 445 | 0 |  |  |  |  | 0 | $worksheet->write($row, $col, $cell, $def_fmt); | 
| 446 |  |  |  |  |  |  | } | 
| 447 |  |  |  |  |  |  | } | 
| 448 |  |  |  |  |  |  |  | 
| 449 |  |  |  |  |  |  |  | 
| 450 |  |  |  |  |  |  | sub open | 
| 451 |  |  |  |  |  |  | { | 
| 452 | 2 |  |  | 2 | 1 | 5 | my ($self, $format, $filename) = @_; | 
| 453 | 2 |  |  |  |  | 4 | my $filehandle; | 
| 454 |  |  |  |  |  |  |  | 
| 455 | 2 | 50 |  |  |  | 7 | $self->format($format) if $format; | 
| 456 | 2 | 50 |  |  |  | 7 | $self->filename($filename) if $filename; | 
| 457 |  |  |  |  |  |  |  | 
| 458 | 2 | 50 |  |  |  | 5 | if ($self->filename) { | 
| 459 | 2 | 50 |  |  |  | 6 | $filehandle = IO::File->new(">" . $self->filename) or die "Failed to open " . $self->filename . ": $!"; | 
| 460 |  |  |  |  |  |  | } | 
| 461 |  |  |  |  |  |  | else { | 
| 462 | 0 |  |  |  |  | 0 | $filehandle = \*STDOUT; | 
| 463 |  |  |  |  |  |  | } | 
| 464 |  |  |  |  |  |  |  | 
| 465 | 2 |  |  |  |  | 345 | $self->filehandle($filehandle); | 
| 466 |  |  |  |  |  |  | } | 
| 467 |  |  |  |  |  |  |  | 
| 468 |  |  |  |  |  |  |  | 
| 469 |  |  |  |  |  |  | sub close | 
| 470 |  |  |  |  |  |  | { | 
| 471 | 2 |  |  | 2 | 1 | 10 | my $self = shift; | 
| 472 |  |  |  |  |  |  |  | 
| 473 | 2 | 50 |  |  |  | 9 | if ($self->filehandle) { | 
| 474 | 2 |  |  |  |  | 7 | $self->filehandle->close(); | 
| 475 | 2 |  |  |  |  | 165 | $self->filehandle(undef); | 
| 476 | 2 |  |  |  |  | 6 | $self->filename(undef); | 
| 477 |  |  |  |  |  |  | } | 
| 478 |  |  |  |  |  |  | } | 
| 479 |  |  |  |  |  |  |  | 
| 480 |  |  |  |  |  |  | =head1 NAME | 
| 481 |  |  |  |  |  |  |  | 
| 482 |  |  |  |  |  |  | WWW::Analytics::MultiTouch::Tabular - Provides various output formats for writing tabular reports | 
| 483 |  |  |  |  |  |  |  | 
| 484 |  |  |  |  |  |  | =head1 SYNOPSIS | 
| 485 |  |  |  |  |  |  |  | 
| 486 |  |  |  |  |  |  | # Simple usage | 
| 487 |  |  |  |  |  |  |  | 
| 488 |  |  |  |  |  |  | use WWW::Analytics::MultiTouch::Tabular; | 
| 489 |  |  |  |  |  |  |  | 
| 490 |  |  |  |  |  |  | my @data = ( [ 1, 2, 3 ], | 
| 491 |  |  |  |  |  |  | [ 4, 5, 6 ], | 
| 492 |  |  |  |  |  |  | [ 7, 8, 9 ], | 
| 493 |  |  |  |  |  |  | ); | 
| 494 |  |  |  |  |  |  | my @reports = ( | 
| 495 |  |  |  |  |  |  | { | 
| 496 |  |  |  |  |  |  | title => "Number of Results in Top 10, by Site", | 
| 497 |  |  |  |  |  |  | sheetname => "Top Positions", | 
| 498 |  |  |  |  |  |  | headings => [ "Site", "Engine", "Top 10 Results", "Unique URLs", "Top 10 Previous Week", "Unique URLs Previous Week" ], | 
| 499 |  |  |  |  |  |  | data => \@data, | 
| 500 |  |  |  |  |  |  | }, | 
| 501 |  |  |  |  |  |  | ... | 
| 502 |  |  |  |  |  |  | ); | 
| 503 |  |  |  |  |  |  | my $output = WWW::Analytics::MultiTouch::Tabular->new({format => 'txt', outfile => $file}); | 
| 504 |  |  |  |  |  |  | $output->print(\@reports); | 
| 505 |  |  |  |  |  |  | $output->close(); | 
| 506 |  |  |  |  |  |  |  | 
| 507 |  |  |  |  |  |  | # With formatting | 
| 508 |  |  |  |  |  |  |  | 
| 509 |  |  |  |  |  |  | my @data = ( [ [ 1, { color => 'red' } ], 2, 3 ], | 
| 510 |  |  |  |  |  |  | [ [ 4, { color => '#123456' ], 5, 6 ], | 
| 511 |  |  |  |  |  |  | [ [ 7, { bold => 1 } ], 8, 9 ], | 
| 512 |  |  |  |  |  |  | ); | 
| 513 |  |  |  |  |  |  |  | 
| 514 |  |  |  |  |  |  | =head1 DESCRIPTION | 
| 515 |  |  |  |  |  |  |  | 
| 516 |  |  |  |  |  |  | Takes a list of reports and outputs them in the specified format (text, csv, or Excel). | 
| 517 |  |  |  |  |  |  |  | 
| 518 |  |  |  |  |  |  | For Excel, supports extended formatting including headers, footers, colours, fonts, images, charts. | 
| 519 |  |  |  |  |  |  |  | 
| 520 |  |  |  |  |  |  | =head1 METHODS | 
| 521 |  |  |  |  |  |  |  | 
| 522 |  |  |  |  |  |  | =head2 new | 
| 523 |  |  |  |  |  |  |  | 
| 524 |  |  |  |  |  |  | $output = WWW::Analytics::MultiTouch::Tabular->new({format => 'txt', filename => $file}); | 
| 525 |  |  |  |  |  |  |  | 
| 526 |  |  |  |  |  |  | Creates a new WWW::Analytics::MultiTouch::Tabular object.  Options are as follows: | 
| 527 |  |  |  |  |  |  |  | 
| 528 |  |  |  |  |  |  | =over 4 | 
| 529 |  |  |  |  |  |  |  | 
| 530 |  |  |  |  |  |  | =item * format | 
| 531 |  |  |  |  |  |  |  | 
| 532 |  |  |  |  |  |  | txt, csv or xls. | 
| 533 |  |  |  |  |  |  |  | 
| 534 |  |  |  |  |  |  | =item * filename | 
| 535 |  |  |  |  |  |  |  | 
| 536 |  |  |  |  |  |  | Name of output file | 
| 537 |  |  |  |  |  |  |  | 
| 538 |  |  |  |  |  |  | =item * header_layout, footer_layout | 
| 539 |  |  |  |  |  |  |  | 
| 540 |  |  |  |  |  |  | See L. | 
| 541 |  |  |  |  |  |  |  | 
| 542 |  |  |  |  |  |  | =back | 
| 543 |  |  |  |  |  |  |  | 
| 544 |  |  |  |  |  |  | =head2 print | 
| 545 |  |  |  |  |  |  |  | 
| 546 |  |  |  |  |  |  | $output->print(\@reports); | 
| 547 |  |  |  |  |  |  |  | 
| 548 |  |  |  |  |  |  | Prints given data in txt, csv, or xls format. | 
| 549 |  |  |  |  |  |  |  | 
| 550 |  |  |  |  |  |  | Each item in @reports is a hash containing the following elements: | 
| 551 |  |  |  |  |  |  |  | 
| 552 |  |  |  |  |  |  | =over 4 | 
| 553 |  |  |  |  |  |  |  | 
| 554 |  |  |  |  |  |  | =item * title | 
| 555 |  |  |  |  |  |  |  | 
| 556 |  |  |  |  |  |  | Report title | 
| 557 |  |  |  |  |  |  |  | 
| 558 |  |  |  |  |  |  | =item * sheetname | 
| 559 |  |  |  |  |  |  |  | 
| 560 |  |  |  |  |  |  | Sheet name, where applicable (as in spreadsheet output). | 
| 561 |  |  |  |  |  |  |  | 
| 562 |  |  |  |  |  |  | =item * headings | 
| 563 |  |  |  |  |  |  |  | 
| 564 |  |  |  |  |  |  | Array of column headings.  Each heading entry may be a scalar (used as is) or a | 
| 565 |  |  |  |  |  |  | two-element array, in which case the first element is the data and the second | 
| 566 |  |  |  |  |  |  | element is the cell format.  See L | for cell formatting details. |  | 
| 567 |  |  |  |  |  |  |  | 
| 568 |  |  |  |  |  |  | =item * data | 
| 569 |  |  |  |  |  |  |  | 
| 570 |  |  |  |  |  |  | Array of data; each row is a row in the output, with columns corresponding to | 
| 571 |  |  |  |  |  |  | the column headings given.  Each data point may be a scalar (used as is) or a | 
| 572 |  |  |  |  |  |  | two-element array, in which case the first element is the data and the second | 
| 573 |  |  |  |  |  |  | element is the cell format.  See L | for cell formatting details. |  | 
| 574 |  |  |  |  |  |  |  | 
| 575 |  |  |  |  |  |  | Examples: | 
| 576 |  |  |  |  |  |  |  | 
| 577 |  |  |  |  |  |  | Simple data array, no formatting: | 
| 578 |  |  |  |  |  |  |  | 
| 579 |  |  |  |  |  |  | data => [ [ 1, 2, 3 ], | 
| 580 |  |  |  |  |  |  | [ 4, 5, 6 ], | 
| 581 |  |  |  |  |  |  | [ 7, 8, 9 ], | 
| 582 |  |  |  |  |  |  | ] | 
| 583 |  |  |  |  |  |  |  | 
| 584 |  |  |  |  |  |  | Data array with first entry in each row formatted: | 
| 585 |  |  |  |  |  |  |  | 
| 586 |  |  |  |  |  |  | data => [ [ [ 1, { color => 'red' } ], 2, 3 ], | 
| 587 |  |  |  |  |  |  | [ [ 4, { color => '#123456' ], 5, 6 ], | 
| 588 |  |  |  |  |  |  | [ [ 7, { bold => 1 } ], 8, 9 ], | 
| 589 |  |  |  |  |  |  | ] | 
| 590 |  |  |  |  |  |  |  | 
| 591 |  |  |  |  |  |  |  | 
| 592 |  |  |  |  |  |  | =item * header_layout, footer_layout | 
| 593 |  |  |  |  |  |  |  | 
| 594 |  |  |  |  |  |  | See L. | 
| 595 |  |  |  |  |  |  |  | 
| 596 |  |  |  |  |  |  | =item * chart | 
| 597 |  |  |  |  |  |  |  | 
| 598 |  |  |  |  |  |  | Insert one or more charts.  Example: | 
| 599 |  |  |  |  |  |  |  | 
| 600 |  |  |  |  |  |  | chart => [ { type => 'column', | 
| 601 |  |  |  |  |  |  | x_scale => 1.5, | 
| 602 |  |  |  |  |  |  | y_scale => 1.5, | 
| 603 |  |  |  |  |  |  | series => [ map { | 
| 604 |  |  |  |  |  |  | { categories => [ -1, -1, 1, scalar @{$data[0]} ], | 
| 605 |  |  |  |  |  |  | values => [ $_, $_, 1, scalar @{$data[0]} ], | 
| 606 |  |  |  |  |  |  | name_formula => [$_, 0], | 
| 607 |  |  |  |  |  |  | name => $data[$_][0], | 
| 608 |  |  |  |  |  |  | } } (0 .. @data - 1) ] | 
| 609 |  |  |  |  |  |  | } ], | 
| 610 |  |  |  |  |  |  |  | 
| 611 |  |  |  |  |  |  | Options are: | 
| 612 |  |  |  |  |  |  |  | 
| 613 |  |  |  |  |  |  | =over 4 | 
| 614 |  |  |  |  |  |  |  | 
| 615 |  |  |  |  |  |  | =item * type | 
| 616 |  |  |  |  |  |  |  | 
| 617 |  |  |  |  |  |  | May be 'area', 'bar', 'column', 'line', 'pie', 'scatter', 'stock'. See | 
| 618 |  |  |  |  |  |  | L for more details on the available types. | 
| 619 |  |  |  |  |  |  |  | 
| 620 |  |  |  |  |  |  | =item * series | 
| 621 |  |  |  |  |  |  |  | 
| 622 |  |  |  |  |  |  | This is the array of data series for the chart.  'categories', 'values' and | 
| 623 |  |  |  |  |  |  | 'name_formula' are given in terms of cell ranges referenced to the start of the | 
| 624 |  |  |  |  |  |  | spreadsheet data.  The heading row may be referenced as -1 relative to the start | 
| 625 |  |  |  |  |  |  | of the data.  See L for more details. | 
| 626 |  |  |  |  |  |  |  | 
| 627 |  |  |  |  |  |  | =item * row, abs_row | 
| 628 |  |  |  |  |  |  |  | 
| 629 |  |  |  |  |  |  | Optional row offset or absolute row position.  Will be placed at the current row if not specified. | 
| 630 |  |  |  |  |  |  |  | 
| 631 |  |  |  |  |  |  | =item * col, abs_col | 
| 632 |  |  |  |  |  |  |  | 
| 633 |  |  |  |  |  |  | Optional column number.  Will be placed at column 0 if not specified. | 
| 634 |  |  |  |  |  |  |  | 
| 635 |  |  |  |  |  |  | =item * x_scale, y_scale | 
| 636 |  |  |  |  |  |  |  | 
| 637 |  |  |  |  |  |  | Optional chart scaling factors. | 
| 638 |  |  |  |  |  |  |  | 
| 639 |  |  |  |  |  |  | =item * title, x_axis, y_axis, legend, chartarea, plotarea | 
| 640 |  |  |  |  |  |  |  | 
| 641 |  |  |  |  |  |  | See the corresponding set_* method in L for more details. | 
| 642 |  |  |  |  |  |  |  | 
| 643 |  |  |  |  |  |  | =back | 
| 644 |  |  |  |  |  |  |  | 
| 645 |  |  |  |  |  |  | =back | 
| 646 |  |  |  |  |  |  |  | 
| 647 |  |  |  |  |  |  | =head2 txt | 
| 648 |  |  |  |  |  |  |  | 
| 649 |  |  |  |  |  |  | $output->txt(\@reports); | 
| 650 |  |  |  |  |  |  |  | 
| 651 |  |  |  |  |  |  | Generate output in plain text format. | 
| 652 |  |  |  |  |  |  |  | 
| 653 |  |  |  |  |  |  | =head2 csv | 
| 654 |  |  |  |  |  |  |  | 
| 655 |  |  |  |  |  |  | $output->csv(\@reports); | 
| 656 |  |  |  |  |  |  |  | 
| 657 |  |  |  |  |  |  | Generate output in CSV format. | 
| 658 |  |  |  |  |  |  |  | 
| 659 |  |  |  |  |  |  | =head2 xls | 
| 660 |  |  |  |  |  |  |  | 
| 661 |  |  |  |  |  |  | $output->xls(\@reports); | 
| 662 |  |  |  |  |  |  |  | 
| 663 |  |  |  |  |  |  | Generate output in Excel spreadsheet format. | 
| 664 |  |  |  |  |  |  |  | 
| 665 |  |  |  |  |  |  |  | 
| 666 |  |  |  |  |  |  | =head2 open | 
| 667 |  |  |  |  |  |  |  | 
| 668 |  |  |  |  |  |  | $output->format('csv'); | 
| 669 |  |  |  |  |  |  | $output->filename("$dir/csv-test.csv"); | 
| 670 |  |  |  |  |  |  | $output->open; | 
| 671 |  |  |  |  |  |  |  | 
| 672 |  |  |  |  |  |  | $output->open("xls", "$dir/xls-test.xls"); | 
| 673 |  |  |  |  |  |  |  | 
| 674 |  |  |  |  |  |  | 'open' opens a file for writing.  It is usually not necessary to call 'open' as it | 
| 675 |  |  |  |  |  |  | is implicit in 'new'.  However, if you wish to re-use the object created with | 
| 676 |  |  |  |  |  |  | 'new' to output a different format or to a different file, then you need to call | 
| 677 |  |  |  |  |  |  | open with the new format/file arguments, or after setting the new format and | 
| 678 |  |  |  |  |  |  | output file with the format and outfile methods. | 
| 679 |  |  |  |  |  |  |  | 
| 680 |  |  |  |  |  |  |  | 
| 681 |  |  |  |  |  |  | =head2 format | 
| 682 |  |  |  |  |  |  |  | 
| 683 |  |  |  |  |  |  | $output->format('xls'); | 
| 684 |  |  |  |  |  |  |  | 
| 685 |  |  |  |  |  |  | Set/get format to be used in L.  L must be called for the format change to take effect. | 
| 686 |  |  |  |  |  |  |  | 
| 687 |  |  |  |  |  |  | =head2 filename | 
| 688 |  |  |  |  |  |  |  | 
| 689 |  |  |  |  |  |  | Set/get filename to be used in L. L must be called for the filename change to take effect. | 
| 690 |  |  |  |  |  |  |  | 
| 691 |  |  |  |  |  |  | If no filename is provided as an argument or previously set, STDOUT will be used. | 
| 692 |  |  |  |  |  |  |  | 
| 693 |  |  |  |  |  |  | =head2 outfile | 
| 694 |  |  |  |  |  |  |  | 
| 695 |  |  |  |  |  |  | 'outfile' is equivalent to 'filename', provided for backward compatibility. | 
| 696 |  |  |  |  |  |  |  | 
| 697 |  |  |  |  |  |  | =head2 filehandle | 
| 698 |  |  |  |  |  |  |  | 
| 699 |  |  |  |  |  |  | $output->filehandle(\*STDOUT); | 
| 700 |  |  |  |  |  |  |  | 
| 701 |  |  |  |  |  |  | As an alternative to 'open', you can set the file handle explicitly using filehandle(). | 
| 702 |  |  |  |  |  |  |  | 
| 703 |  |  |  |  |  |  | =head2 close | 
| 704 |  |  |  |  |  |  |  | 
| 705 |  |  |  |  |  |  | Close file | 
| 706 |  |  |  |  |  |  |  | 
| 707 |  |  |  |  |  |  | =head1 HEADERS AND FOOTERS | 
| 708 |  |  |  |  |  |  |  | 
| 709 |  |  |  |  |  |  | A set of images, rows of text and/or spreadsheet operations to create page headers and footers.  'header_layout' is used prior to placing any data on the page, and 'footer_layout' afterwards.  Example: | 
| 710 |  |  |  |  |  |  |  | 
| 711 |  |  |  |  |  |  | 'header_layout' => { | 
| 712 |  |  |  |  |  |  | 'hide_gridlines' => '2', | 
| 713 |  |  |  |  |  |  | 'image' => [ | 
| 714 |  |  |  |  |  |  | { | 
| 715 |  |  |  |  |  |  | 'filename' => 'http://www.multitouchanalytics.com/images/multitouch-analytics-header.jpg', | 
| 716 |  |  |  |  |  |  | 'col' => 0, | 
| 717 |  |  |  |  |  |  | 'row' => 1, | 
| 718 |  |  |  |  |  |  | 'x_scale' => 0.7, | 
| 719 |  |  |  |  |  |  | 'y_scale' => 0.7 | 
| 720 |  |  |  |  |  |  | }, | 
| 721 |  |  |  |  |  |  | ], | 
| 722 |  |  |  |  |  |  | 'header' => [ | 
| 723 |  |  |  |  |  |  | { | 
| 724 |  |  |  |  |  |  | 'colspan' => '5', | 
| 725 |  |  |  |  |  |  | 'cell_format' => { | 
| 726 |  |  |  |  |  |  | 'color' => 'white', | 
| 727 |  |  |  |  |  |  | 'align' => 'center', | 
| 728 |  |  |  |  |  |  | 'bold' => 1, | 
| 729 |  |  |  |  |  |  | 'bg_color' => 'blue', | 
| 730 |  |  |  |  |  |  | 'size' => '16' | 
| 731 |  |  |  |  |  |  | }, | 
| 732 |  |  |  |  |  |  | 'text' => 'Multi Touch Reporting', | 
| 733 |  |  |  |  |  |  | 'col' => 0, | 
| 734 |  |  |  |  |  |  | 'row' => '5' | 
| 735 |  |  |  |  |  |  | }, | 
| 736 |  |  |  |  |  |  | { | 
| 737 |  |  |  |  |  |  | 'cell_format' => { | 
| 738 |  |  |  |  |  |  | 'align' => 'right', | 
| 739 |  |  |  |  |  |  | 'bold' => 1 | 
| 740 |  |  |  |  |  |  | }, | 
| 741 |  |  |  |  |  |  | 'text' => [ | 
| 742 |  |  |  |  |  |  | 'Generation Date:', | 
| 743 |  |  |  |  |  |  | 'Report Type:', | 
| 744 |  |  |  |  |  |  | 'Date Range:', | 
| 745 |  |  |  |  |  |  | 'Analysis Window:' | 
| 746 |  |  |  |  |  |  | ], | 
| 747 |  |  |  |  |  |  | 'col' => 0, | 
| 748 |  |  |  |  |  |  | 'row' => '7' | 
| 749 |  |  |  |  |  |  | }, | 
| 750 |  |  |  |  |  |  | { | 
| 751 |  |  |  |  |  |  | 'text' => [ | 
| 752 |  |  |  |  |  |  | '@generation_date', | 
| 753 |  |  |  |  |  |  | '@title', | 
| 754 |  |  |  |  |  |  | '@start_date - @end_date', | 
| 755 |  |  |  |  |  |  | '@window_length days' | 
| 756 |  |  |  |  |  |  | ], | 
| 757 |  |  |  |  |  |  | 'col' => 1, | 
| 758 |  |  |  |  |  |  | 'row' => '7' | 
| 759 |  |  |  |  |  |  | } | 
| 760 |  |  |  |  |  |  | ], | 
| 761 |  |  |  |  |  |  | 'start_row' => '10' | 
| 762 |  |  |  |  |  |  | } | 
| 763 |  |  |  |  |  |  | } | 
| 764 |  |  |  |  |  |  |  | 
| 765 |  |  |  |  |  |  | Options are as follows: | 
| 766 |  |  |  |  |  |  |  | 
| 767 |  |  |  |  |  |  | =over 4 | 
| 768 |  |  |  |  |  |  |  | 
| 769 |  |  |  |  |  |  | =item * image | 
| 770 |  |  |  |  |  |  |  | 
| 771 |  |  |  |  |  |  | Specifies an image (PNG or JPEG) or images to be placed into the spreadsheet. | 
| 772 |  |  |  |  |  |  | Multiple images may be inserted by specifying an array of option hashes, | 
| 773 |  |  |  |  |  |  | comprising the following keys: | 
| 774 |  |  |  |  |  |  |  | 
| 775 |  |  |  |  |  |  | =over 4 | 
| 776 |  |  |  |  |  |  |  | 
| 777 |  |  |  |  |  |  | =item * row | 
| 778 |  |  |  |  |  |  |  | 
| 779 |  |  |  |  |  |  | Optional row number.  Will be placed at the current row if not specified. | 
| 780 |  |  |  |  |  |  |  | 
| 781 |  |  |  |  |  |  | =item * col | 
| 782 |  |  |  |  |  |  |  | 
| 783 |  |  |  |  |  |  | Optional column number.  Will be placed at column 0 if not specified. | 
| 784 |  |  |  |  |  |  |  | 
| 785 |  |  |  |  |  |  | =item * filename | 
| 786 |  |  |  |  |  |  |  | 
| 787 |  |  |  |  |  |  | Filename or URL of the image. | 
| 788 |  |  |  |  |  |  |  | 
| 789 |  |  |  |  |  |  | =item * x_offset, y_offset | 
| 790 |  |  |  |  |  |  |  | 
| 791 |  |  |  |  |  |  | Optional pixel offsets of image from top-left of cell. | 
| 792 |  |  |  |  |  |  |  | 
| 793 |  |  |  |  |  |  | =item * x_scale, y_scale | 
| 794 |  |  |  |  |  |  |  | 
| 795 |  |  |  |  |  |  | Optional image scaling factors. | 
| 796 |  |  |  |  |  |  |  | 
| 797 |  |  |  |  |  |  | =back | 
| 798 |  |  |  |  |  |  |  | 
| 799 |  |  |  |  |  |  | =item * header, footer | 
| 800 |  |  |  |  |  |  |  | 
| 801 |  |  |  |  |  |  | Specifies formatted rows of text. 'header' is intended to be used for | 
| 802 |  |  |  |  |  |  | 'header_layout' and 'footer' for 'footer_layout', but it doesn't actually matter | 
| 803 |  |  |  |  |  |  | if they are used the other way around.  Multiple rows may be inserted by | 
| 804 |  |  |  |  |  |  | specifying an array of option hashes, comprising the following keys: | 
| 805 |  |  |  |  |  |  |  | 
| 806 |  |  |  |  |  |  | =over 4 | 
| 807 |  |  |  |  |  |  |  | 
| 808 |  |  |  |  |  |  | =item * cell_format | 
| 809 |  |  |  |  |  |  |  | 
| 810 |  |  |  |  |  |  | See L | . |  | 
| 811 |  |  |  |  |  |  |  | 
| 812 |  |  |  |  |  |  | =item * row | 
| 813 |  |  |  |  |  |  |  | 
| 814 |  |  |  |  |  |  | Optional row number.  Will be placed at the current row if not specified. | 
| 815 |  |  |  |  |  |  |  | 
| 816 |  |  |  |  |  |  | =item * col | 
| 817 |  |  |  |  |  |  |  | 
| 818 |  |  |  |  |  |  | Optional column number.  Will be placed at column 0 if not specified. | 
| 819 |  |  |  |  |  |  |  | 
| 820 |  |  |  |  |  |  | =item * rowspan, colspan | 
| 821 |  |  |  |  |  |  |  | 
| 822 |  |  |  |  |  |  | Optional row and column spans for merged cells. | 
| 823 |  |  |  |  |  |  |  | 
| 824 |  |  |  |  |  |  | =item * text | 
| 825 |  |  |  |  |  |  |  | 
| 826 |  |  |  |  |  |  | Lines of text.  If given as an array, each line will be inserted on subsequent | 
| 827 |  |  |  |  |  |  | rows, keeping the same formatting options. | 
| 828 |  |  |  |  |  |  |  | 
| 829 |  |  |  |  |  |  | Variables may be specified in the text as @variable_name, and will be | 
| 830 |  |  |  |  |  |  | substituted.  Valid variable names are the top level keys passed in the report | 
| 831 |  |  |  |  |  |  | hash to print(). | 
| 832 |  |  |  |  |  |  |  | 
| 833 |  |  |  |  |  |  | =back | 
| 834 |  |  |  |  |  |  |  | 
| 835 |  |  |  |  |  |  | =item * Any worksheet or page setup method from L | 
| 836 |  |  |  |  |  |  |  | 
| 837 |  |  |  |  |  |  | e.g. keep_leading_zeros, show_comments, set_first_sheet, set_tab_color, hide_gridlines, set_zoom, etc. | 
| 838 |  |  |  |  |  |  |  | 
| 839 |  |  |  |  |  |  | Any valid Spreadsheet::WriteExcel worksheet method will be invoked with the given values, i.e. | 
| 840 |  |  |  |  |  |  |  | 
| 841 |  |  |  |  |  |  | hide_gridlines => 1 | 
| 842 |  |  |  |  |  |  | print_area => [ 1, 2, 3, 4 ] | 
| 843 |  |  |  |  |  |  |  | 
| 844 |  |  |  |  |  |  | invokes $worksheet->hide_gridlines(1) and $worksheet->print_area(1, 2, 3, 4). | 
| 845 |  |  |  |  |  |  |  | 
| 846 |  |  |  |  |  |  | =back | 
| 847 |  |  |  |  |  |  |  | 
| 848 |  |  |  |  |  |  | =head1 CELL FORMAT | 
| 849 |  |  |  |  |  |  |  | 
| 850 |  |  |  |  |  |  | Cell formatting options are defined through a hashref of any text formatting | 
| 851 |  |  |  |  |  |  | options from L; specifically, any of 'font', 'size', | 
| 852 |  |  |  |  |  |  | 'color', 'bold', 'italic', 'underline', 'font_strikeout', 'font_script', | 
| 853 |  |  |  |  |  |  | 'font_outline', 'font_shadow', 'num_format', 'locked', 'hidden', 'align', | 
| 854 |  |  |  |  |  |  | 'valign', 'rotation', 'text_wrap', 'test_justlast', 'center_across', 'indent', | 
| 855 |  |  |  |  |  |  | 'shrink', 'pattern', 'bg_color', 'fg_color', 'border', 'bottom', 'top', 'left', | 
| 856 |  |  |  |  |  |  | 'right', 'border_color', 'bottom_color', 'top_color', 'left_color', | 
| 857 |  |  |  |  |  |  | 'right_color'. | 
| 858 |  |  |  |  |  |  |  | 
| 859 |  |  |  |  |  |  | =head1 AUTHOR | 
| 860 |  |  |  |  |  |  |  | 
| 861 |  |  |  |  |  |  | Jon Schutz, C<<  >> | 
| 862 |  |  |  |  |  |  |  | 
| 863 |  |  |  |  |  |  |  | 
| 864 |  |  |  |  |  |  | =head1 BUGS | 
| 865 |  |  |  |  |  |  |  | 
| 866 |  |  |  |  |  |  | Please report any bugs or feature requests to C, or through | 
| 867 |  |  |  |  |  |  | the web interface at L.  I will be notified, and then you'll | 
| 868 |  |  |  |  |  |  | automatically be notified of progress on your bug as I make changes. | 
| 869 |  |  |  |  |  |  |  | 
| 870 |  |  |  |  |  |  |  | 
| 871 |  |  |  |  |  |  | =head1 SUPPORT | 
| 872 |  |  |  |  |  |  |  | 
| 873 |  |  |  |  |  |  | You can find documentation for this module with the perldoc command. | 
| 874 |  |  |  |  |  |  |  | 
| 875 |  |  |  |  |  |  | perldoc WWW::Analytics::MultiTouch | 
| 876 |  |  |  |  |  |  |  | 
| 877 |  |  |  |  |  |  |  | 
| 878 |  |  |  |  |  |  | You can also look for information at: | 
| 879 |  |  |  |  |  |  |  | 
| 880 |  |  |  |  |  |  | =over 4 | 
| 881 |  |  |  |  |  |  |  | 
| 882 |  |  |  |  |  |  | =item * RT: CPAN's request tracker | 
| 883 |  |  |  |  |  |  |  | 
| 884 |  |  |  |  |  |  | L | 
| 885 |  |  |  |  |  |  |  | 
| 886 |  |  |  |  |  |  | =item * AnnoCPAN: Annotated CPAN documentation | 
| 887 |  |  |  |  |  |  |  | 
| 888 |  |  |  |  |  |  | L | 
| 889 |  |  |  |  |  |  |  | 
| 890 |  |  |  |  |  |  | =item * CPAN Ratings | 
| 891 |  |  |  |  |  |  |  | 
| 892 |  |  |  |  |  |  | L | 
| 893 |  |  |  |  |  |  |  | 
| 894 |  |  |  |  |  |  | =item * Search CPAN | 
| 895 |  |  |  |  |  |  |  | 
| 896 |  |  |  |  |  |  | L | 
| 897 |  |  |  |  |  |  |  | 
| 898 |  |  |  |  |  |  | =back | 
| 899 |  |  |  |  |  |  |  | 
| 900 |  |  |  |  |  |  |  | 
| 901 |  |  |  |  |  |  | =head1 COPYRIGHT & LICENSE | 
| 902 |  |  |  |  |  |  |  | 
| 903 |  |  |  |  |  |  | Copyright 2010 YourAmigo Ltd. | 
| 904 |  |  |  |  |  |  |  | 
| 905 |  |  |  |  |  |  | Permission is hereby granted, free of charge, to any person obtaining a copy | 
| 906 |  |  |  |  |  |  | of this software and associated documentation files (the "Software"), to deal | 
| 907 |  |  |  |  |  |  | in the Software without restriction, including without limitation the rights | 
| 908 |  |  |  |  |  |  | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | 
| 909 |  |  |  |  |  |  | copies of the Software, and to permit persons to whom the Software is | 
| 910 |  |  |  |  |  |  | furnished to do so, subject to the following conditions: | 
| 911 |  |  |  |  |  |  |  | 
| 912 |  |  |  |  |  |  | The above copyright notice and this permission notice shall be included in | 
| 913 |  |  |  |  |  |  | all copies or substantial portions of the Software. | 
| 914 |  |  |  |  |  |  |  | 
| 915 |  |  |  |  |  |  | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | 
| 916 |  |  |  |  |  |  | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | 
| 917 |  |  |  |  |  |  | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | 
| 918 |  |  |  |  |  |  | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | 
| 919 |  |  |  |  |  |  | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | 
| 920 |  |  |  |  |  |  | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | 
| 921 |  |  |  |  |  |  | THE SOFTWARE. | 
| 922 |  |  |  |  |  |  |  | 
| 923 |  |  |  |  |  |  | =cut | 
| 924 |  |  |  |  |  |  |  | 
| 925 |  |  |  |  |  |  | 1; |